Паттерн Identity Map в Golang

Kate

Administrator
Команда форума
Identity Map — это паттерн проектирования, предназначенный для управления доступом к объектам, которые загружаются из базы данных. Основная его задача — обеспечить, чтобы каждый объект был загружен только один раз, что предотвращает излишние запросы к базе данных и повышает производительность приложения.

Identity Map можно реализовать в Golang и с помощью него можно управлять объектами более эффективней, сокращая задержки и нагрузку на сервера БД.

Немного про Identity Map​

Паттерн Identity Map работает путём хранения ссылок на уже загруженные объекты в специальной структуре данных — карте.

Когда объект запрашивается из БД, Identity Map сначала проверяет, содержится ли этот объект уже в карте. Если объект находится в карте, он возвращается из неё, избегая нового запроса к БД. Если объект не найден, он загружается из БД, после чего добавляется в карту для будущего использования. Все это обеспечивает целостность ссылок на объекты.

В зависимости от требований и контекста приложения могут использоваться различные типы карт: Explicit, Generic, Session, и Class. Эти типы отличаются уровнем обобщения и специализации в управлении объектами.

Итак, проблемы решает этот паттерн?

  1. Избыточные запросы к БД: основная проблема, которую решает Identity Map, заключается в уменьшении избыточных запросов к БД за одними и теми же объектами.
  2. Несоответствие данных: поскольку каждый объект загружается только один раз и все ссылки на этот объект ведут к одному и тому же экземпляру, Identity Map помогает поддерживать консистентность данных в приложении.
Ну и естественно это уменьшает количество запросов к БД.

Реализация в Golang​

Нужно создать структуру, которая будет функционировать как карта для хранения объектов, загруженных из БД. Основная цель — убедиться, что каждый объект загружается один раз, а дальнейшие запросы к этому же объекту возвращают уже существующий экземпляр из карты.

package main

import (
"fmt"
"sync"
)

type User struct {
ID int
Name string
}

// IdentityMap структура для хранения пользователей
type IdentityMap struct {
sync.RWMutex
users map[int]*User
}

// NewIdentityMap создает новый экземпляр IdentityMap
func NewIdentityMap() *IdentityMap {
return &IdentityMap{
users: make(map[int]*User),
}
}

// Get возвращает пользователя по ID, если он существует в карте
func (im *IdentityMap) Get(id int) *User {
im.RLock()
defer im.RUnlock()
return im.users[id]
}

// Add добавляет пользователя в карту, если его там нет
func (im *IdentityMap) Add(user *User) {
im.Lock()
defer im.Unlock()
if _, ok := im.users[user.ID]; !ok {
im.users[user.ID] = user
}
}

func main() {
identityMap := NewIdentityMap()

// добавление юзеров в карту
identityMap.Add(&User{ID: 1, Name: "Alice"})
identityMap.Add(&User{ID: 2, Name: "Bob"})

// получение пользователей из карты
user := identityMap.Get(1)
fmt.Println("User:", user.Name) // вывод: User: Alice
}
Здесь создали структуру IdentityMap, которая хранит мапу users. Элементы добавляются в эту карту через метод Add, и можно получить их через метод Get. При каждом обращении к методу Get сначала проверяется наличие объекта в карте, и если он есть, возвращаем его, не обращаясь к БД.

Для защиты данных от конкурентного доступа юзаем sync.RWMutex, который позволяет множеству читателей одновременно читать данные, не блокируя их до тех пор, пока не появится писатель.

В микросервисах, где разные сервисы могут работать с одними и теми же данными, важно обеспечить консистентность данных между сервисами. Identity Map можно интегрировать с централизованным кэшем, к примеру как Redis, чтобы управлять объектами на уровне нескольких сервисов:

package main

import (
"fmt"
"github.com/go-redis/redis/v8" // импорт клиента Redis
"context"
)

var ctx = context.Background()

type User struct {
ID int
Name string
}

// клиент Redis для кэширования объектов пользователя
var redisClient *redis.Client

func init() {
redisClient = redis.NewClient(&redis.Options{
Addr: "localhost:6379", // адрес сервера Redis
})
}

// функция для получения пользователя из Redis
func getUserFromCache(id int) *User {
val, err := redisClient.Get(ctx, fmt.Sprintf("user:%d", id)).Result()
if err != nil {
return nil
}
// предполагаем, что данные пользователя сериализованы в JSON
var user User
err = json.Unmarshal([]byte(val), &user)
if err != nil {
return nil
}
return &user
}

// Функция для добавления пользователя в Redis
func addUserToCache(user *User) {
jsonData, err := json.Marshal(user)
if err != nil {
fmt.Println("Error marshalling user:", err)
return
}
redisClient.Set(ctx, fmt.Sprintf("user:%d", user.ID), jsonData, 0) // без истечения срока
}

func main() {
// Добавление и получение пользователя из кэша
user := User{ID: 1, Name: "Alice"}
addUserToCache(&user)

cachedUser := getUserFromCache(1)
if cachedUser != nil {
fmt.Println("Cached User:", cachedUser.Name)
}
}

Данные в Identity Map должны быть всегда актуальными, так как они зачастую измененяются данных. Для этого можно реализовать механизмы инвалидации кэша, когда данные обновляются:

// функция для обновления пользователя
func updateUserInCache(user *User) {
// сначала обновляем данные в БД...
// предполагаем, что БД успешно обновлена

// обновляем данные в кэше
addUserToCache(user)
}

// функция для удаления пользователя из кэша
func deleteUserFromCache(id int) {
redisClient.Del(ctx, fmt.Sprintf("user:%d", id))
}


 
Сверху