Стоит ли кэшировать пустые результаты запросов к базе данных
Что проверяют
Понимаете ли вы, зачем кэшируют отсутствие данных, и различаете ли «ключа нет в кэше» и «в кэше лежит пустой ответ». Заодно смотрят на инвалидацию: пустой ответ устаревает ровно так же, как обычный.
Как отвечать
Да, кэшировать стоит, и главная причина не экономия запросов, а защита от пробивания кэша. Если по несуществующему ключу кэш всегда промахивается, поток таких запросов бьёт прямо в базу, а именно такой поток даёт перебор идентификаторов или сломанный клиент.
В Go пустой результат приходит явным признаком, и его важно отделить от настоящей ошибки:
err := db.QueryRowContext(ctx, q, id).Scan(&name)
switch {
case errors.Is(err, sql.ErrNoRows):
cache.Set(key, tombstone, shortTTL)
return nil, ErrNotFound
case err != nil:
return nil, err // ошибку базы не кэшируем
}Условия, при которых приём работает:
- в кэше лежит явный маркер пустоты, иначе нулевое значение структуры не отличить от промаха;
- у маркера свой короткий срок жизни, заметно короче обычного, чтобы появившаяся запись стала видна быстро;
- при создании записи кэш по её ключу чистится сразу, не дожидаясь истечения срока;
- ошибки базы и таймауты не кэшируются никогда, иначе секундный сбой растянется на весь срок жизни записи.
Хорошо добавить про схлопывание одновременных промахов по одному ключу в один поход в базу, для этого в экосистеме есть готовый пакет singleflight. И про фильтр Блума перед кэшем, когда множество существующих ключей известно и велико.
Где спотыкаются
Кладут в кэш нулевое значение структуры вместо маркера. Код читает пустоту, считает её промахом и всё равно идёт в базу: защита не работает, а память расходуется.
Второе: дают отрицательным записям тот же срок жизни, что и обычным. Пользователь только что зарегистрировался, а его профиль ещё несколько минут отвечает «не найдено», потому что отрицательный ответ успел закрепиться.
Третье: путают отсутствие строки с ошибкой запроса. Это разные ветки, и если проверять только факт ненулевой ошибки, недоступная база честно закэшируется как пустой ответ для всех ключей подряд.