golang

Паттерн Identity Map в Golang

  • понедельник, 29 апреля 2024 г. в 00:00:08
https://habr.com/ru/companies/otus/articles/809009/

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

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 рассматривают в рамках практических онлайн-курсов. Подробнее в каталоге.