Что такое TTL в контексте кеширования?

Ответ

TTL (Time To Live) — это механизм, который определяет время жизни записи в кеше. По истечении этого времени запись считается «протухшей» (stale) и подлежит удалению или обновлению при следующем запросе.

Зачем нужен TTL?

  1. Актуальность данных: Гарантирует, что пользователи не будут получать устаревшую информацию.
  2. Управление памятью: Автоматически очищает ненужные данные, предотвращая переполнение кеша и утечки памяти.

Реализация в Go

Существует два основных подхода к реализации TTL-кеша:

  1. Пассивная очистка (Lazy Eviction): Время жизни проверяется только в момент запроса данных (Get). Если запись просрочена, она удаляется и не возвращается пользователю. Это самый простой способ.
  2. Активная очистка (Active Eviction): Фоновая горутина периодически сканирует весь кеш и удаляет просроченные записи. Этот подход лучше управляет памятью, но добавляет сложности.

Для большинства задач проще и надежнее использовать готовые библиотеки, например, github.com/patrickmn/go-cache.

Пример простой реализации (пассивная очистка)

import (
    "sync"
    "time"
)

type CacheItem struct {
    Value    interface{}
    Expiry   int64 // Время истечения в Unix наносекундах
}

type TtlCache struct {
    mu    sync.RWMutex
    items map[string]CacheItem
}

func (c *TtlCache) Set(key string, value interface{}, ttl time.Duration) {
    c.mu.Lock()
    defer c.mu.Unlock()

    if c.items == nil {
        c.items = make(map[string]CacheItem)
    }

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

func (c *TtlCache) Get(key string) (interface{}, bool) {
    c.mu.RLock()
    defer c.mu.RUnlock()

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

    // Проверяем, не истекло ли время жизни
    if time.Now().UnixNano() > item.Expiry {
        // Можно добавить удаление ключа здесь, но это потребует `c.mu.Lock()`
        return nil, false
    }

    return item.Value, true
}

Ответ 18+ 🔞

А, ТТЛ, блядь! Ну это ж классика, ёпта! Это как молоко в холодильнике — стоит поставишь дату, а потом смотришь, а оно уже скисло, пиздец. Вот ТТЛ — это и есть та самая дата на упаковке для данных в кеше.

Ну и нахуя он нужен-то, этот ТТЛ?

  1. Чтобы не жрать просрочку. Представь, ты кешируешь курс доллара, а он там уже неделю как обновился, а у тебя в кеше лежит старая цифра. Клиенту выдашь — он тебе такую ебучую иску влепит, мало не покажется. ТТЛ гарантирует, что данные не залежатся, как тот самый творог на задней полке.
  2. Чтобы память не проебать. Если всё кешировать навечно, то рано или поздно оперативка скажет «ну всё, пидоры, я устала, я ухожу» и твой сервис накроется медным тазом. ТТЛ сам выносит мусор, как образцовый дворник.

Как это в Go сделать, блядь?

Тут два главных пути, как и в жизни: можно быть ленивой жопой, а можно — занудой с графиком уборки.

  1. Ленивая жопа (Пассивная очистка). Проверяем срок годности только когда лезем в холодильник за едой (Get). Открыл баночку, а там плесень — ну нахуй, выкинул и сделал вид, что её и не было. Проще пареной репы.
  2. Зануда с графиком (Активная очистка). Запускаем фоновую уборщицу-горутину, которая раз в N времени проходит по всем полкам и выкидывает всё просроченное. Память чище, но возни больше — надо следить, чтобы уборщица сама не сожрала все ресурсы.

Серьёзно, для прода лучше не выёбывайся и бери готовую библиотеку, типа github.com/patrickmn/go-cache. Там всё уже придумано до нас, эти, умные дядьки.

Ну ладно, вот тебе пример, как ленивая жопа работает:

import (
    "sync"
    "time"
)

type CacheItem struct {
    Value    interface{}
    Expiry   int64 // Когда сдохнуть, в наносекундах Unix
}

type TtlCache struct {
    mu    sync.RWMutex
    items map[string]CacheItem
}

func (c *TtlCache) Set(key string, value interface{}, ttl time.Duration) {
    c.mu.Lock()
    defer c.mu.Unlock()

    if c.items == nil {
        c.items = make(map[string]CacheItem)
    }

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

func (c *TtlCache) Get(key string) (interface{}, bool) {
    c.mu.RLock()
    defer c.mu.RUnlock()

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

    // А не протухло ли?
    if time.Now().UnixNano() > item.Expiry {
        // Тут можно было бы удалить ключ, но для этого нужна уже полная блокировка (Lock), а не RLock.
        // Короче, нахуй, вернём "не найдено".
        return nil, false
    }

    return item.Value, true
}

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