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