Привет, Хабр!
Identity Map — это паттерн проектирования, предназначенный для управления доступом к объектам, которые загружаются из базы данных. Основная его задача — обеспечить, чтобы каждый объект был загружен только один раз, что предотвращает излишние запросы к базе данных и повышает производительность приложения.
Identity Map можно реализовать в Golang и с помощью него можно управлять объектами более эффективней, сокращая задержки и нагрузку на сервера БД.
Немного про Identity Map
Паттерн Identity Map работает путём хранения ссылок на уже загруженные объекты в специальной структуре данных — карте.
Когда объект запрашивается из БД, Identity Map сначала проверяет, содержится ли этот объект уже в карте. Если объект находится в карте, он возвращается из неё, избегая нового запроса к БД. Если объект не найден, он загружается из БД, после чего добавляется в карту для будущего использования. Все это обеспечивает целостность ссылок на объекты.
В зависимости от требований и контекста приложения могут использоваться различные типы карт: Explicit, Generic, Session, и Class. Эти типы отличаются уровнем обобщения и специализации в управлении объектами.
Итак, проблемы решает этот паттерн?
Избыточные запросы к БД: основная проблема, которую решает Identity Map, заключается в уменьшении избыточных запросов к БД за одними и теми же объектами.
Несоответствие данных: поскольку каждый объект загружается только один раз и все ссылки на этот объект ведут к одному и тому же экземпляру, 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)) }
Больше полезных инструментов эксперты OTUS рассматривают в рамках практических онлайн-курсов. Подробнее в каталоге.
