Ответ
Кейс: Оптимизация высоконагруженного сервиса рекомендаций за счет реализации многоуровневого кэширования с умной инвалидацией.
Проблема: Сервис, генерирующий персональные рекомендации товаров, испытывал пиковые нагрузки (до 5k RPS). Основная проблема — «горячие» SQL-запросы к базе данных для расчета рекомендаций, которые были тяжелыми (~200мс) и часто возвращали одинаковые данные для пользователей одной сегментной группы. Это приводило к высокой нагрузке на БД и увеличению времени отклика.
Исследование: Анализ показал, что 70% запросов в течение 10-минутного окна приходилось на 3 основных сегмента пользователей. Данные для этих сегментов обновлялись не чаще раза в час.
Решение: Мы внедрили двухуровневую стратегию кэширования в памяти приложения с использованием IMemoryCache в ASP.NET Core.
- Кэш первого уровня (L1): Быстрый in-memory кэш для результатов целых рекомендаций на ключе
"recs:segment:{segmentId}". - Кэш второго уровня (L2): Кэш отдельных «строительных блоков» (например,
"block:popular_in:{categoryId}"), из которых собирается итоговый список рекомендаций. Это позволило переиспользовать данные между разными сегментами.
Ключевая техническая деталь — каскадная инвалидация через IChangeToken:
// Создаем CancellationTokenSource для группы зависимых кэш-ключей
var cts = new CancellationTokenSource();
var changeToken = new CancellationChangeToken(cts.Token);
// Связываем этот токен с несколькими кэш-записями
var cacheOptionsL1 = new MemoryCacheEntryOptions()
.AddExpirationToken(changeToken)
.SetAbsoluteExpiration(TimeSpan.FromMinutes(60));
_memoryCache.Set("recs:segment:123", recommendations, cacheOptionsL1);
_memoryCache.Set("recs:segment:456", otherRecommendations, cacheOptionsL1); // Тот же токен!
// При обновлении данных фоновым джобом инвалидируем ВСЕ связанные записи одной операцией
cts.Cancel(); // Это очистит и "recs:segment:123", и "recs:segment:456"
Дополнительная оптимизация — защита от «Cache Stampede»:
Чтобы при одновременной инвалидации кэша множество параллельных запросов не пошли генерировать данные заново, мы использовали Lazy<T> и SemaphoreSlim для организации «одиночной пересборки» (single recomputation).
Результат:
- Снижение нагрузки на БД: На 75% для эндпоинта рекомендаций.
- Улучшение времени отклика: P95 latency упал с ~250мс до ~15мс.
- Снижение затрат: Уменьшилась необходимость в масштабировании инстансов БД.
Вывод: Этот кейс наглядно показал важность не просто добавления кэша, а проектирования его стратегии инвалидации и учета конкурентного доступа, что часто является более сложной задачей, чем сама реализация кэширования.