> Какие данные кэшируются и как работает кэширование (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-стеке типичная архитектура выглядит так:

  1. L1-кэш (in-process) - живёт в памяти каждого инстанса. Быстрый доступ (наносекунды), но данные дублируются между подами. Используется для горячих данных с коротким TTL.
  2. 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:

  1. Проверяем L1 → если hit, возвращаем.
  2. Проверяем L2 → если hit, кладём в L1, возвращаем.
  3. Идём в БД → кладём в L2 и L1, возвращаем.

Для надёжности добавляем graceful degradation: если Redis недоступен, работаем только с L1 или напрямую с БД, но логируем ошибку.

Метрики: hit rate, latency p99, размер кэша, количество evictions. Если hit rate ниже 80% - пересматриваем ключи или TTL.

Пример кода

GO
package cache
import (
"context"
"time"
"github.com/allegro/bigcache"
"github.com/redis/go-redis/v9"
"golang.org/x/sync/singleflight"
)
type Cache struct {
l1 *bigcache.BigCache
l2 *redis.Client
sf singleflight.Group
}
func (c *Cache) Get(ctx context.Context, key string, load func() ([]byte, error)) ([]byte, error) {
// L1
if v, err := c.l1.Get(key); err == nil {
return v, nil
}
// singleflight для защиты от stampede
v, err, _ := c.sf.Do(key, func() (interface{}, error) {
// L2
if v, err := c.l2.Get(ctx, key).Bytes(); err == nil {
c.l1.Set(key, v)
return v, nil
}
// DB
data, 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 - если он лежит, сервис должен работать, а не падать.

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

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