← назад к разделу

Redis — это хранилище в оперативной памяти. Казалось бы, процесс упал — данные пропали. На самом деле Redis умеет сохраняться на диск, выдерживать отказ сервера и масштабироваться на несколько машин. Разберём, как это работает и какие настройки выбрать.

клиент считает слот сам и идёт сразу на нужный узел команда клиента SET user:7 GET user:100 GET user:100 повтор после сбоя CRC16(ключ) % 16384 слот 2780 слот 9308 слот 9308 узел A слоты 0–5460 узел B слоты 5461–10922 узел C слоты 10923–16383 replica A replica B replica C узел Aслоты 0–5460 узел Bслоты 5461–10922 узел B не отвечает replica B → primaryслоты 5461–10922кластер сам повысил replica, слоты те же

Пространство ключей разрезано на 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.

RDB снимок 12:00 записи мимо диска падение 12:05 потеряно 5 мин AOF журнал открыт каждая в журнал падение 12:05 потеряна 1 с

Одна и та же авария в двух режимах: смотрите на правый столбец, там вся разница между снимком раз в пять минут и журналом с ежесекундным сбросом.

Память и политики вытеснения

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 недостаточно: стоит одному из них самому оказаться недоступным — большинства уже не собрать, и переключения не будет.

primary молчит нет ответа 5 с отказ признан кворум 2 из 3 выбран лидер согласие большинства новый primary 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 показывают задержку, отставание реплики выносят в оповещение.

Что почитать дальше