> Какие данные кэшируются и как работает кэширование (Go)
Уровень: senior · Роль: backend · Язык: Go · Категория: Технические вопросы
Компании: Wildberries
Стек: Go
> Пример ответа
Короткий ответ
Кэширование в Go-сервисах обычно покрывает три уровня: данные из БД (частые SELECT-запросы), результаты вычислений (тяжёлые функции) и внешние API-вызовы. Основные инструменты - in-memory кэш (sync.Map, bigcache, freecache) и распределённый (Redis, Memcached). Ключевой принцип - кэшировать только то, что дорого пересчитывать и редко меняется, с инвалидацией по TTL или событийной модели. Важно учитывать consistency, memory limits и cache stampede.
Подробное объяснение
Кэширование решает проблему latency и нагрузки на нижележащие системы. В Go-стеке типичная архитектура выглядит так:
- L1-кэш (in-process) - живёт в памяти каждого инстанса. Быстрый доступ (наносекунды), но данные дублируются между подами. Используется для горячих данных с коротким TTL.
- L2-кэш (Redis/Memcached) - общий для всех инстансов. Медленнее (миллисекунды), но консистентнее. Хранит данные, которые должны быть одинаковыми для всех пользователей.
Что кэшировать:
- Результаты агрегаций (count, sum, avg) - пересчёт дорогой.
- Профили пользователей, справочники, конфигурации - редкие изменения.
- Ответы внешних API - если есть rate limit или задержки.
- Сессии и токены - если не используете JWT.
Что не кэшировать:
- Данные, которые меняются каждую секунду (биржевые котировки в реальном времени).
- Персональные данные с высокими требованиями к актуальности (баланс счёта).
- Огромные объекты, которые не влезают в память.
Инвалидация:
- TTL - самый простой, но возможна устаревшая информация.
- Write-through - обновление кэша при записи в БД.
- Event-driven - публикация события об изменении, подписчики сбрасывают ключи.
- Versioning - хранение версии данных в ключе.
Cache stampede - когда TTL истекает, и N запросов одновременно летят в БД. Решения: singleflight (Go-пакет golang.org/x/sync/singleflight), блокировки, раннее обновление (probabilistic early expiration).
На практике
В Go-сервисах часто используют связку: bigcache для L1 (потому что sync.Map не даёт TTL) и Redis для L2. Типичный паттерн - cache-aside:
- Проверяем L1 → если hit, возвращаем.
- Проверяем L2 → если hit, кладём в L1, возвращаем.
- Идём в БД → кладём в L2 и L1, возвращаем.
Для надёжности добавляем graceful degradation: если Redis недоступен, работаем только с L1 или напрямую с БД, но логируем ошибку.
Метрики: hit rate, latency p99, размер кэша, количество evictions. Если hit rate ниже 80% - пересматриваем ключи или TTL.
Пример кода
GOpackage cacheimport ("context""time""github.com/allegro/bigcache""github.com/redis/go-redis/v9""golang.org/x/sync/singleflight")type Cache struct {l1 *bigcache.BigCachel2 *redis.Clientsf singleflight.Group}func (c *Cache) Get(ctx context.Context, key string, load func() ([]byte, error)) ([]byte, error) {// L1if v, err := c.l1.Get(key); err == nil {return v, nil}// singleflight для защиты от stampedev, err, _ := c.sf.Do(key, func() (interface{}, error) {// L2if v, err := c.l2.Get(ctx, key).Bytes(); err == nil {c.l1.Set(key, v)return v, nil}// DBdata, err := load()if err != nil {return nil, err}c.l2.Set(ctx, key, data, 5*time.Minute)c.l1.Set(key, data)return data, nil})if err != nil {return nil, err}return v.([]byte), nil}
Как отвечать на собеседовании
Начните с классификации: что кэшируем и зачем. Затем опишите уровни (L1/L2) и их trade-off. Обязательно упомяните инвалидацию и cache stampede - это показывает понимание проблем, а не только API. Приведите пример из реального проекта: что выбрали, почему, какие метрики получили. Если спросят про Redis - расскажите про структуры данных (не только string), pipeline, cluster mode.
Что проверяет интервьюер
- Понимание разницы между in-memory и distributed кэшем.
- Умение выбирать стратегию инвалидации под конкретный сценарий.
- Знание проблем: consistency, stampede, memory pressure.
- Практический опыт: как дебажили проблемы с кэшем, как меряли hit rate.
- Понимание влияния кэша на общую архитектуру (например, при масштабировании).
Типичные ошибки
- Кэширование всего подряд без анализа стоимости пересчёта.
- Отсутствие TTL или слишком длинный TTL для динамических данных.
- Игнорирование cache stampede - в итоге БД падает при рестарте.
- Использование sync.Map как основного кэша без TTL и eviction.
- Не учитывать сериализацию: хранение Go-структур напрямую в Redis через gob - плохая идея, лучше JSON или protobuf.
- Не проверять ошибки Redis - если он лежит, сервис должен работать, а не падать.
> Похожие задачи по Go
Что происходит если памяти не хватает
Что происходит с персональными данными при обработке
Как подбирать тайм-ауты для backend взаимодействия, например HTTP запросов
Какие требования по шифрованию данных существуют
> Похожие задачи по backend
Что происходит если памяти не хватает
Что происходит с персональными данными при обработке
Как подбирать тайм-ауты для backend взаимодействия, например HTTP запросов
Какие требования по шифрованию данных существуют
> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?
Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью