Что такое Cache Stampede и как его предотвратить?
Cache Stampede (Thundering Herd) — это ситуация, когда TTL горячего ключа истекает, и множество одновременных запросов обращаются к БД за одними и теми же данными, вызывая её перегрузку.
Аналогия из жизни: представьте кассу в магазине, которая закрылась на перерыв. Все покупатели из её очереди одновременно бросаются в соседнюю кассу, создавая давку, хотя достаточно было бы пустить одного, а остальным подождать.
Как происходит
Пример
1. Ключ "popular-product" в кэше (10 000 запросов/сек)
2. TTL истёк → ключ удалён
3. 100 запросов одновременно видят cache miss
4. 100 одинаковых запросов к БД → перегрузка
5. БД медленно отвечает → ещё больше запросов → каскадный сбой
Решения
1. Locking (единственный запрос обновляет кэш)
Пример кода
public Product getProduct(Long id) {
Product cached = cache.get("product:" + id);
if (cached != null) return cached;
// Только один поток обновляет кэш
String lockKey = "lock:product:" + id;
if (redisTemplate.opsForValue().setIfAbsent(lockKey, "1", Duration.ofSeconds(10))) {
try {
Product product = productRepository.findById(id).orElseThrow();
cache.put("product:" + id, product, Duration.ofMinutes(10));
return product;
} finally {
redisTemplate.delete(lockKey);
}
} else {
// Другие потоки ждут или используют stale значение
Thread.sleep(50);
return getProduct(id); // retry
}
}
2. Probabilistic Early Expiration (XFetch)
Каждый запрос с некоторой вероятностью обновляет кэш до истечения TTL. Чем ближе к истечению — тем выше вероятность.
3. Refresh-Ahead (Caffeine)
Пример
LoadingCache<Long, Product> cache = Caffeine.newBuilder()
.expireAfterWrite(Duration.ofMinutes(30))
.refreshAfterWrite(Duration.ofMinutes(25)) // фоновое обновление за 5 мин до истечения
.build(id -> productRepository.findById(id).orElseThrow());
4. Stale-While-Revalidate
Возвращать протухшее значение, пока фоновый поток обновляет кэш. Пользователь получает ответ мгновенно, а данные обновятся в фоне.
Сравнение решений
| Решение | Сложность | Гарантии | Применимость |
|---|---|---|---|
| Locking | Средняя | Один запрос к БД | Redis (распределённый) |
| Probabilistic Early Expiration | Средняя | Вероятностное | Любой кэш |
| Refresh-Ahead | Низкая | Фоновое обновление | Caffeine (in-process) |
| Stale-While-Revalidate | Средняя | Stale данные на время обновления | HTTP, CDN, custom |
Ключевые принципы
- Cache Stampede опасен для горячих ключей с высоким RPS
- Locking — самый надёжный, но добавляет сложность
- Caffeine
refreshAfterWrite— элегантное решение для in-process кэша - Для Redis — locking через
SETNX+ TTL
Частые ошибки
- Игнорировать проблему — “у нас не бывает” до первой распродажи или launch event
- Lock без TTL — если поток упал, lock навсегда, все ждут, deadlock
- Retry без backoff — 100 потоков спамят retry каждые 50 мс, создавая ту же нагрузку
Как используется в 2026
- Caffeine
refreshAfterWrite— стандарт для L1 - Redis locking через Redisson — для L2
- CDN (Cloudflare, CloudFront) решают stampede на уровне edge
На собеседовании: интервьюер проверяет, сталкивались ли вы с проблемами производительности кэша на практике. Частая ошибка — не знать термин Cache Stampede и не иметь готового решения (locking или refresh-ahead).