Какие существуют стратегии инвалидации кеша в 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, похожий на наш пример, но с фоновой очисткой.