> Как реализовать TTL кэш на Redis без использования таблиц (Go)

Уровень: senior · Роль: backend · Язык: Go · Категория: Технические вопросы

Компании: Русклимат

Стек: Redis, Go

> Пример ответа

Короткий ответ

TTL кэш на Redis реализуется через встроенный механизм expiration: команда SET с параметром EX или PX, либо EXPIRE для существующего ключа. Redis удаляет ключи лениво при обращении и активно фоновым процессом. Для Go используем клиент go-redis и метод Set(ctx, key, value, ttl). Никаких таблиц не нужно - Redis сам управляет временем жизни ключей, что даёт атомарность и производительность без дополнительного кода.

Подробное объяснение

Redis предоставляет нативные механизмы TTL, которые не требуют внешних структур данных:

  • Команда SET key value EX seconds - задаёт значение и время жизни атомарно.
  • Команда EXPIRE key seconds - устанавливает TTL для уже существующего ключа.
  • Команда TTL key - возвращает оставшееся время жизни в секундах (-1 - без срока, -2 - ключ не существует).
  • Команда PERSIST key - снимает TTL.

Механизм удаления работает в двух режимах:

  • Ленивое удаление - при попытке доступа к истёкшему ключу Redis возвращает nil и удаляет его.
  • Активное удаление - фоновый цикл периодически сканирует выборку ключей с TTL и удаляет истёкшие.

В Go с клиентом go-redis/v9 это выглядит тривиально:

GO
err := rdb.Set(ctx, "user:123", data, 10*time.Second).Err()

Важные нюансы:

  • TTL задаётся в секундах (EX) или миллисекундах (PX), в Go - через time.Duration.
  • Для атомарной установки с условием используйте SET key value EX ttl NX (только если ключа нет) или XX (только если есть).
  • Истечение не гарантирует мгновенное удаление - ключ может существовать до следующего цикла активной очистки, но для клиента он уже недоступен.
  • Не используйте GET + EXPIRE отдельно - это не атомарно и может привести к гонкам.

На практике

Типичный сценарий - кэширование результата тяжёлой операции:

  1. Пытаемся получить значение по ключу.
  2. Если nil - выполняем операцию, сохраняем результат с TTL.
  3. Если значение есть - возвращаем из кэша.

Дополнительные практики:

  • Используйте префиксы в ключах для логической группировки, например cache:user:123.
  • Для защиты от cache stampede применяйте SET NX EX с коротким TTL на блокировку.
  • Для инвалидации по паттерну используйте SCAN с MATCH и удаляйте ключи через DEL - но это не атомарно и дорого при большом количестве ключей.
  • Если нужно хранить сложные структуры (хэши, списки), TTL задаётся отдельной командой EXPIRE после HSET или LPUSH.

Пример кода

GO
package main
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/redis/go-redis/v9"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
func getUserFromCache(ctx context.Context, rdb *redis.Client, id int) (*User, error) {
key := fmt.Sprintf("cache:user:%d", id)
// 1. Пытаемся получить из кэша
val, err := rdb.Get(ctx, key).Result()
if err == redis.Nil {
// 2. Кэш пуст - грузим из источника
user := &User{ID: id, Name: "Alice"}
data, _ := json.Marshal(user)
// 3. Сохраняем с TTL 5 минут
err = rdb.Set(ctx, key, data, 5*time.Minute).Err()
if err != nil {
return nil, err
}
return user, nil
} else if err != nil {
return nil, err
}
// 4. Декодируем из кэша
var user User
if err := json.Unmarshal([]byte(val), &user); err != nil {
return nil, err
}
return &user, nil
}
func main() {
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
ctx := context.Background()
user, err := getUserFromCache(ctx, rdb, 1)
if err != nil {
panic(err)
}
fmt.Printf("User: %+v\n", user)
}

Как отвечать на собеседовании

Начните с прямого ответа: Redis сам поддерживает TTL через EXPIRE и SET EX, поэтому таблицы не нужны. Затем покажите понимание механики удаления - ленивое и активное. Упомяните атомарность операций и типичный паттерн cache-aside. Если спросят про edge cases, расскажите про cache stampede и решение через SET NX EX. Для senior-уровня добавьте рассуждение о trade-off между точным удалением и производительностью: Redis не гарантирует мгновенное удаление, но для клиента ключ недоступен сразу после истечения.

Что проверяет интервьюер

  • Знание базовых команд Redis и их параметров.
  • Понимание внутреннего механизма expiration.
  • Умение применять TTL в реальном сценарии на Go.
  • Осознание проблем конкурентного доступа и способов их решения.
  • Способность объяснить, почему Redis не требует отдельной таблицы для TTL - это встроенная фича.

Типичные ошибки

  • Попытка реализовать TTL вручную через хранение timestamp и сравнение в коде - избыточно и неатомарно.
  • Использование GET и EXPIRE отдельно вместо одной команды SET EX - гонки.
  • Забывают, что TTL возвращает -2 для несуществующего ключа, и путают с -1.
  • Предположение, что истёкший ключ мгновенно удаляется из памяти - на самом деле удаление может быть отложено.
  • Не учитывают, что SET с TTL перезаписывает значение и сбрасывает TTL - если нужно обновить только значение, используйте SET без TTL, а затем EXPIRE.

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью