TTL, политики вытеснения и maxmemory

Память конечна. Что делает Redis, когда она заканчивается? Ответ зависит от политики вытеснения, и выбрать её — ваша задача.

Без настройки лимита памяти и политики вытеснения Redis рано или поздно упрётся в OOM. С правильной политикой он сам выкинет наименее нужное и продолжит работать.

Redis держит данные в RAM, а её немного. Когда память на исходе, Redis должен решить: отказывать в записи или удалять старые ключи? Это управляется параметром maxmemory и политикой вытеснения (eviction policy).

Лимит памяти

# В redis.conf или через CONFIG SET
CONFIG SET maxmemory 256mb
CONFIG SET maxmemory-policy allkeys-lru

maxmemory задаёт потолок. По достижении вступает в силу политика.

Политики вытеснения

   Что делать при заполнении памяти?

   noeviction      -- отказывать в записи (ошибка). Дефолт.
   allkeys-lru     -- выкинуть давно не используемый ключ
   allkeys-lfu     -- выкинуть редко используемый ключ
   allkeys-random  -- выкинуть случайный ключ
   volatile-lru    -- как allkeys-lru, но только среди
                      ключей С TTL
   volatile-ttl    -- выкинуть ключ с ближайшим истечением
   volatile-lfu / volatile-random -- аналогично, среди ключей с TTL

LRU (Least Recently Used) — выкидывает то, к чему дольше всего не обращались. LFU (Least Frequently Used) — то, к чему обращаются реже всего. Префикс allkeys — среди всех ключей; volatile — только среди ключей с установленным TTL.

Какую политику выбрать

  • allkeys-lru — отличный дефолт для чистого кэша, когда часть данных запрашивают намного чаще остальных (принцип Парето).
  • allkeys-lfu — когда важна именно частота, а не свежесть обращения.
  • volatile-* — когда в Redis есть и кэш (с TTL), и важные данные (без TTL): вытесняется только кэш.
  • noeviction — когда Redis используется как хранилище, и терять данные нельзя (лучше отказ в записи).

Демонстрация: LRU-кэш на dict + OrderedDict

Чтобы понять, как Redis выбирает жертву при allkeys-lru, реализуем LRU-кэш сами:

from collections import OrderedDict

class LRUCache:
    def __init__(self, maxsize):
        self.maxsize = maxsize
        self.data = OrderedDict()

    def get(self, key):
        if key not in self.data:
            return None
        self.data.move_to_end(key)   # обращение -> "освежаем"
        return self.data[key]

    def set(self, key, value):
        if key in self.data:
            self.data.move_to_end(key)
        self.data[key] = value
        if len(self.data) > self.maxsize:
            # вытесняем НАИМЕНЕЕ недавно использованный (слева)
            evicted, _ = self.data.popitem(last=False)
            print(f"  Вытеснен (LRU): {evicted}")

cache = LRUCache(maxsize=3)
for k in ["a", "b", "c"]:
    cache.set(k, k.upper())
print("Кэш заполнен:", list(cache.data.keys()))

cache.get("a")          # обратились к a -> a теперь "свежий"
print("После get('a'):", list(cache.data.keys()))

cache.set("d", "D")     # переполнение -> вытеснится самый старый
print("После set('d'):", list(cache.data.keys()))
print("\nВытеснился 'b' — к нему дольше всего не обращались.")

Именно так Redis при allkeys-lru выбирает жертву: ключ, к которому дольше всего не обращались. Только Redis использует приближённый LRU для скорости.

Как работает под капотом

Точный LRU требовал бы хранить связный список всех ключей и двигать узлы при каждом обращении — дорого. Поэтому Redis использует приближённый LRU: при вытеснении он берёт небольшую случайную выборку ключей и удаляет из неё наименее недавно использованный. Чем больше выборка (maxmemory-samples), тем точнее приближение. LFU работает похоже, но считает частоту обращений со «старением» счётчика.

Частые ошибки

  • Оставить noeviction для кэша. При заполнении памяти записи начнут падать с ошибкой.
  • Ключи без TTL при volatile-политике. Вытеснять будет нечего — снова OOM или отказ.
  • Слишком длинные TTL. Кэш раздувается, вытеснение работает чаще, эффективность падает.

Best practices

  • Для чистого кэша: maxmemory + allkeys-lru (или allkeys-lfu).
  • Задавайте TTL под частоту изменения данных: профили — часы, цены — секунды, конфиги — минуты.
  • Следите за метрикой evicted_keys в INFO: рост = нужно больше памяти или короче TTL.

Итог: maxmemory задаёт лимит, политика вытеснения решает, что удалять при заполнении. allkeys-lru — хороший дефолт для кэша. Redis использует приближённый LRU/LFU ради скорости. TTL подбирайте под частоту изменения данных.

Проверьте себя
1. Что делает Redis по политике allkeys-lru при достижении лимита памяти?
AОтказывает во всех новых записях
BУдаляет ключ, к которому дольше всего не обращались, среди всех ключей
CУдаляет только ключи с истёкшим TTL
DСбрасывает все данные на диск и очищает память
2. Почему Redis использует приближённый (approximate) LRU вместо точного?
AТочный LRU даёт неверные результаты
BТочный LRU требовал бы дорогого учёта порядка всех ключей; приближённый берёт случайную выборку и работает быстрее
CПриближённый LRU надёжнее сохраняет данные
DЭто требование лицензии Redis