Redis — это хранилище в оперативной памяти. Казалось бы, процесс упал — данные пропали. На самом деле Redis умеет сохраняться на диск, выдерживать отказ сервера и масштабироваться на несколько машин. Разберём, как это работает и какие настройки выбрать.
Пространство ключей разрезано на 16384 слота, и каждый слот закреплён за узлом. Номер слота считается из имени ключа, поэтому клиент знает адрес заранее и ходит на один узел, а не опрашивает все. Когда узел отвечать перестаёт, его слоты целиком переезжают на replica — диапазон не меняется, меняется только машина, которая за него отвечает.
Персистентность: RDB и AOF
По умолчанию Redis хранит данные только в памяти. Если сервер перезапустится — всё пропадёт. Чтобы этого не было, есть два механизма сохранения.
RDB (Redis Database) — это снапшот: Redis делает полный «снимок» данных на диск в двоичном формате (dump.rdb). Снапшот можно делать по расписанию или вручную командой SAVE / BGSAVE. BGSAVE запускает дочерний процесс и не блокирует Redis.
# Сохранить снапшот вручную (асинхронно, не блокирует)
BGSAVE
# Посмотреть, когда было последнее сохранение
LASTSAVE
Конфигурация в redis.conf:
# Сохранять снапшот если за 60 секунд изменилось хотя бы 1000 ключей
save 60 1000
# Файл снапшота
dbfilename dump.rdb
dir /var/lib/redis
Плюс RDB: компактный файл, быстрый старт после перезапуска. Минус: если сервер упал между снапшотами — данные за этот промежуток теряются.
Есть и второй минус, менее очевидный и куда более болезненный. «Не блокирует Redis» не значит «бесплатно». Дочерний процесс получает не копию данных, а те же самые страницы памяти, и копируются они по мере изменения: пока идёт снимок, каждая запись в Redis отщепляет ещё одну страницу. При активной записи потребление памяти за время снимка может вырасти почти вдвое. Именно так Redis чаще всего и погибает: снимок стартовал в час пик, памяти не хватило, ядро убило процесс. Отсюда практическое правило — держать maxmemory заметно ниже объёма памяти машины, с запасом на снимок.
И ещё одна строка, о которой вспоминают только в момент аварии: по умолчанию включён режим stop-writes-on-bgsave-error yes. Если очередной снимок не удался — кончилось место на диске, не хватило прав на каталог, — Redis начинает отказывать во всех записях, пока снимок не пройдёт успешно. Со стороны приложения это выглядит загадочно: кэш вдруг отвечает ошибкой на каждый SET, хотя память свободна и сервер жив. Причина при этом лежит в журнале Redis одной строкой про неудавшийся BGSAVE.
AOF (Append Only File) — это журнал команд: каждая команда записи дописывается в файл (appendonly.aof). При рестарте Redis просто «воспроизводит» весь файл заново.
# Включить AOF
appendonly yes
appendfilename "appendonly.aof"
# Политика fsync (сброс на диск):
# always — после каждой команды (максимальная надёжность, медленнее)
# everysec — раз в секунду (хороший баланс, потеря максимум 1 с данных)
# no — ОС решает сама (быстро, ненадёжно)
appendfsync everysec
Плюс AOF: теряете максимум одну секунду данных. Минус: файл растёт, старт медленнее. Redis умеет периодически «сжимать» AOF (BGREWRITEAOF), убирая лишние команды. С Redis 7.0 журнал лежит не одним файлом: базовый снимок и добавочные хранятся в каталоге appendonlydir, а appendfilename задаёт лишь основу их имён.
Короткая формула: для важных данных включайте и RDB, и AOF одновременно — Redis умеет работать с обоими. При старте приоритет у AOF.
Одна и та же авария в двух режимах: смотрите на правый столбец, там вся разница между снимком раз в пять минут и журналом с ежесекундным сбросом.
Память и политики вытеснения
Redis живёт в RAM, значит нужно задать лимит — иначе он съест всю память сервера.
# Лимит памяти (например, 512 мегабайт)
maxmemory 512mb
# Что делать когда лимит достигнут — политика вытеснения
maxmemory-policy allkeys-lru
Политики вытеснения — что Redis делает, когда памяти не осталось (по умолчанию noeviction):
| Политика | Поведение |
|---|---|
noeviction | Отказывать в записи ошибкой (данные не трогает) |
allkeys-lru | Вытеснять любые ключи по принципу «давно не использовался» |
volatile-lru | Вытеснять только ключи с TTL, по LRU |
allkeys-lfu | Вытеснять любые ключи по принципу «реже всего запрашивался» |
volatile-lfu | То же, но только среди ключей с TTL |
allkeys-random | Вытеснять любые ключи случайно |
volatile-ttl | Вытеснять ключи с наименьшим оставшимся TTL |
volatile-random | Вытеснять ключи с TTL случайно |
Когда что выбирать:
- Redis как кэш (данные можно потерять) →
allkeys-lru. Самый распространённый выбор. - Если в кэше есть заметно «горячие» ключи, которые нужны постоянно, →
allkeys-lfu: он смотрит не на время последнего обращения, а на частоту. - Redis как кэш, но только часть ключей с TTL →
volatile-lru. - Redis как основное хранилище (нельзя терять данные) →
noevictionплюс контроль памяти со стороны приложения.
Рядом со значением Redis держит две пометки: когда к ключу обращались в последний раз и как часто. Первую смотрит LRU, вторую — LFU:
живой пример
import java.util.LinkedHashMap;
import java.util.Map;
public class EvictionDemo {
record Stat(long lastUsed, long hits) {}
public static void main(String[] args) {
Map<String, Stat> keys = new LinkedHashMap<>();
String[] calls = {"user:42", "user:42", "user:42", "user:42", "user:42",
"report:jan", "report:feb", "report:jan"};
for (int tick = 1; tick <= calls.length; tick++) {
Stat was = keys.getOrDefault(calls[tick - 1], new Stat(0, 0));
keys.put(calls[tick - 1], new Stat(tick, was.hits() + 1));
}
String lru = null, lfu = null;
for (String key : keys.keySet()) {
if (lru == null || keys.get(key).lastUsed() < keys.get(lru).lastUsed()) lru = key;
if (lfu == null || keys.get(key).hits() < keys.get(lfu).hits()) lfu = key;
}
keys.forEach((key, stat) -> System.out.println(
key + ": обращений " + stat.hits() + ", последнее в такт " + stat.lastUsed()));
System.out.println("allkeys-lru выбросит " + lru);
System.out.println("allkeys-lfu выбросит " + lfu);
}
}
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
LRU выбросит user:42 — к нему обращались давно, хотя чаще всех; LFU выбросит report:feb и оставит горячий ключ. Частоту Redis считает приблизительно и постепенно обнуляет (lfu-log-factor, lfu-decay-time), иначе ключ, популярный год назад, не вытеснился бы никогда.
Одну вещь эта программа приукрашивает, и её стоит проговорить. Она перебирает все ключи и находит худший честно — настоящий Redis так не делает. Перебирать миллионы ключей на каждой нехватке памяти слишком дорого, поэтому он берёт случайную горстку (по умолчанию пять штук, настройка maxmemory-samples) и выбрасывает худший из неё. Значит, и LRU, и LFU здесь — приближения: вылетит действительно холодный ключ, но не обязательно самый холодный из всех. Увеличенная выборка даёт результат ближе к идеальному и стоит дороже по процессору.
В Spring Boot @Cacheable о вытеснении не знает: при следующем обращении к выброшенному ключу значение просто вычислится заново.
Сколько памяти заложить
Число в maxmemory — не размер машины, и разница между ними существенна.
Запас на снимок. Когда Redis сохраняет данные на диск, он порождает дочерний процесс, который видит те же страницы памяти; страницы, изменённые во время сохранения, копируются. На нагруженной записью базе это может дать заметный дополнительный расход — в худшем случае до удвоения. Поэтому maxmemory ставят заметно ниже физической памяти: обычно 60–70 % от неё, а не 90 %.
Буферы клиентов. Каждое соединение имеет буфер вывода, и он не входит в учёт данных. Опасен здесь не обычный клиент, а реплика: её буфер (client-output-buffer-limit replica) должен вмещать поток изменений за время первичной синхронизации. Если он переполнится, сервер разорвёт соединение с репликой, та начнёт синхронизацию заново — и так по кругу, бесконечной чередой полных перекачек. Симптом узнаваемый: реплика никогда не догоняет, а в журнале повторяются сообщения о начале полной синхронизации.
Фрагментация. Отношение занятой процессом памяти к учтённым данным (mem_fragmentation_ratio в INFO memory): около 1,0–1,5 — норма, 2,0 и выше — память тратится впустую, помогает включение фоновой дефрагментации (activedefrag yes). Значение меньше 1,0 означает, что часть памяти ушла в подкачку, и это уже авария: Redis с подкачкой перестаёт быть быстрым.
Частичная пересинхронизация
Разрыв связи с репликой не обязан означать полную перекачку данных. Мастер держит кольцевой буфер последних изменений (repl-backlog-size), и если реплика вернулась, пока её позиция ещё в буфере, она догоняет частично — получает только пропущенное.
Отсюда практический смысл настройки: буфер должен покрывать типичное время разрыва. При потоке записи в десять мегабайт в секунду и желании пережить минутный разрыв нужно порядка шестисот мегабайт — значение по умолчанию (единицы мегабайт) переживает лишь секунды. Проверяется это по счётчикам sync_full и sync_partial_ok в INFO stats: если полных синхронизаций много, буфер мал.
Кластер: чего не умеет и что придётся делать руками
Три вещи, которые определяют жизнь с кластером.
Минимум три мастера. Кластер рассчитан на решение большинством, и с двумя узлами оно не работает. К каждому мастеру обычно добавляют реплику — то есть настоящий минимум это шесть процессов, желательно на разных машинах.
Команды на несколько ключей не работают между слотами. MGET a b c, SINTER, транзакция и скрипт на Lua, если ключи попали в разные слоты, отвечают ошибкой CROSSSLOT Keys in request don't hash to the same slot. Лечится хеш-тегом: часть ключа в фигурных скобках определяет слот, поэтому user:{42}:profile и user:{42}:cart гарантированно лежат вместе. Планировать это надо заранее: переименовать ключи в работающей системе дороже, чем придумать схему сразу.
Переезд слотов — отдельная операция. Добавление узла не перераспределяет данные само: слоты переносят командой перебалансировки, и это онлайновый процесс (ключи мигрируют пачками, клиенты получают перенаправления). Он безопасен, но не мгновенен и нагружает кластер, поэтому его планируют, а не делают в час пик.
Пороги для мониторинга
Поля INFO полезны с числами, а не сами по себе.
Коэффициент попаданий (keyspace_hits к сумме с keyspace_misses): для кэша ниже 80 % — повод разбираться, ниже 50 % — кэш скорее мешает, чем помогает.
Вытеснения (evicted_keys): нули при политике вытеснения означают, что памяти хватает. Постоянный поток вытеснений — это работающая политика; тревожит не сам факт, а рост темпа и одновременное падение попаданий: значит, из памяти выбрасывается то, что ещё нужно.
Отказы по памяти (рост ошибок записи OOM command not allowed) при политике noeviction — авария: приложение уже не может писать.
Заблокированные клиенты (blocked_clients) — те, кто ждёт на блокирующих командах. Небольшое постоянное число нормально для очередей на списках; рост означает, что обработчики не справляются.
Задержка. SLOWLOG GET показывает последние медленные команды с их аргументами — первое место, куда смотрят при жалобе на подтормаживания; redis-cli --latency измеряет задержку самого сервера, --latency-history показывает её во времени.
Отставание реплики (master_repl_offset против позиции реплики, lag в INFO replication) — метрика, по которой ставят оповещение: отставание в секунды означает, что чтение с реплики отдаёт устаревшие данные.
Свой Redis, управляемый сервис или Valkey
Вопрос «что именно вы эксплуатируете» стоит первым. Свой экземпляр на своей машине — полный контроль и полная ответственность: обновления, безопасность, переключение при отказе, резервные копии, дежурство. Управляемый сервис снимает это за деньги и ограничивает: часть команд запрещена, настройки доступны не все, версия обновляется по расписанию провайдера.
Отдельный вопрос — какой именно продукт. После смены лицензии Redis появился форк Valkey под свободной лицензией, поддержанный крупными игроками; он совместим по протоколу и командам, и часть облаков предлагает именно его. Практический вывод: при выборе между «Redis» и «Valkey» смотрят не на возможности (они совпадают), а на то, что поддерживает ваш провайдер и под какой лицензией вам можно это использовать.
Репликация: primary и replica
Репликация позволяет держать несколько копий данных на разных серверах. Один сервер — primary (принимает записи), остальные — replica (только чтение, синхронизируются с primary).
# На replica в redis.conf (или в CLI):
REPLICAOF 192.168.1.10 6379
Replica постоянно получает поток изменений от primary. При первом подключении primary делает BGSAVE, отправляет снапшот и досылает накопленные команды — так replica догоняет.
Что даёт репликация:
- Горизонтальное масштабирование чтения — приложение читает с replica, снижая нагрузку на primary.
- Резервная копия — если primary упадёт, replica хранит данные.
Само по себе replica не переключается в режим primary при сбое — для этого нужен Sentinel.
И главное свойство, о котором легко забыть: репликация асинхронная. Primary отвечает клиенту «записал» сразу, не дожидаясь, пока изменение доедет до replica. Значит, при переключении часть уже подтверждённых записей исчезнет — те, что успели попасть на primary и не успели на копию. Полностью это не лечится: синхронной репликации у Redis нет. Ограничить можно: min-replicas-to-write 1 запрещает primary принимать записи, когда с ним не на связи ни одной replica, а команда WAIT позволяет приложению дождаться, пока конкретная запись доедет до нужного числа копий. Оба приёма замедляют запись, поэтому их включают там, где потеря дороже задержки.
Sentinel: отказоустойчивость
Redis Sentinel — это набор процессов-наблюдателей, которые следят за primary и replica. Если primary стал недоступен, Sentinel голосованием выбирает новый primary из replica и оповещает приложения.
# sentinel.conf — минимальная конфигурация
sentinel monitor mymaster 192.168.1.10 6379 2
# «mymaster» — имя кластера
# 2 — кворум: сколько Sentinel должны согласиться, что primary недоступен
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 60000
Типичная схема: 3 узла Sentinel (нечётное число для кворума), 1 primary, 1-2 replica. Приложение подключается к Sentinel, тот сообщает актуальный адрес primary.
В этой схеме кворум и большинство случайно совпали, и из-за этого их часто путают. Это разные числа с разными задачами. Кворум отвечает только на вопрос «считаем ли primary упавшим»: столько наблюдателей должны увидеть его недоступным, чтобы объявить отказ. А чтобы кто-то из них действительно провёл переключение, нужно согласие большинства всех Sentinel — и это большинство считается от их общего числа, настройкой его не понизить. Поэтому кворум меньше большинства ничего не ускоряет: при трёх наблюдателях и кворуме 1 отказ заметит один, а переключать всё равно будут двое. И поэтому же двух Sentinel недостаточно: стоит одному из них самому оказаться недоступным — большинства уже не собрать, и переключения не будет.
Три кадра переключения подряд: обратите внимание, что кворум только признаёт отказ, а переключение проводит большинство всех наблюдателей.
В Spring Boot Sentinel настраивается через application.yml:
spring:
data:
redis:
sentinel:
master: mymaster
nodes:
- sentinel1:26379
- sentinel2:26379
- sentinel3:26379
Cluster: шардирование
Redis Cluster нужен когда данных слишком много для одного сервера или нагрузка на запись слишком высокая. Cluster автоматически распределяет ключи по нескольким primary-узлам — это и есть шардирование.
Cluster делит пространство ключей на 16384 хэш-слота. Каждый primary отвечает за свой диапазон слотов.
Номер слота — это CRC16 от имени ключа по модулю 16384; клиент считает его сам и идёт сразу на нужный узел. Если в имени есть фигурные скобки, хешируется только то, что внутри: {user:42}:cart и {user:42}:orders гарантированно попадут в один слот. Это важно, потому что операция сразу над несколькими ключами (MGET, транзакция, скрипт) в кластере работает только внутри одного слота.
# Минимальный кластер: 3 primary + 3 replica (6 узлов)
# Запуск через redis-cli:
redis-cli --cluster create \
192.168.1.10:6379 192.168.1.11:6379 192.168.1.12:6379 \
192.168.1.13:6379 192.168.1.14:6379 192.168.1.15:6379 \
--cluster-replicas 1
В Spring Boot кластер настраивается аналогично Sentinel:
spring:
data:
redis:
cluster:
nodes:
- 192.168.1.10:6379
- 192.168.1.11:6379
- 192.168.1.12:6379
Lettuce (драйвер по умолчанию в Spring Data Redis) умеет работать с кластером прозрачно: сам считает слот и идёт сразу на нужный узел.
Только карту слотов он запрашивает при подключении и по умолчанию больше не обновляет. Узлы переключились, слоты переехали — а клиент продолжает ходить по старой карте, ловить перенаправления и тратить на них лишний круг, пока приложение не перезапустят. Поэтому в кластере обновление карты включают явно: по расписанию и по событию перенаправления.
spring:
data:
redis:
lettuce:
cluster:
refresh:
period: 60s # перечитывать карту слотов раз в минуту
adaptive: true # и сразу, если узел ответил перенаправлением
Что выбрать: RDB, Sentinel или Cluster
Порядок выбора простой: одному серверу с некритичными перезапусками хватает RDB; нужна отказоустойчивость — Sentinel; данные не помещаются на один сервер — Cluster, и переключение при сбое у него своё, Sentinel рядом с ним не ставят.
Мониторинг и типичные проблемы
Команда INFO
INFO возвращает подробную статистику о состоянии сервера:
# Общая информация
INFO
# Только раздел памяти
INFO memory
# Только статистика репликации
INFO replication
Ключевые метрики:
used_memory— сколько памяти занятоconnected_clients— число подключенийkeyspace_hits/keyspace_misses— попадания и промахи кэшаrdb_last_bgsave_status— статус последнего снапшотаrole— primary или replica
Медленные команды
Redis выполняет команды в один поток, поэтому команда, чьё время пропорционально размеру данных, — это пауза для всех клиентов сразу. Классика — KEYS *: перебор всего пространства ключей одним неделимым вызовом, на миллионах ключей это секунды простоя. Замена — SCAN: курсорный обход порциями, между вызовами сервер обслуживает остальных; в ответ могут попасть дубли, а вызывать его нужно до нулевого курсора. Тот же приём у родственников: вместо SMEMBERS на огромном множестве — SSCAN, FLUSHALL — с ASYNC. Долгий Lua-скрипт блокирует сервер точно так же: атомарность скриптов не бесплатна.
Чтобы такие команды не искать вручную, Redis ведёт журнал медленных:
# Настроить порог в redis.conf (в микросекундах, 10000 = 10 мс)
slowlog-log-slower-than 10000
slowlog-max-len 128
Порог живёт в конфигурации, а журнал смотрят уже из клиента:
# Посмотреть медленные команды
SLOWLOG GET 10
# Итерировать ключи без блокировки (вместо KEYS)
SCAN 0 MATCH user:* COUNT 100
Большие ключи
Огромное значение в одном ключе (например, список из миллиона элементов) — частая проблема. Такой ключ:
- долго сериализуется при сохранении в RDB/AOF;
- блокирует сервер при операциях вроде
DEL(удаление большого списка — синхронная операция).
Такие ключи удаляют UNLINK вместо DEL — освобождение памяти уходит в фон.
# Найти большие ключи (встроенный сканер)
redis-cli --bigkeys
# Асинхронное удаление большого ключа
UNLINK huge_list_key
Глубже: свой Redis или управляемый сервисрасширенное
Вся статья выше про redis.conf, Sentinel и Cluster, и честный вопрос после неё: кто это будет обслуживать. В 2026 году у большинства команд ответ «облако», и полезно понимать, что при этом меняется, а что остаётся вашим.
Управляемый сервис (ElastiCache и MemoryDB у AWS, Memorystore у Google, Managed Redis у российских облаков) берёт на себя то, что в этой статье про эксплуатацию: установку и обновление версий, реплику и переключение при отказе, снимки по расписанию, метрики в общей консоли. По умолчанию облака предлагают Valkey, а не Redis, из-за истории с лицензией, о которой сказано в статье про основы, и для приложения это ничего не меняет. Наценка за управление обычно в полтора-два раза к цене тех же виртуальных машин, и её сравнивают не с железом, а с часами инженера на ночные переключения.
Что остаётся вашим при любом варианте. Размер памяти и политика вытеснения: облако даст выбрать, но не подскажет, что noeviction уронит запись, когда память кончится. Устройство ключей и большие ключи: ни один провайдер не спасёт от HGETALL на миллионе полей. Медленные команды и число поездок, то есть всё из статей про структуры и про основы. Режим отказоустойчивости: реплика в другой зоне доступности стоит денег и включается явно. И разделение сред: один экземпляр на кэш и на очереди задач это привычная экономия, которая при первом инциденте кэша роняет и очереди.
Что остаётся недоступным: часть настроек redis.conf, модули, которые провайдер не включил, и точная версия, к которой вы привыкли. Своя установка оправдана, когда нужен именно Redis Stack с модулями, когда трафик такой, что наценка облака становится заметной статьёй, или когда данные по требованию регулятора не могут покинуть контур. Тогда всё, что в этой статье, становится вашей работой, и вопрос «кто дежурит» задают до установки, а не после.
То же самое относится к ClickHouse: ClickHouse Cloud и управляемые версии у облаков снимают ZooKeeper и Keeper, реплики и резервные копии, но оставляют вам порядок сортировки, размер вставок и число партиций, потому что это свойства ваших данных, а не сервиса.
Коротко
RDB— снапшот на диск, быстрый старт, возможна потеря данных между снапшотами.AOF— журнал команд, теряете максимум секунду, файл нужно периодически перезаписывать.maxmemoryставят на 60–70 % памяти машины (запас на копирование страниц при снимке и на буферы клиентов), а политику выбирают по роли:allkeys-lruдля кэша,allkeys-lfuдля горячих ключей,noevictionдля хранилища, где данные нельзя терять.- Репликация даёт резервную копию и масштабирование чтения; Sentinel добавляет автоматическое переключение при сбое.
- Cluster нужен при нехватке памяти или записи на одном узле: 16384 слота, номер слота — CRC16 от ключа.
KEYS *блокирует сервер — используйтеSCAN;DELбольшого ключа тоже блокирует — используйтеUNLINK.- Управляемый Redis снимает переключения, снимки и обновления за наценку в полтора-два раза; память, вытеснение, ключи и медленные команды остаются вашими при любом варианте.
- Переполнение буфера реплики даёт бесконечную полную пересинхронизацию; короткий разрыв закрывает частичная, её ёмкость задаёт
repl-backlog-size, а проверяется счётчикамиsync_fullиsync_partial_ok. - Кластеру нужно минимум три мастера, команды на ключи из разных слотов отвечают
CROSSSLOT(лечится хеш-тегом), а переезд слотов — отдельная плановая операция. - Пороги: попадания ниже 80 % — разбираться, отказ записи при
noeviction— авария,SLOWLOGи--latencyпоказывают задержку, отставание реплики выносят в оповещение.