> Стоит ли кэшировать пустые результаты запросов к базе данных (Go)

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

Компании: VK

Стек: Go

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

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

Да, кэшировать пустые результаты стоит, но с осторожностью. Пустой результат - это тоже валидный ответ системы, и его отсутствие в кэше приводит к повторным дорогим запросам в БД. Однако нужно различать "пусто, потому что данных нет" и "пусто, потому что ошибка". Кэшировать нужно только первый случай, с коротким TTL и отдельным ключом, чтобы избежать эффекта "dogpile" и проблем с инвалидацией.

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

Пустой результат запроса - это не отсутствие данных, а факт: "по данному условию записей нет". Если не кэшировать такой ответ, каждый повторный запрос будет бить в БД, что особенно болезненно для тяжёлых агрегаций или поисковых запросов.

Основные аргументы за кэширование:

  • Снижение нагрузки на БД - частые запросы с пустым результатом (например, проверка существования пользователя по email) могут создавать значительный трафик.
  • Стабильность latency - пустой ответ обычно быстрее, но сам запрос всё равно выполняется, и при высокой конкуренции это может деградировать.
  • Предсказуемость - кэширование пустых результатов делает поведение системы более равномерным.

Ключевые trade-off:

  • Инвалидация - пустой результат сложнее инвалидировать, потому что нет конкретной записи, на которую можно повесить триггер. Приходится использовать TTL или событийную инвалидацию по типу данных.
  • Риск устаревания - если данные появляются редко, но критичны, короткий TTL обязателен. Иначе пользователь будет видеть "пусто" дольше, чем нужно.
  • Dogpile effect - если TTL истёк одновременно у многих запросов, все они пойдут в БД. Для пустых результатов это особенно опасно, потому что они часто возникают при массовых проверках (например, спам-фильтры).

Практическое правило: кэшировать пустой результат, если:

  • запрос выполняется часто;
  • результат не меняется быстро;
  • стоимость запроса в БД выше стоимости кэша;
  • есть приемлемый TTL (секунды или минуты).

Не кэшировать, если:

  • пустой результат - это редкое исключение, а не норма;
  • данные критичны и должны быть актуальными в реальном времени;
  • запрос уже покрыт другим кэшем (например, по родительской сущности).

На практике

В Go с Redis или in-memory кэшем (например, bigcache или ristretto) пустой результат кэшируется как обычное значение, но с пометкой. Часто используют специальный sentinel-объект, чтобы отличить "пусто" от "нет данных в кэше".

Рекомендации:

  • Используйте отдельный ключ с суффиксом :empty или храните структуру с полем found: false.
  • TTL для пустых результатов делайте короче, чем для обычных (например, 30 секунд против 5 минут).
  • Для критичных данных добавьте механизм принудительной инвалидации при записи (например, при создании новой записи сбрасывайте кэш по соответствующему префиксу).
  • При использовании Redis рассмотрите SETNX для защиты от dogpile - только один запрос идёт в БД, остальные ждут или получают устаревший ответ.

Пример кода

GO
type Cache interface {
Get(ctx context.Context, key string) ([]byte, bool, error)
Set(ctx context.Context, key string, value []byte, ttl time.Duration) error
}
const emptyMarker = "[]" // или специальный sentinel
func GetUsersByFilter(ctx context.Context, cache Cache, db *sql.DB, filter string) ([]User, error) {
key := "users:filter:" + filter
data, found, err := cache.Get(ctx, key)
if err != nil {
// при ошибке кэша - идём в БД, но не пишем в кэш
return fetchFromDB(ctx, db, filter)
}
if found {
if string(data) == emptyMarker {
return []User{}, nil
}
var users []User
if err := json.Unmarshal(data, &users); err != nil {
return nil, err
}
return users, nil
}
users, err := fetchFromDB(ctx, db, filter)
if err != nil {
return nil, err
}
if len(users) == 0 {
// короткий TTL для пустых результатов
cache.Set(ctx, key, []byte(emptyMarker), 30*time.Second)
} else {
data, _ := json.Marshal(users)
cache.Set(ctx, key, data, 5*time.Minute)
}
return users, nil
}

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

Начните с прямого ответа: "Да, стоит, но с оговорками". Затем структурируйте ответ:

  1. Обоснуйте, почему пустой результат - это валидный кэшируемый ответ.
  2. Перечислите условия, при которых кэширование оправдано (частота, стоимость запроса, TTL).
  3. Опишите риски: инвалидация, dogpile, устаревание.
  4. Приведите конкретный пример из практики (например, проверка уникальности email).
  5. Упомяните, как реализовать в Go: sentinel-значение, короткий TTL, защита от dogpile.

Если интервьюер спрашивает про конкретные сценарии - отвечайте через trade-off: "В этом случае я бы кэшировал, потому что..." или "Здесь лучше не кэшировать, так как...".

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

  • Понимание разницы между "нет данных" и "ошибка".
  • Умение оценивать trade-off между нагрузкой на БД и актуальностью данных.
  • Знание паттернов кэширования: TTL, sentinel, dogpile protection.
  • Практический опыт работы с кэшем в Go (Redis, in-memory).
  • Способность принимать инженерные решения, а не следовать шаблонам.

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

  • Категоричный ответ "да" или "нет" без анализа контекста.
  • Игнорирование проблемы инвалидации пустых результатов.
  • Использование одинакового TTL для пустых и непустых ответов.
  • Отсутствие защиты от dogpile при массовых запросах.
  • Кэширование ошибок БД как пустых результатов - это грубая ошибка, которая маскирует сбои.
  • Хранение пустого результата как null в кэше, что делает невозможным отличие от отсутствия ключа.

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

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