> Использовали ли кэши в 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%.
Пример кода
GOpackage cacheimport ("context""encoding/json""time""github.com/redis/go-redis/v9""golang.org/x/sync/singleflight")type RedisCache struct {client *redis.Clientsf 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.Secondif err := c.client.Set(ctx, key, b, ttl+jitter).Err(); err != nil {return nil, err}return data, nil})if err != nil {return err}// копируем результат в destb, _ := 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, не понимать, когда какой применять.
> Похожие задачи по backend
Какие типы и структуры данных поддерживает Redis
Использовали ли инструменты для асинхронности в Django, например Celery и Redis
> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?
Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью