> Стоит ли кэшировать пустые результаты запросов к базе данных (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 - только один запрос идёт в БД, остальные ждут или получают устаревший ответ.
Пример кода
GOtype 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 = "[]" // или специальный sentinelfunc GetUsersByFilter(ctx context.Context, cache Cache, db *sql.DB, filter string) ([]User, error) {key := "users:filter:" + filterdata, 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 []Userif 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}
Как отвечать на собеседовании
Начните с прямого ответа: "Да, стоит, но с оговорками". Затем структурируйте ответ:
- Обоснуйте, почему пустой результат - это валидный кэшируемый ответ.
- Перечислите условия, при которых кэширование оправдано (частота, стоимость запроса, TTL).
- Опишите риски: инвалидация, dogpile, устаревание.
- Приведите конкретный пример из практики (например, проверка уникальности email).
- Упомяните, как реализовать в Go: sentinel-значение, короткий TTL, защита от dogpile.
Если интервьюер спрашивает про конкретные сценарии - отвечайте через trade-off: "В этом случае я бы кэшировал, потому что..." или "Здесь лучше не кэшировать, так как...".
Что проверяет интервьюер
- Понимание разницы между "нет данных" и "ошибка".
- Умение оценивать trade-off между нагрузкой на БД и актуальностью данных.
- Знание паттернов кэширования: TTL, sentinel, dogpile protection.
- Практический опыт работы с кэшем в Go (Redis, in-memory).
- Способность принимать инженерные решения, а не следовать шаблонам.
Типичные ошибки
- Категоричный ответ "да" или "нет" без анализа контекста.
- Игнорирование проблемы инвалидации пустых результатов.
- Использование одинакового TTL для пустых и непустых ответов.
- Отсутствие защиты от dogpile при массовых запросах.
- Кэширование ошибок БД как пустых результатов - это грубая ошибка, которая маскирует сбои.
- Хранение пустого результата как
nullв кэше, что делает невозможным отличие от отсутствия ключа.
> Похожие задачи по backend
Для чего нужны центры сертификации и чем самоподписанные сертификаты отличаются от выданных центрами
Что такое MITM (man-in-the-middle) атака и как с ней бороться
Что такое негативный кэш и почему он вреден
Является ли одиночный update транзакцией
> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?
Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью