Знать внутренности Redis полезно, но в проде выигрывает тот, кто умеет его эксплуатировать: правильно настроить пул соединений, поймать деградацию по метрикам до того, как её заметят пользователи, и — что важнее всего — вовремя понять, что задача вообще не для Redis. Эта финальная статья серии «Redis: глубокое погружение» собирает предыдущие шесть в практическую рамку.
Сквозная нить серии — «Redis как кэш vs Redis как источник истины» — здесь сводится в явную карту выбора. Но начнём с трёх вещей, которые стенд этой серии обнаружил живьём и которые объединяет одно свойство: они врут молча.
В статье
- Пул соединений: размер решает, и это измерено
- Ловушка в самом инструменте: клиент, который скрывает таймауты
- Backup, который молча ничего не восстановил
LATENCY HISTORY— не журнал команд- Типовые инциденты серии
- Логические базы: 16 — это не лимит, но и не изоляция
- Кто подключён:
CLIENT LISTи почемуname=пустое у всех - Карта выбора: Redis или не Redis
- Что дальше
- Источники
Пул соединений: размер решает, и это измерено
Сценарий: 50 горутин одновременно пишут через клиент с пулом на 5, 20 и 50 соединений. Три отдельных клиента, не один растущий.
Сначала о двух вещах, без которых замер был бы пустым. Первая: в go-redis PoolSize — это только базовое число соединений; если их не хватает, пул растёт сверх него, пока не задан MaxActiveConns. Без явного ограничения все три «размера» были бы тремя одинаково безлимитными пулами, и ни один не дал бы ни единого таймаута. Вторая: ретраи выключены (MaxRetries: -1) — иначе клиент сам бы повторял команду после таймаута пула, и в таблице ниже стояли бы числа на порядок меньше реального контеншна. Почему именно на порядок — в следующем разделе. Обе вещи проверены на стенде, а не вычитаны.
Результаты по доле отказов (обе ориентации порядка × 3 повтора на образ, PoolTimeout 2 мс):
| Образ | Ориентация | size=5 | size=20 | size=50 |
|---|---|---|---|---|
| redis:8.8 | прямая (5→20→50) | 53.3 / 52.3 / 53.7% | 4.9 / 3.3 / 8.2% | 0.0% ×3 |
| redis:8.8 | обратная (50→20→5) | 53.3 / 54.0 / 53.1% | 5.1 / 3.9 / 5.0% | 0.0% ×3 |
| valkey:8.1 | прямая (5→20→50) | 52.7 / 54.0 / 53.9% | 2.8 / 6.8 / 6.0% | 0.0% ×3 |
| valkey:8.1 | обратная (50→20→5) | 54.0 / 54.0 / 53.8% | 3.0 / 3.9 / 3.3% | 0.0% ×3 |
Сводно: 52.3–54.0% отказов на пуле в 5, 2.8–8.2% на пуле в 20, 0.0% на пуле в 50 — во всех 12 прогонах. Пул размером с конкурентность очереди не создаёт по построению.
Но процент отказов — целиком функция выбранного порога, и цитировать его без порога рядом нечестно. Тот же сценарий с разными значениями PoolTimeout:
PoolTimeout |
size=5 | size=20 | size=50 |
|---|---|---|---|
| 1 мс | 73.2% | 41.3% | 0.0% |
| 2 мс (взято для таблицы выше) | 53.3% | 6.7% | 0.0% |
| 5 мс | 0.4% | 0.2% | 0.0% |
Направление везде одно, но абсолютные проценты меняются в разы при сдвиге порога на пару миллисекунд. 2 мс здесь — не «типичный прод-таймаут», а значение, дающее содержательный контраст на быстром локальном Redis. В проде PoolTimeout обычно на порядки выше, и отказов при той же латентности почти не будет.
Ещё одна важная оговорка: абсолютные числа этого раздела — Docker Desktop на Windows, а не нативный Redis. На голом Linux по loopback round-trip — единицы-десятки микросекунд, здесь — единицы миллисекунд, то есть на два порядка больше. Надёжны только относительные сравнения внутри одного прогона.
Java даёт то же направление, но другие абсолюты — и это не вывод «Lettuce хуже go-redis». Зеркало на GenericObjectPool поверх Lettuce: ≈74% отказов на size=5, ≈42% на size=20, 0% на size=50. Сравнение не выровнено сразу по трём осям: Go-бинарник исполняется нативно на Windows-хосте, а Java — внутри WSL2, то есть по разному сетевому пути; алгоритмы ожидания в пулах разные; и на Java-стороне есть эффект порядка, которого у Go нет. Ни один из трёх вкладов стенд не изолирует.
Про эффект порядка стоит сказать отдельно, потому что он реально нашёлся. Армы идут последовательно в одном процессе, поэтому позиция в очереди — потенциальный конфаундер, и обе ориентации прогонялись именно ради его проверки. У Go порядок не значит ничего: size=5 даёт 52.3–53.7% первой армой и 53.1–54.0% последней, интервалы совпадают. А у Java значит, и заметно: на Redis size=5 даёт 78.0–78.6% первой армой и 70.2–70.3% последней — интервалы не пересекаются, разрыв около 8 процентных пунктов. Одна ориентация врала бы примерно на эту величину. Правдоподобный механизм — прогрев JVM, но стенд его не диагностировал: зафиксирован факт наличия эффекта порядка, а не объяснённый механизм.
А вот чего стенд честно не даёт — так это сравнимой латентности между размерами пула. Причина тонкая и стоит объяснения. Перцентили считаются только по успешным операциям, а на пуле в 5 «успешные» — это выжившая половина: те, кому соединение досталось быстро. Медленные попытки в выборку не попали, они стали таймаутами. На пуле в 50 в выборке все 100%. Это цензурирование, и оно систематически занижает перцентили тесных пулов. Насколько существенно — видно из собственной Java-армы того же стенда, где цензурирование сильнее: там p50 идёт в обратную сторону, 0.842 мс на size=5 против 1.659 мс на size=50. Тот же сценарий, противоположный порядок. Поэтому напрашивавшееся «без очереди успешные операции быстрее по построению» пришлось снять: это теоретическое рассуждение, поданное как измерение, и опровергается оно соседней армой того же стенда.
Что можно утверждать честно: доля отказов монотонно падает с ростом пула — устойчиво, в обеих ориентациях, на обоих образах, в обоих клиентах.
Ловушка в самом инструменте: клиент, который скрывает таймауты
Это первая из трёх ловушек, и она объясняет, почему наивная версия замера выше показала бы неправду.
По умолчанию go-redis сам повторяет команду после таймаута пула. Проверка живьём на пуле в одно соединение: внутренний счётчик клиента насчитал 1660 таймаутов ожидания за прогон, а до вызывающего кода в виде ошибки дошло только 30. Остальные 1630 ретрай тихо поглотил — ценой добавленной латентности, а не видимой ошибки.
Практический вывод: «число pool timeout ошибок» с дефолтными ретраями занижено на порядок и остроту контеншна не отражает. Если вы мониторите исчерпание пула по числу ошибок — вы видите верхушку.
Backup, который молча ничего не восстановил
Вторая ловушка, и самая неприятная, потому что обнаруживается в худший момент.
Цикл проверялся полностью: посеяли 6001 ключ, сняли живой снапшот через redis-cli --rdb, уничтожили volume, доказали, что данные реально исчезли (свежий инстанс: DBSIZE=0, точечный ключ отсутствует — иначе скрипт падает), восстановили и сверили.
redis:8.8 |
valkey/valkey:8.1 |
|
|---|---|---|
| DBSIZE до backup | 6001 | 6001 |
| Размер снапшота | 717372 байт | 717373 байт |
| DBSIZE на доказательно пустом инстансе | 0 | 0 |
| Строка загрузки при restore | Done loading RDB, keys loaded: 6001, keys expired: 0 |
то же |
| DBSIZE после restore | 6001 | 6001 |
| Содержимое точечного ключа | побайтово совпало | побайтово совпало |
А теперь находка. Стандартная конфигурация поднимает сервер с --appendonly yes. Если положить dump.rdb в пустой volume и поднять сервер с этим дефолтом — сервер полностью игнорирует ваш dump.rdb: создаёт пустой AOF base file и стартует с DBSIZE=0. Без единой ошибки или предупреждения. В логе только BGSAVE done, 0 keys saved — сервер честно сохраняет пустое текущее состояние, а не ваше.
То есть план восстановления «положить dump.rdb в data dir и поднять сервис» молча не работает, если AOF включён конфигом по умолчанию. И это не бросается в глаза без явной проверки DBSIZE — процедура отработала, ошибок нет, данных нет.
Обход: поднимать сервер для загрузки с временно выключенным appendonly, либо убедиться, что каталога appendonlydir нет до первого старта. И проверять DBSIZE после восстановления — всегда.
Отдельная оговорка про сам снапшот: redis-cli --rdb — это снимок на момент завершения передачи, а не строго на момент вызова команды. В изолированном прогоне это не повлияло ни на что, но в общем случае «снапшот = момент T» — не гарантия.
LATENCY HISTORY — не журнал команд
Третья ловушка — в инструменте диагностики.
Проверка: latency-monitor-threshold в 1 мс, slowlog-log-slower-than в 1 мс, затем гарантированно медленная реальная операция — KEYS над 300000 ключами. (DEBUG SLEEP не подошёл: он заблокирован как protected config и неизменяем через CONFIG SET даже с правами по умолчанию — проверено живьём.)
redis:8.8 |
valkey/valkey:8.1 |
|
|---|---|---|
Клиентское время KEYS |
65.386 мс | 53.632 мс |
SLOWLOG: серверное время KEYS |
38.6 мс | 25.3 мс |
KEYS попал в SLOWLOG? |
да | да |
KEYS попал в LATENCY HISTORY command? |
НЕТ | НЕТ |
SLOWLOG поймал медленную команду, а LATENCY HISTORY — нет. Это не сбой стенда, а устройство latency-monitor: он хранит по событию максимум за секунду, а не каждое событие. KEYS (38.6 мс) исполнился в ту же секунду, что и посев данных (411 мс) — максимум забрал посев, и KEYS в историю не попал вообще. На Valkey видно ещё нагляднее: посев, KEYS и очистка уложились в одну секунду, и LATENCY HISTORY содержит одну-единственную запись — три команды, один максимум.
Вывод, который стоит унести: LATENCY HISTORY — не журнал команд. Медленная команда, попавшая в одну секунду с более медленной соседкой, в нём не появится, и её отсутствие там ничего не доказывает. Для вопроса «какая команда тормозила» есть SLOWLOG — у него отдельная запись на команду. LATENCY HISTORY отвечает на другой вопрос: «насколько плохо было в секунду N».
Заодно про сам KEYS: 38.6 мс серверного времени — это время, в течение которого сервер занят только им, потому что цикл событий один. Оговорка: конкурентных клиентов во время KEYS стенд не запускал, так что «все остальные ждали» здесь — известное свойство однопоточной модели, а не отдельно измеренный в этом прогоне факт. Измерено именно то, сколько сервер был занят.
Типовые инциденты серии
Собираем находки шести статей в набор того, что реально ломается.
| Инцидент | Симптом | Что смотреть | Откуда |
|---|---|---|---|
| Простой, невидимый для error rate | 45 секунд без записи, при этом 1 ошибка на 279597 попыток — 0.0004%, любой порог по доле ошибок зелёный | латентность хвоста, а не процент ошибок | Cluster/Sentinel |
| Sentinel молча не сделал failover | мастер мёртв, кворум жив, конфиг верен, переключения нет | redis-cli -p 26379 info sentinel | grep tilt |
Cluster/Sentinel |
| Split-brain | два мастера, подтверждённые записи не видны клиентам | расхождение узлов по role, поведение при партиции |
Cluster/Sentinel |
| Внезапный отказ записи | OOM command not allowed, чтения работают |
used_memory против maxmemory, evicted_keys |
память |
| Политика настроена, вытеснение не работает | volatile-* без TTL вырождается в noeviction |
evicted_keys под нагрузкой, а не конфиг |
память |
| Ложная тревога по фрагментации | mem_fragmentation_ratio 1.41, а фрагментации нет |
allocator_frag_ratio — если ≈1.00, аллокатор ни при чём |
память |
| Шторм соединений | до 54% отказов на тесном пуле при PoolTimeout 2 мс (порог решает: при 5 мс — 0.4%), а с дефолтными ретраями видно на порядок меньше |
PoolStats().Timeouts клиента, не только ошибки |
эта статья |
| Restore прошёл, данных нет | DBSIZE=0 после восстановления, ошибок ноль |
DBSIZE после каждого восстановления |
эта статья |
Дубликаты после XCLAIM |
сообщение обработано дважды | идемпотентность обработчика | streams/Lua |
| Latency-спайк от одной команды | сервер занят одной O(N)-операцией | SLOWLOG (не LATENCY HISTORY) |
эта статья + event loop |
| Непонятно, кто нагружает инстанс | CLIENT LIST полон, но name= пустое у всех, а addr за k8s или NAT не всегда указывает на под и уж тем более на процесс |
name= и user= вместо IP: ClientName в драйвере и отдельный ACL-пользователь |
эта статья |
Общая мораль набора: почти все эти инциденты не кричат. Простой, невидимый для доли ошибок; failover без failover’а; восстановление без данных; вытеснение без вытеснения. Мониторинг, настроенный на пороги по процентам и на «упало/не упало», их не увидит.
Оговорка, которую здесь важно не смазать: 45-секундный простой всё-таки дал одну ошибку — детектор, срабатывающий на сам факт ошибки, его бы заметил. Проспал бы его монитор, считающий долю. А вот случай, когда клиент вообще не отдаёт ошибку и возвращает OK после многосекундной блокировки, на стенде поймался в сценарии Sentinel, а не Cluster. Это разные вещи, и путать их не стоит.
Логические базы: 16 — это не лимит, но и не изоляция
Регулярный вопрос при заезде нескольких сервисов на один Redis: «баз всего 16, как разделить?» В нём две ошибки сразу — и про число, и про слово «база».
Шестнадцать — это не потолок, а значение по умолчанию для обычного standalone-инстанса. Оно меняется в redis.conf:
databases 64Параметр читается при старте, CONFIG SET его не меняет — нужен перезапуск. После него доступны базы 0–63, переключение обычное:
SELECT 17Текущее значение всегда можно посмотреть:
CONFIG GET databasesТак что расширить можно далеко за 16. Проблема в другом: то, что Redis называет базами, — это пространства имён внутри одного инстанса, а не изолированные базы в смысле PostgreSQL. Всё, что серия разбирала как свойства инстанса, у них общее:
| Что | Общее на весь инстанс |
|---|---|
| Процесс и событийный цикл | да — одна дорогая команда блокирует всех, независимо от номера базы |
Память и maxmemory |
да — отдельного лимита на базу нет |
| Политика вытеснения | да — maxmemory и выбор жертв относятся к инстансу, а не к базе; механика вытеснения |
| Персистентность RDB/AOF | да — снимок и журнал общие, базы восстанавливаются вместе |
| Репликация и failover | да — реплицируется инстанс целиком |
| Административные операции | во многом да — например, FLUSHALL сносит все базы разом |
Практическое следствие, которое ломает саму идею «разделили сервисы по базам»: давление памяти в одной базе выселяет ключи из другой. Если сервис в DB 5 залил данных до maxmemory, ключи сервиса в DB 0 начнут вытесняться — в зависимости от maxmemory-policy. Никакой границы, за которую отвечает номер базы, здесь нет. Оговорка про источник: это документированное устройство Redis (maxmemory — лимит инстанса, кандидаты на вытеснение выбираются по базам инстанса), а не результат стенда этой серии — стенд из статьи про вытеснение работал в пределах одной базы и cross-DB-эффект не проверял.
В Redis Cluster логических баз нет вовсе: поддерживается только DB 0, а команда SELECT в кластерном режиме не используется — она запрещена. Приложение должно исходить из DB 0 и разделять данные префиксами ключей. Это отдельная причина не строить схему на номерах баз: приложение, которое однажды может переехать в Cluster, придётся переписывать. Разбор самого Cluster — в статье про репликацию, Cluster и Sentinel.
Что делать вместо этого — три варианта, по убыванию связанности:
-
Один инстанс, разные префиксы ключей — если совместное потребление памяти и одного событийного цикла допустимо. Это же единственный вариант, совместимый с Cluster:
auth:session:123 billing:invoice:456 rate-limit:user:789 -
Разные инстансы (или контейнеры) — если сервисам нужны разные TTL-политики, лимиты памяти, политики вытеснения, режимы персистентности или независимое обслуживание. Это единственный способ получить настоящую изоляцию ресурсов и отказов.
-
Разные логические базы — только как слабое логическое разделение внутри одного приложения (например, отделить кэш от временных структур), когда общие память, персистентность и вытеснение — осознанно приемлемы.
Права доступа при этом удобнее строить не по базам, а по шаблонам ключей — ACL это умеет:
ACL SETUSER service-a on >password ~service-a:* -@all +get +set +del +expireПорядок правил здесь не косметика: они применяются слева направо, поэтому -@all идёт первым и обнуляет права, а нужные команды разрешаются после него. Соблазнительное +@all вместо этого списка выглядит эквивалентно, но им не является: шаблон ~service-a:* ограничивает только команды, у которых есть аргумент-ключ, и на FLUSHALL, CONFIG или SHUTDOWN не распространяется вообще — у них ключей нет. То есть пользователь с ~service-a:* +@all спокойно снесёт весь инстанс. Плюс +@all автоматически включает команды, которых на момент настройки ещё не существует, — из будущих версий и загруженных модулей.
Короткий вывод: число баз расширяется тривиально, но изоляции это не даёт. Если нужна именно изоляция — это разные инстансы; если достаточно порядка в ключах — это префиксы.
Кто подключён: CLIENT LIST и почему name= пустое у всех
Разделили сервисы префиксами и нарезали ACL — и почти сразу упираетесь в вопрос, с которого начинается разбор любого инцидента на общем инстансе: кто в нём сейчас сидит. Встроенный ответ ровно один — CLIENT LIST, по строке на соединение:
id=42679 addr=10.0.4.19:49366 laddr=10.0.1.7:6379 fd=20 name= age=26739 idle=26738
flags=N db=7 cmd=get user=default resp=3 lib-name=go-redis(,go1.25.12) lib-ver=9.18.0На вопрос «кто это» работают пять полей, и у каждого своя граница:
| Поле | Что говорит | Граница |
|---|---|---|
addr |
адрес и порт того, с чего пришло соединение | это не обязательно адрес процесса: за NAT, Service или частью CNI-настроек виден адрес шлюза или узла |
laddr |
адрес сервера, на который пришло соединение | про клиента не говорит ничего |
name |
имя, выставленное самим клиентом | по умолчанию пустое; никак не подтверждается |
user |
аутентифицированный ACL-пользователь | по умолчанию у всех default |
lib-name / lib-ver |
библиотека и её версия | язык и драйвер, но не приложение |
Плюс age и idle — возраст соединения и простой в секундах: пара, по которой видно залипшие коннекты, живущие сутки без единой команды.
Первая ловушка здесь — не в Redis, а в разборе его вывода, и она тихая. Имя поля addr является подстрокой laddr. Любая нежадно не закреплённая регулярка ловит второе вместо первого:
redis-cli CLIENT LIST | sed -E 's/.*addr=([0-9.]+):[0-9]+.*/\1/'Результат выглядит абсолютно правдоподобно — список IP, в котором просто нет ничего подозрительного. Только это адреса самого сервера: на каждой строке подставился laddr. Инстанс, к которому ходят полсети, выглядит как инстанс, к которому ходит он сам. Ошибки нет, вывод есть, вывод неверный. Закрепление на начало строки лечит полностью:
redis-cli CLIENT LIST | sed -E 's/^id=[0-9]+ addr=([0-9.]+):[0-9]+ .*/\1/'Оговорка про природу этого пункта: это детерминированное свойство формата, видное прямо из списка полей, а не находка стенда. Сама документация предупреждает о соседнем риске — набор полей расширяется от версии к версии (watch появился в 7.4, io-thread в 8.0), и парсер обязан переживать незнакомые и отсутствующие поля.
Вторая граница принципиальнее и регуляркой не лечится. Что именно окажется в addr у клиента из Kubernetes, зависит от сетевого пути: это может быть pod IP при маршрутизируемой сети, адрес узла при SNAT на выходе из кластера, адрес Service или вовсе внешнего шлюза. Универсального ответа нет, и в этом вся проблема — по одному IP надёжно выйти на приложение нельзя ни в одном из этих случаев. Худший вариант вполне обычен: несколько узлов дают несколько адресов на десятки приложений, и CLIENT LIST честно покажет ровно эти адреса. Идентифицировать себя должен клиент.
Способов ровно три, и путать их не стоит — они дают разную степень доверия:
| Механизм | Поле | Кто выставляет | Можно ли выдать себя за другого |
|---|---|---|---|
CLIENT SETNAME |
name |
приложение | да — это просто ярлык |
CLIENT SETINFO (7.2+) |
lib-name, lib-ver |
драйвер, автоматически | да |
| ACL-пользователь | user |
сервер, по факту аутентификации | нет |
Отсюда практический вывод: name отвечает на вопрос «что это за процесс» для диагностики, а user — на вопрос «кто это на самом деле» для разграничения прав. Первое удобно, второе доказуемо.
Ключевая тонкость SETNAME — имя живёт на соединении, а не на клиенте. Выставить его вручную после подключения бессмысленно: в пуле имя получит ровно одно соединение из десятков, а после переподключения исчезнет и оно. Нужна опция драйвера, которая шлёт команду на каждом новом коннекте, — и здесь три языка серии ведут себя по-разному:
- Go, go-redis (v9.21.0) — поле
ClientNameвOptions, с прямым описанием в исходнике: «will execute theCLIENT SETNAME ClientNamecommand for each conn». Задаётся и через URL-параметрclient_name. РядомIdentitySuffix(суффикс к имени) иDisableIdentity, отключающийCLIENT SETINFO. - Java, Lettuce (7.6.0.RELEASE) —
clientNameвRedisURI(параметр URI илиwithClientName()), плюс отдельныеlibraryNameиlibraryVersion. - Rust, redis-rs (1.4.1) — а вот здесь опции нет. В
RedisConnectionInfoестьlib_name,lib_verиskip_set_lib_name, то естьCLIENT SETINFOнастраивается, а вот поля под имя соединения не существует:CLIENT SETNAMEреализован как обычная команда, и вызывать её нужно самому — в пуле это хук после установки соединения, а не разовый вызов при старте.
Версии здесь названы не для порядка: это состояние на тех релизах, что зафиксированы в стендах серии, и по master оно меняется — состав полей драйверов стоит перепроверять под свою версию.
rdb := redis.NewClient(&redis.Options{
Addr: "redis.internal:6379",
DB: 5,
ClientName: "billing-api",
})После этого в CLIENT LIST появится name=billing-api — и вопрос «чей это коннект» закрывается без опроса разработчиков. Но поле user при этом останется default: ClientName шлёт только CLIENT SETNAME и к аутентификации отношения не имеет. Чтобы заполнилось и оно, клиент должен логиниться отдельным ACL-пользователем, то есть к тем же опциям добавляются Username и Password. Это ровно та развилка, о которой шла речь выше: имя вы себе назначаете сами, пользователя вам подтверждает сервер.
Последнее, о чём стоит помнить перед тем, как звать CLIENT LIST на нагруженном проде: у команды сложность O(N) по числу соединений, и выполняется она в том же единственном событийном цикле, что и весь трафик, — механика подробно в статье про event loop. На инстансе с десятками тысяч клиентов это не бесплатная диагностика. Сузить выборку помогают фильтры CLIENT LIST TYPE normal|master|replica|pubsub и CLIENT LIST ID <id>, а разорвать конкретное соединение — CLIENT KILL. Оговорка: сложность здесь взята из документации команды, отдельным замером стенда серии она не проверялась.
Карта выбора: Redis или не Redis
Теперь можно собрать нить серии в решение.
Redis почти вне конкуренции:
- Кэш. Данные восстановимы из источника, потеря приемлема по определению, приоритет — скорость. Здесь всё, что серия разбирала как «границы», перестаёт быть проблемой: вытеснение — благо, асинхронная репликация — нормально,
rdb-onlyили вовсе без персистентности — осознанный выбор. - Счётчики, rate limiting, лидерборды. Атомарные операции над структурами, измеренные 9000 сохранённых инкрементов против гонки — ровно про это.
- Лёгкая шина, очередь задач. Streams с consumer groups, если объём укладывается в память, а история не нужна дольше дней.
- Session store — с осознанным пониманием окна потери.
Где честнее взять другое:
- Источник истины для денег и учёта. AOF
everysecтеряет до секунды при падении ОС или питания; подkill -9процесса, как показал стенд, не теряется ничего ни в одном AOF-режиме, но это свойство краха процесса, а не durability-гарантия. Добавление реплик границу не убирает, а сдвигает: репликация асинхронна,WAITсужает окно, но не закрывает, а split-brain реален и измерен. Если каждая подтверждённая запись обязана пережить любой отказ — это требование к системе с консенсусом в фундаменте. - Строгие транзакции.
MULTI/EXEC— не то же самое, что ACID-транзакции; подробности в отдельной статье про транзакции. - Крупный датасет как первичное хранилище. Память дороже диска, и упор в
maxmemoryприходит молча и внезапно. - Аналитика. Не та модель данных и не та нагрузка.
- Поток сообщений как источник истины на недели. Партиционирование под масштаб, долгий ретеншен, exactly-once — задачи брокерной платформы; Streams их не решают по конструкции.
Практический критерий, к которому сводится вся серия: спросите, что произойдёт, если конкретная подтверждённая запись исчезнет. Если ответ «перечитаем из источника» — Redis. Если ответ «потеряем деньги или доверие» — вопрос не в настройке Redis, а в том, тот ли это инструмент.
Что дальше
Серия закончена. Если читать её целиком: модель данных и кодировки → событийный цикл → персистентность → репликация, Cluster, Sentinel → память и вытеснение → streams и Lua → эта статья.
Смежное: клиенты на Go, Java и Rust — паттерны устойчивости, переподключение и работа с failover со стороны приложения; введение, паттерны и антипаттерны — если нужен вход попроще; Valkey после форкаСкоро — лицензии, governance и что из этого следует при выборе; гео-поискСкоро — где Redis GEO на карте вариантов.
Про Redis и Valkey по итогам всей серии — аккуратно, потому что соблазн подвести красивую черту большой. Ни одно расхождение по производительности и поведению под нагрузкой проверки не пережило: каждое такое кажущееся различие при перепрогоне оказывалось либо разной загрузкой хоста, либо малой выборкой, либо разными сетевыми путями. Именно это — практический вывод про пару: на разобранных здесь механизмах они работают одинаково, и клиентский код их не различает.
Но «одинаково» — не «идентично», и две мелочи устояли. У Valkey в INFO sentinel нет счётчика sentinel_total_tilt, который есть у Redis, — детерминированное различие API, к загрузке хоста отношения не имеющее; если мониторинг считает накопленные входы в TILT, на Valkey метрики не будет. И точка отказа под noeviction у образов разная — диапазоны не пересеклись, направление подтвердилось всеми прогонами, а причина сдвига осталась неустановленной. Обе мелочи не меняют выбора между движками, но честнее их назвать, чем подвести под общую черту.
Источники
- Официальная документация: redis.io, раздел redis.io/docs.
- Диагностика latency: redis.io/docs — Latency monitoring, Latency troubleshooting.
- Персистентность и восстановление: redis.io/docs — Persistence.
- Логические базы: redis.io/docs —
SELECT(базы как пространства имён внутри инстанса, общие RDB/AOF, запретSELECTи только DB 0 в кластерном режиме), Cluster spec. - Клиенты: go-redis, Lettuce — Connection Pooling.
- Идентификация соединений: redis.io/docs —
CLIENT LIST(полный список полей, сложность O(N), фильтрыTYPEиID),CLIENT SETNAME,CLIENT SETINFO,CLIENT KILL, ACL. Опции драйверов сверены по исходникам на тех же версиях, что зафиксированы в стендах серии:Options.ClientName,IdentitySuffix,DisableIdentityв go-redis v9.21.0,clientNameв Lettuce 7.6.0.RELEASERedisURI,RedisConnectionInfoбез поля имени соединения в redis-rs 1.4.1. Ссылки закреплены на теги намеренно: вmaster/mainсостав полей меняется. - Смежное на сайте: клиенты на Go, Java, Rust, введение и антипаттерны, «Ландшафт консенсуса».
- Стенд: digital-cookbook/databases/redis/deep-dive (
redis:8.8,valkey/valkey:8.1, модульops-stand: Go-сценарииpool-sizing/monitoring, Java/Lettuce-зеркало; backup/restore —ops/ops-demo.sh).
Комментарии