Память и политики вытеснения
Redis держит данные в оперативной памяти, а она конечна. Что произойдёт, когда память закончится? Ответ зависит от лимита maxmemory и выбранной политики вытеснения — и от этого выбора зависит, останется ли Redis кэшем или начнёт отвергать записи.
Вытеснение (eviction) — удаление существующих ключей, чтобы освободить место под новые записи, когда использование памяти достигает лимита
maxmemory. Политика вытеснения определяет, какие именно ключи удалять — и удалять ли вообще.
Зачем это на практике
Без явного лимита Redis будет потреблять память, пока операционная система не вмешается: придёт OOM-killer и убьёт процесс, или начнётся своппинг — и латентность Redis, ради которой его и брали, рухнет. Поэтому в проде почти всегда задают maxmemory. Но сам по себе лимит — это лишь граница; что делать при её достижении, решает политика. От неё зависит роль Redis: с вытеснением он работает как кэш (старое уходит, новое влезает), без вытеснения — как хранилище, которое честно отказывает в записи, когда полно.
# redis.conf
maxmemory 2gb
maxmemory-policy allkeys-lru
# то же на лету
redis-cli CONFIG SET maxmemory 2gb
redis-cli CONFIG SET maxmemory-policy allkeys-lru
Политики вытеснения
Политики делятся на две группы по области действия: allkeys-* могут удалить любой ключ, volatile-* — только ключи, у которых задан TTL (срок жизни). Внутри группы различается стратегия выбора жертвы.
| Политика | Что делает |
noeviction | ничего не удаляет; новые записи получают ошибку при достижении лимита |
allkeys-lru | удаляет давно не используемые ключи (LRU) среди всех ключей |
allkeys-lfu | удаляет реже всего используемые ключи (LFU) среди всех |
allkeys-random | удаляет случайный ключ среди всех |
volatile-lru | LRU, но только среди ключей с TTL |
volatile-lfu | LFU, но только среди ключей с TTL |
volatile-ttl | удаляет ключ с TTL, у которого срок истечёт раньше всех |
volatile-random | случайный ключ среди ключей с TTL |
Две аббревиатуры стоит запомнить. LRU (Least Recently Used) выбрасывает ключ, к которому дольше всего не обращались. LFU (Least Frequently Used) — тот, к которому обращались реже всего. Разница принципиальна: к ключу могли обратиться один раз только что (свежий по LRU, но крайне непопулярный по LFU). Для кэша с устойчиво «горячими» элементами LFU обычно точнее.
Как выбрать политику
Если Redis используется как кэш и любой ключ не жалко потерять — allkeys-lru (или allkeys-lfu при чётком разделении горячих/холодных данных) почти всегда правильный выбор. Если Redis — хранилище, где терять данные нельзя, ставьте noeviction: лучше получить ошибку записи и среагировать, чем молча лишиться данных. Политики volatile-* хороши в смешанном сценарии: часть ключей вечная (без TTL, её не тронут), часть — кэш с TTL (только её и вытесняют). А volatile-ttl разумен, когда «скоро и так истечёт» — естественный кандидат на удаление.
Иллюстрация LRU на Python
Чтобы прочувствовать LRU, смоделируем кэш фиксированной ёмкости. OrderedDict помнит порядок вставки/обращения: при доступе к ключу переносим его в конец (он «свежий»), а при переполнении удаляем самый давний — из начала.
from collections import OrderedDict
class LRUCache:
def __init__(self, capacity):
self.capacity = capacity
self.store = OrderedDict()
def get(self, key):
if key not in self.store:
return None
# обращение делает ключ "свежим" — переносим в конец
self.store.move_to_end(key)
return self.store[key]
def set(self, key, value):
if key in self.store:
self.store.move_to_end(key)
self.store[key] = value
if len(self.store) > self.capacity:
# удаляем самый давний ключ (он в начале)
evicted, _ = self.store.popitem(last=False)
print(f"вытеснен ключ: {evicted}")
cache = LRUCache(capacity=3)
cache.set("a", 1)
cache.set("b", 2)
cache.set("c", 3)
cache.get("a") # теперь 'a' свежий, 'b' — самый давний
cache.set("d", 4) # переполнение -> вытесняем 'b'
cache.set("e", 5) # переполнение -> вытесняем 'c'
print("осталось:", list(cache.store.keys()))
Вывод:
вытеснен ключ: b вытеснен ключ: c осталось: ['a', 'd', 'e']
Обратите внимание: хотя a добавили самым первым, обращение get("a") сделало его свежим, поэтому первым вытеснился b. Это и есть суть LRU — «давно не трогали» важнее, чем «давно создали».
Как это работает под капотом
Тонкость в том, что Redis не реализует идеальный LRU/LFU — точный учёт потребовал бы хранить и перестраивать список по всем ключам, а это дорого и по памяти, и по времени. Вместо этого используется аппроксимация на выборке: при необходимости что-то вытеснить Redis берёт несколько случайных ключей (число задаёт maxmemory-samples, по умолчанию 5) и удаляет худший из этой выборки. Больше выборка — точнее приближение к настоящему LRU, но дороже. Для оценки «свежести» в объекте каждого ключа хранится небольшое поле (примерное время последнего доступа для LRU или логарифмический счётчик частоты для LFU).
Ещё важно отделять вытеснение от истечения TTL. Истечение по TTL срабатывает независимо от maxmemory: ключ с истёкшим сроком удаляется либо лениво (при обращении к нему), либо фоновым сэмплированием просроченных ключей. Вытеснение же запускается именно при нехватке памяти. Когда лимит достигнут, Redis на каждой записи освобождает достаточно памяти согласно политике — и только потом применяет команду; при noeviction освобождать нечем, поэтому команда отклоняется.
Частые ошибки
- Не задать
maxmemoryвовсе. Тогда Redis растёт до вмешательства ОС: OOM-kill или своп с обвалом латентности. Всегда ставьте лимит в проде. - Использовать кэш с
noeviction. При заполнении памяти все новые записи начнут падать с ошибкойOOM command not allowed— для кэша это поломка; нужна одна изallkeys-*. - Ставить
volatile-*, не задавая TTL ключам. Если ключей с TTL нет, вытеснять нечего — и при заполнении памяти поведение становится как уnoeviction(записи отвергаются). - Ждать математически точного LRU/LFU. Redis аппроксимирует на выборке; изредка «не самый старый» ключ может быть удалён. Точность регулируется
maxmemory-samplesценой ресурсов. - Путать вытеснение и TTL. TTL удаляет по сроку независимо от памяти; вытеснение — по нехватке памяти. Это разные механизмы, и оба надо учитывать.
Итоги
maxmemoryзадаёт лимит памяти; без него Redis рискует нарваться на OOM-kill или своп.maxmemory-policyрешает, что делать при достижении лимита:noevictionотвергает записи,allkeys-*удаляют любой ключ,volatile-*— только ключи с TTL.- LRU выбрасывает давно не используемый ключ, LFU — реже всего используемый; для кэша обычно подходит
allkeys-lru/allkeys-lfu, для хранилища —noeviction. - Redis использует приближённый LRU/LFU на случайной выборке (
maxmemory-samples), а не точный алгоритм по всем ключам. - Истечение по TTL и вытеснение по памяти — разные механизмы: первый зависит от срока, второй — от заполнения памяти.