Какие существуют стратегии инвалидации кеша в Go-приложении?

Ответ

Инвалидация кеша — это процесс удаления или пометки данных в кеше как неактуальных. Это одна из самых сложных проблем в кешировании. В Go-приложении можно реализовать несколько основных стратегий.

Рассмотрим на примере простого конкурентно-безопасного кеша:

import (
    "sync"
    "time"
)

type CacheItem struct {
    Value     interface{}
    ExpiresAt time.Time
}

type Cache struct {
    mu    sync.RWMutex // RWMutex для оптимизации чтений
    items map[string]CacheItem
}

func NewCache() *Cache {
    return &Cache{
        items: make(map[string]CacheItem),
    }
}

1. Инвалидация по времени жизни (Time-To-Live, TTL)

Самая распространенная стратегия. Каждой записи в кеше присваивается срок жизни, по истечении которого она считается недействительной. Проверка происходит в момент запроса данных.

// Set добавляет элемент с TTL
func (c *Cache) Set(key string, value interface{}, ttl time.Duration) {
    c.mu.Lock()
    defer c.mu.Unlock()

    c.items[key] = CacheItem{
        Value:     value,
        ExpiresAt: time.Now().Add(ttl),
    }
}

// Get извлекает элемент, проверяя его TTL
func (c *Cache) Get(key string) (interface{}, bool) {
    c.mu.RLock()
    defer c.mu.RUnlock()

    item, found := c.items[key]
    if !found {
        return nil, false
    }

    // Проверка TTL. Если срок истек, элемент считается отсутствующим.
    if time.Now().After(item.ExpiresAt) {
        // В реальном приложении здесь можно запустить горутину для удаления
        // или иметь фоновый процесс очистки.
        return nil, false
    }

    return item.Value, true
}

2. Явная (ручная) инвалидация

Данные удаляются из кеша по прямому вызову, например, после обновления этих данных в базе данных.

// Delete удаляет элемент из кеша по ключу
func (c *Cache) Delete(key string) {
    c.mu.Lock()
    defer c.mu.Unlock()
    delete(c.items, key)
}

// Пример использования:
// db.UpdateUser(user)
// userCache.Delete(user.ID)

3. Политики вытеснения (Eviction Policies)

Когда кеш достигает максимального размера, старые данные вытесняются, чтобы освободить место для новых. Распространенные политики:

  • LRU (Least Recently Used): Вытесняется элемент, который дольше всех не использовался.
  • LFU (Least Frequently Used): Вытесняется элемент, который использовался реже всего.
  • FIFO (First-In, First-Out): Вытесняется самый старый элемент.

Реализация таких политик сложна, поэтому для production-систем настоятельно рекомендуется использовать готовые библиотеки, которые предоставляют эти и другие возможности "из коробки":

  • groupcache: Распределенный кеш от Google.
  • ristretto: Высокопроизводительный, конкурентный кеш с LFU-политикой.
  • go-cache: Простой кеш в памяти с TTL, похожий на наш пример, но с фоновой очисткой.

Ответ 18+ 🔞

Да ты посмотри, какая хуйня творится с этим кешированием! Инвалидация кеша — это, блядь, процесс удаления или пометки данных в кеше как неактуальных. Одна из самых ебученных проблем, на которую можно налететь, когда начинаешь с кешем работать. В Go-приложении можно, конечно, нагородить несколько стратегий, но готовься, что мозги набекрень вывернет.

Вот, смотри, пример простого конкурентно-безопасного кеша, чтоб хотя бы не грохнулось всё в один момент:

import (
    "sync"
    "time"
)

type CacheItem struct {
    Value     interface{}
    ExpiresAt time.Time
}

type Cache struct {
    mu    sync.RWMutex // RWMutex для оптимизации чтений
    items map[string]CacheItem
}

func NewCache() *Cache {
    return &Cache{
        items: make(map[string]CacheItem),
    }
}

1. Инвалидация по времени жизни (Time-To-Live, TTL)

Ну, самая, блядь, распространённая. Каждой записи в кеше присваивается срок жизни, как молоку в холодильнике. Кончился срок — нахуй, в мусорку. Проверка происходит, когда кто-то пришёл за данными.

// Set добавляет элемент с TTL
func (c *Cache) Set(key string, value interface{}, ttl time.Duration) {
    c.mu.Lock()
    defer c.mu.Unlock()

    c.items[key] = CacheItem{
        Value:     value,
        ExpiresAt: time.Now().Add(ttl),
    }
}

// Get извлекает элемент, проверяя его TTL
func (c *Cache) Get(key string) (interface{}, bool) {
    c.mu.RLock()
    defer c.mu.RUnlock()

    item, found := c.items[key]
    if !found {
        return nil, false
    }

    // Проверка TTL. Если срок истек, элемент считается отсутствующим.
    if time.Now().After(item.ExpiresAt) {
        // В реальном приложении здесь можно запустить горутину для удаления
        // или иметь фоновый процесс очистки.
        return nil, false
    }

    return item.Value, true
}

2. Явная (ручная) инвалидация

Тут всё просто, как три копейки. Данные удаляются из кеша по прямому вызову, например, после того как ты эти же данные в базе обновил. Логично же, ёпта? В кеше теперь лежит хуйня старая.

// Delete удаляет элемент из кеша по ключу
func (c *Cache) Delete(key string) {
    c.mu.Lock()
    defer c.mu.Unlock()
    delete(c.items, key)
}

// Пример использования:
// db.UpdateUser(user)
// userCache.Delete(user.ID)

3. Политики вытеснения (Eviction Policies)

А вот это уже, блядь, ёперный театр! Когда кеш достигает максимального размера, старые данные надо выкидывать, чтобы место для новых было. Как в коммуналке, когда новый жилец приезжает. Основные политики:

  • LRU (Least Recently Used): Вытесняется тот, кого дольше всех не трогали. Забытый, блядь, сирота.
  • LFU (Least Frequently Used): Вытесняется тот, кого реже всего спрашивали. Непопулярный, сука.
  • FIFO (First-In, First-Out): Вытесняется тот, кто первым зашёл. Старейшина, нахуй.

Реализовать эту хуйню с нуля — это ж мозги вытекут, честно. Поэтому для продакшена бери готовые библиотеки, они уже всё за тебя придумали, эти мудя:

  • groupcache: Распределённый кеш от гугла, серьёзная штука.
  • ristretto: Высокопроизводительный, конкурентный кеш с LFU-политикой. Быстрый, как удар током.
  • go-cache: Простой кеш в памяти с TTL, похожий на наш пример, но там уже фоновый уборщик мусора бегает.