Redis Cache Stampede: Как предотвратить с помощью блокировок и джиттера TTL

Узнайте, как внезапный всплеск запросов к истекшему ключу кэша может перегрузить вашу базу данных и как остановить это с помощью мьютекс-блокировок, вероятностной ранней перекомпьютации и джиттера TTL в Redis.

Redis Cache Stampede: Как предотвратить с помощью блокировок и джиттера TTL

Cache stampede (или cache thundering herd) — это ситуация, когда множество запросов одновременно пытаются обновить истекший ключ кэша, вызывая лавину обращений к базе данных. Это может привести к значительной нагрузке и даже к отказу системы.

Проблема

Представьте, что у вас есть кэшированный результат сложного запроса к базе данных. Когда срок действия кэша истекает, первый запрос, который не находит данные, начинает пересчитывать результат. Однако если в этот момент приходит много других запросов, они все видят, что кэш пуст, и тоже начинают пересчет. В результате база данных получает шквал одинаковых запросов.

Решения

  1. Мьютекс-блокировки (Mutex Locks)

    • Используйте команду SET NX (Set if Not eXists) для установки блокировки на ключ. Только один процесс сможет установить блокировку и начать пересчет. Остальные ждут или используют устаревшие данные.
    • Пример на Python с использованием Redis:
      import redis
      import time
      
      r = redis.Redis()
      lock_key = "lock:mykey"
      cache_key = "mykey"
      ttl = 60
      
      def get_data():
          # Попытка получить данные из кэша
          data = r.get(cache_key)
          if data is not None:
              return data
      
          # Попытка получить блокировку
          lock = r.set(lock_key, "locked", nx=True, ex=10)
          if lock:
              # Пересчет данных (имитация долгой операции)
              time.sleep(2)
              new_data = "expensive result"
              r.setex(cache_key, ttl, new_data)
              r.delete(lock_key)
              return new_data
          else:
              # Ожидание или использование устаревших данных
              time.sleep(0.1)
              return get_data()  # рекурсивная попытка
      
  2. Вероятностная ранняя перекомпьютация (Probabilistic Early Recomputation)

    • Вместо того чтобы ждать истечения TTL, вы можете начать пересчет заранее с некоторой вероятностью. Например, если TTL составляет 60 секунд, то при каждом запросе, когда до истечения остается менее 10 секунд, с вероятностью 50% запускается фоновый пересчет.
    • Это снижает вероятность одновременного истечения многих ключей.
  3. Джиттер TTL (TTL Jitter)

    • Добавьте случайное смещение к TTL каждого ключа, чтобы они истекали не одновременно. Например, вместо фиксированного TTL в 60 секунд используйте значение от 55 до 65 секунд.
    • Пример:
      import random
      base_ttl = 60
      jitter = random.uniform(-5, 5)
      ttl = base_ttl + jitter
      r.setex(cache_key, ttl, data)
      

Заключение

Cache stampede — серьезная проблема, но ее можно эффективно предотвратить с помощью комбинации блокировок, вероятностной ранней перекомпьютации и джиттера TTL. Выбор метода зависит от ваших требований к согласованности данных и допустимой нагрузке на базу данных.