Паттерн Identity Map в Golang

—

от автора

Привет, Хабр!

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)) }

Больше полезных инструментов эксперты OTUS рассматривают в рамках практических онлайн-курсов. Подробнее в каталоге.


ссылка на оригинал статьи https://habr.com/ru/articles/809009/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *