> Использовали ли кэши в Go, например Redis, и как кэшировали (Redis, Go)

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

Компании: Wildberries

Стек: Redis, Go

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

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

Да, использовал Redis в Go-сервисах для кэширования горячих данных: профилей пользователей, справочников, результатов вычислений. Основной подход - cache-aside с TTL, ключи с префиксами и версионированием, сериализация в JSON или MessagePack. Для защиты от каскадных сбоев применял singleflight, для согласованности - инвалидацию по событийной шине. В высоконагруженных местах использовал локальный in-memory кэш первого уровня (например, bigcache) с Redis как вторым уровнем.

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

Кэширование в Go с Redis обычно строится вокруг нескольких паттернов:

  • Cache-aside (lazy loading): приложение сначала проверяет Redis, при промахе читает из БД, кладёт в Redis с TTL. Простой, предсказуемый, но есть риск устаревания данных.
  • Write-through / write-behind: запись сразу в кэш и БД (или асинхронно). Используется реже, когда чтение критичнее записи.
  • Инвалидация: удаление ключа при изменении данных - через событийную шину (Kafka, NATS) или явные вызовы в сервисе-владельце данных.

Ключевые решения при проектировании:

  • Формат ключа: namespace:entity:version:id - например, user:profile:v2:12345. Версия позволяет массово мигрировать формат.
  • TTL: подбирается под бизнес-логику. Для справочников - 5-15 минут, для сессий - 30-60 минут, для тяжёлых агрегатов - до часа.
  • Сериализация: encoding/json для простоты, MessagePack или protobuf для производительности. При больших объёмах - сжатие (snappy, zstd).
  • Защита от проблем:
    • Cache stampede - одновременные запросы при инвалидации. Решается singleflight (golang.org/x/sync/singleflight) или блокировкой на Redis (SET NX).
    • Thundering herd - то же самое, но на уровне БД. Помогает случайный TTL (jitter).
    • Hot keys - один ключ читается очень часто. Решается локальным кэшем или шардированием ключа.
    • Согласованность - при обновлении данных удаляем ключ, а не обновляем. Это снижает риск гонок.

Для локального кэша в Go часто используют bigcache или freecache - они не требуют GC-нагрузки. Синхронизация между уровнями - через короткий TTL (1-5 секунд) или подписку на инвалидацию.

На практике

В реальных проектах я обычно делал так:

  • Отдельный пакет cache с интерфейсом Get/Set/Delete, реализацией на go-redis/v9 и mock для тестов.
  • Обёртка над singleflight - чтобы не дублировать логику в каждом сервисе.
  • Метрики: hit rate, latency, ошибки Redis - через Prometheus.
  • Graceful degradation: если Redis недоступен, идём в БД напрямую, но логируем и ставим аларм.

Пример из практики: сервис рекомендаций кэшировал топ-100 элементов для каждого пользователя. Ключ - recs:user:{id}:v3, TTL - 10 минут, singleflight на уровне запроса. При обновлении модели - инвалидация через Kafka-событие. Hit rate держался на уровне 95-98%.

Пример кода

GO
package cache
import (
"context"
"encoding/json"
"time"
"github.com/redis/go-redis/v9"
"golang.org/x/sync/singleflight"
)
type RedisCache struct {
client *redis.Client
sf singleflight.Group
}
func (c *RedisCache) Get(ctx context.Context, key string, dest any) error {
data, err := c.client.Get(ctx, key).Bytes()
if err == redis.Nil {
return ErrMiss
}
if err != nil {
return err
}
return json.Unmarshal(data, dest)
}
func (c *RedisCache) GetOrLoad(ctx context.Context, key string, ttl time.Duration, dest any, load func() (any, error)) error {
err := c.Get(ctx, key, dest)
if err == nil {
return nil
}
if err != ErrMiss {
return err
}
v, err, _ := c.sf.Do(key, func() (any, error) {
// повторная проверка - вдруг другой запрос уже загрузил
if err := c.Get(ctx, key, dest); err == nil {
return dest, nil
}
data, err := load()
if err != nil {
return nil, err
}
b, err := json.Marshal(data)
if err != nil {
return nil, err
}
// jitter для избегания одновременного истечения
jitter := time.Duration(rand.Intn(10)) * time.Second
if err := c.client.Set(ctx, key, b, ttl+jitter).Err(); err != nil {
return nil, err
}
return data, nil
})
if err != nil {
return err
}
// копируем результат в dest
b, _ := json.Marshal(v)
return json.Unmarshal(b, dest)
}
func (c *RedisCache) Invalidate(ctx context.Context, key string) error {
return c.client.Del(ctx, key).Err()
}

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

Начни с конкретного примера: где именно использовал Redis, какие данные кэшировал и почему. Затем опиши паттерн (cache-aside), формат ключей, TTL и как решал проблему согласованности. Обязательно упомяни singleflight - это показывает понимание реальных проблем. Если спросят про локальный кэш - расскажи про bigcache и когда он нужен.

Не углубляйся в синтаксис go-redis, если не просят. Лучше показать архитектурное мышление: как выбирал между cache-aside и write-through, как боролся с hot keys, как деградировал при сбое Redis.

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

  • Понимание trade-off между кэшированием и согласованностью.
  • Умение проектировать ключи и TTL под бизнес-задачу.
  • Знание типовых проблем: stampede, hot keys, инвалидация.
  • Практический опыт с go-redis и инструментами защиты (singleflight).
  • Умение объяснить, почему выбрал именно этот подход, а не другой.

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

  • Говорить "кэшировал всё подряд" без конкретики - это сразу снижает уровень.
  • Не упоминать инвалидацию - интервьюер решит, что не сталкивался с реальными данными.
  • Использовать только JSON без обсуждения альтернатив - показывает неглубокий опыт.
  • Забывать про деградацию при отказе Redis - критично для senior.
  • Путать cache-aside и write-through, не понимать, когда какой применять.

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

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