Identity Map Pattern in Golang

Identity Map can be implemented in Golang and can be used to manage objects more efficiently, reducing latency and load on database servers.

A little about Identity Map

The Identity Map pattern works by storing references to already loaded objects in a special data structure – map.

When an object is queried from the database, Identity Map first checks to see if the object is already contained in the map. If the object is in the map, it returns from it, avoiding a new query to the database. If an object is not found, it is loaded from the database and then added to the map for future use. All this ensures the integrity of object references.

Depending on the requirements and context of the application, different types of cards can be used: Explicit, Generic, Session, and Class. These types differ in the level of generalization and specialization in object management.

So, does this pattern solve problems?

  1. Redundant database queries: The main problem that Identity Map solves is to reduce redundant database queries for the same objects.

  2. Data inconsistency: Because each object is loaded only once and all references to that object point to the same instance, an Identity Map helps maintain data consistency across an application.

Well, of course, this reduces the number of queries to the database.

Implementation in Golang

We need to create a structure that will function as a map for storing objects loaded from the database. The main goal is to ensure that each object is loaded once, and further requests to the same object return an already existing instance from the 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
}

A structure has been created here IdentityMapwhich stores the map users. Elements are added to this map via the method Addand you can get them through the method Get. Each time the method is called Get First, the presence of an object in the map is checked, and if it exists, we return it without accessing the database.

To protect data from competitive access we use sync.RWMutexwhich allows multiple readers to read data simultaneously without blocking until a writer arrives.

In microservices, where different services may work on the same data, it is important to ensure data consistency between services. Identity Map can be integrated with a centralized cache, such as Redis, to manage objects at the level of several services:

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

The data in the Identity Map must always be up to date, as the data changes frequently. To do this, you can implement cache invalidation mechanisms when data is updated:

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

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

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

OTUS experts discuss more useful tools in practical online courses. More details in the catalog.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *