Gymterview
senior

Что такое 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).