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 подбирайте под частоту изменения данных.