Вопрос проверяет, понимаете ли вы, что кеш — это не только “кешировать весь ответ”, и умеете ли вы находить переиспользуемые части вычислений и снижать нагрузку даже при уникальных ответах.
Если полный ответ уникален, кешируют не “ответ целиком”, а его части: справочники, настройки, права, данные профиля, результаты тяжёлых подзапросов или промежуточные вычисления. Часто помогает кеш на уровне “строительных блоков”: отдельные сущности по ключу, а итоговый ответ собирается из них. Можно кешировать не данные, а “защиту от перегрузки”: лимиты, локальные prefetch, короткий TTL на часто запрашиваемые компоненты. Ещё один вариант — кешировать отрицательные результаты (например, “нет данных”) и использовать stale-данные при временных сбоях.
Уникальность итогового ответа не означает, что все вычисления внутри него уникальны. Обычно есть общие фрагменты, которые повторяются между пользователями или между запросами одного пользователя.
Вместо ключа вида cache:user:<id>:full_response выбирают ключи для компонентов:
справочники (countries, currencies, categories)
настройки/фичи (feature_flags)
профиль пользователя (user_profile)
права/роли (user_permissions)
часто используемые сущности по id (товар, отель, город)
Сборка ответа тогда становится:
взять несколько кусочков из кеша/БД
склеить в ответ
Если есть дорогая операция (например, расчёт скоринга/фильтров):
кешируйте результат этой операции на короткий TTL
ключ строится по входным параметрам, а не по пользователю целиком
Определение: Memoization — запоминание результата функции по входам, чтобы не считать повторно.
Пример идеи ключа:
score:<segment>:<param_hash>
Частая проблема: несколько горутин одновременно запрашивают один и тот же ресурс.
Решение:
кеш сущностей в Redis с TTL
дедупликация “в полёте” (singleflight) внутри сервиса
Пример singleflight (коротко):
import "golang.org/x/sync/singleflight"
var g singleflight.Group
func getHotel(ctx context.Context, id string) (any, error) {
v, err, _ := g.Do("hotel:"+id, func() (any, error) {
// 1) попробовать Redis
// 2) иначе БД
// ... детали опущены
return fetchFromDB(ctx, id)
})
return v, err
}
Иногда полезно кешировать:
“нет данных” (чтобы не долбить БД при пустых выдачах)
частичный результат (например, только список id, а детали догружаются отдельно)
Важно:
TTL для негативного кеша обычно короткий, чтобы не “залипнуть” на пустоте
Определение: Stale-while-revalidate — можно отдать слегка устаревшие данные быстро, а обновление сделать в фоне.
Подход:
при истечении TTL не блокировать пользователя долгим пересчётом
отдавать “последнее значение”, а пересчёт запускать асинхронно
измеряйте hit rate и latency
задавайте разумные TTL и лимиты размера
продумывайте инвалидацию: TTL + события, если есть стрим изменений
избегайте “кеша на миллионы уникальных ключей” без ограничений (память улетит)
Если ответы уникальны, кешируют не ответ целиком, а переиспользуемые части и промежуточные вычисления, плюс применяют дедупликацию запросов и stale-стратегии. Это снижает нагрузку и стабилизирует latency даже без повторяемости полного ответа.