Москва: проверенные б/у ноутбуки с гарантией и проверкой перед оплатой
+7 906 722-94-45

Главная → Статьи → Распределённые блокировки: Redis и ZooKeeper

Распределённые блокировки на практике: Redlock в Redis и ZooKeeper

Как работают распределённые блокировки: SETNX с TTL в Redis, Redlock, ephemeral-узлы и рецепты ZooKeeper, типичные гонки и когда блокировка не нужна.

Прямой ответ

Распределённая блокировка (distributed lock) — координация конкурентного доступа к общему ресурсу в распределённой системе (только один процесс в кластере выполняет критическую секцию — микросерисы — «задача в одном экземпляре» — cron-задача на N репликах — без блокировки — N запусков — Celery); Redis: SETNX + TTL (базовый паттерн): SET lock_key unique_value NX PX 30000 (NX — только если нет, PX — TTL 30 с — Redis — атомарно — «взял блокировку или нет» — один вызов — Celery); уникальный value (обязателен): value = уникальный токен (UUID — структура — освобождение: удалить только если value совпадает (Lua-скрипт — атомарно — Redis — иначе «блокировка истекла, взял другой, я удалил чужую» — гонка — идемпотентность); проблема TTL (главная боль): TTL слишком короткий (задача не успела — блокировка истекла — второй процесс взял — два процесса в критической секции — гонка — «блокировка истекла раньше задачи»); fencing token (решение — но сложно): блокировка возвращает монотонный токен (resource проверяет токен — старый отклоняет — «stale lock holder» — идемпотентность — Redis без fencing (нет монотонного счётчика из коробки) — ZooKeeper/etcd дают — выбор); Redlock (Redis-мультиузел — спорный): блокировка на N (5) независимых Redis-мастерах (кворум 3+ — «большинство дало блокировку» — консенсус-аналогия — отказоустойчивость одного узла), но критика (Martin Kleppmann): GC-паузы/часы ломают гарантии (процесс «завис» — блокировка истекла — второй взял — первый «очнулся» — оба в секции — eventual — Redlock не даёт строгой корректности без fencing — идемпотентность); ZooKeeper/etcd (сильные гарантии): ephemeral-узел (создать временный узел — сессия оборвалась — узел удалён — failover — «процесс умер — блокировка освободилась автоматически» — TTL не нужен — Redis-отличие); recipe (ZooKeeper): sequential-ephemeral-узлы (каждый клиент создаёт узел с порядковым номером — блокировку держит наименьший — остальные watch предыдущего — консенсус — «очередь на блокировку» — справедливо — FIFO — Celery-аналогия); fencing token (ZooKeeper zxid/etcd revision — монотонный — resource отклоняет stale — идемпотентность — сильная корректность — консенсус); когда Redis-блокировка ок: эффективность (не корректность — «избежать двойной работы» — не «гарантировать единственность» — Celery — дубли допустимы/idempotent — идемпотентность), низкая задержка нужна (Redis — «быстрая блокировка для кэша»); когда ZooKeeper/etcd: корректность критична (лидерство/выбор мастера — Patroni — «два мастера — раскол» — недопустимо — консенсус), fencing нужен (идемпотентность — монотонный токен — ZooKeeper/etcd — Patroni); когда блокировка не нужна: идемпотентность решает (дубли не вредят — идемпотентность — «блокировка для идемпотентной задачи» — оверхед — техдолг), single-writer (одна реплика пишет — CQRS-write — блокировка не нужна — event-driven).

Практика: надёжные блокировки без гонок

Redis-блокировка корректно: (1) SET NX PX с уникальным value (Redis — атомарный захват), (2) освобождение Lua-скриптом (if redis.call("get",KEYS[1])==ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end — Redis — «удалить только свою» — атомарно — идемпотентность), (3) TTL с запасом (задача ~10 с — TTL 30 с — но «задача зависла» — TTL истекает — второй взял — гонка), (4) watchdog (продление TTL пока задача жива — Redlock-клиенты делают — «блокировка жива, пока работаю» — Redis — но «процесс завис» — watchdog не продлевает — TTL истекает — корректно — идемпотентность); ZooKeeper-блокировка: ephemeral-sequential-узел (консенсус — создание /lock/lock-0001 — наименьший держит — остальные watch — Patroni-аналогия — «процесс умер — узел удалён — блокировка свободна» — сессия — failover); etcd (альтернатива ZooKeeper — Patroni — lease + revision (монотонный fencing — консенсус — «etcd для блокировок» — moderne, Kubernetes-стек — k8s — Patroni); антипаттерны: блокировка без TTL (Redis — «процесс умер — блокировка навсегда» — deadlock — TTL обязателен — Celery), освобождение чужой блокировки (без уникального value — Redis — Lua-скрипт — идемпотентность), блокировка для неидемпотентной задачи без fencing (идемпотентность — Redis-блокировка не гарантирует — fencing нужен — ZooKeeper/etcd — консенсус).

Практический сценарий: cron-задача отчётности без дублей

Контекст: сервис отчётности на 4 репликах (k8s — k8s), cron «каждый час — сформировать суточный отчёт» (тяжёлая задача — ~5 мин — ёмкость), боль: без координации — 4 реплики запускают отчёт (4 одинаковых отчёта — дубли — трата ресурсов — Celery); решение — Redis-блокировка (задача идемпотентна — отчёт перезаписывается, дубли не вредят данным — идемпотентность — достаточно эффективности, не корректности — Redis ок): (1) при запуске cron — SET report:lock {uuid} NX PX 600000 (TTL 10 мин — задача 5 мин — запас ×2 — Redis); (2) взяла блокировку → формирует отчёт, не взяла → выходит (3 реплики вышли — 1 работает — Celery); (3) watchdog (каждые 2 мин продлевает TTL — задача дольше 10 мин — блокировка не истечёт — Redis); (4) освобождение Lua-скриптом (по завершении — удалить свою — Redis — идемпотентность); (5) идемпотентность отчёта (отчёт пишется с ключом «дата» — перезапись — дубль безвреден — идемпотентность — «если блокировка подвела и 2 отчёта — перезапись — не дубль»); (6) мониторинг (метрики — «блокировка взята/не взята/истекла» — алерт если задача > TTL — алерты — incident); результат: отчёт формируется один раз (3 реплики вышли — ресурсы сэкономлены — ёмкость), идемпотентность — страховка (блокировка подвела — отчёт перезаписан — не дубль — идемпотентность), watchdog (долгая задача — блокировка жива — Redis); почему не ZooKeeper: задача идемпотентна (корректность не критична — Redis достаточно — выбор — ZooKeeper — оверхед для эффективности — техдолг — Patroni — «Redis для эффективности, ZooKeeper для корректности» — ADR); вывод: блокировка решила задачу (один отчёт — Celery), но идемпотентность — настоящая защита (идемпотентность — «блокировка — оптимизация, идемпотентность — корректность» — event-driven).

FAQ

Redlock надёжен или нет — в чём спор? Redlock — алгоритм Redis-блокировки на N мастерах (кворум — консенсус-аналогия), спор (antirez vs Martin Kleppmann): antirez (автор Redis) — Redlock безопасен для большинства задач, Kleppmann — GC-паузы/сдвиг часов ломают гарантии (процесс взял блокировку, «завис» на GC — блокировка истекла — второй взял — первый «очнулся» — оба в критической секции — идемпотентность — без fencing — некорректно); вывод: Redlock ок для эффективности (избежать двойной работы — дубли допустимы — Celery), не для корректности (гарантия единственности — нужен fencing — ZooKeeper/etcd — консенсус — Patroni); «Redlock гарантирует корректность» — миф (без fencing — нет — идемпотентность — Kleppmann прав для строгих случаев — event-driven — «использую Redlock для критичной блокировки» — риск — fencing или ZooKeeper — ADR — техдолг).

Что такое fencing token и зачем он? Fencing token — монотонно возрастающий номер блокировки (каждый новый holder получает больший токен — resource отклоняет запросы с меньшим токеном — идемпотентность — «stale holder отклонён»); зачем: решает проблему «зависшего процесса» (процесс взял блокировку токен 33, «завис» — блокировка истекла — второй взял токен 34 — первый «очнулся», пишет с токеном 33 — resource видит 33<34 — отклоняет — идемпотентность — корректность без «двух в секции»); где есть: ZooKeeper (zxid), etcd (revision — консенсус — монотонные из коробки — Patroni); где нет: Redis (нет монотонного счётчика из коробки — Redlock без fencing — Redis — можно эмулировать INCR-счётчиком, но хрупко — идемпотентность); «fencing — сложно» — да, но для корректности нужен (ZooKeeper/etcd дают — консенсус — Redis — только эффективность — выбор — ADR — критичная блокировка — fencing обязателен — event-driven).

Когда распределённая блокировка вообще не нужна? Часто не нужна (оверхед): идемпотентность решает (дубли безвредны — идемпотентность — «блокировка для идемпотентной задачи» — лишняя — сценарий выше — Celery), single-writer (одна реплика пишет — CQRS-write — «кто пишет» определено архитектурой — блокировка не нужна — event-driven), optimistic concurrency (версия/expected_seq — event sourcing — конфликт — повтор — транзакции — «без блокировки, с версией»), база даёт (SELECT FOR UPDATE — Postgres — «блокировка на уровне БД» — не распределённая — транзакции); когда нужна: конкурентный доступ к неделимому ресурсу (один пишет в файл/вызывает внешний API с лимитом — Celery), лидерство (один мастер — Patroni — консенсус); «блокировка везде» — антипаттерн (оверхед + сложность + риск deadlock — техдолг — «идемпотентность вместо блокировки» — часто проще и надёжнее — идемпотентность — ADR).

Redis или ZooKeeper/etcd для блокировок — что выбрать? Redis: быстро (Redis — низкая задержка — «быстрая блокировка»), проще (Redis уже есть — Redis — не добавлять ZooKeeper), но без fencing (эффективность, не корректность — идемпотентность — Redlock спорный — консенсус); ZooKeeper/etcd: сильные гарантии (консенсус — консенсус — ephemeral-узлы — автоосвобождение — failover — fencing из коробки — Patroni), но тяжелее (отдельный кластер — эксплуатация — ёмкость — зависимости); выбор: эффективность + низкая задержка + идемпотентность → Redis (Celery — сценарий выше), корректность + лидерство + fencing → ZooKeeper/etcd (Patroni — консенсус); вывод: Redis для «избежать двойной работы», ZooKeeper/etcd для «гарантировать единственность» — ADR — не «один лучше» — разные задачи — техдолг — event-driven.

Источники: Redis docs (distributed locks), ZooKeeper recipes, etcd docs — ссылки в шапке.