Классическая задача микросервисов: N подов, а cron-задачу должен выполнять ровно один; или доступ к ресурсу — в один поток. Redis даёт примитивы для взаимного исключения и выбора лидера.
Простая блокировка: SET NX EX
# Захват: атомарно "если нет ключа — установить с TTL"
SET lock:cron-report "pod-abc-f3e2" NX EX 30
OK # захватили
(nil) # кто-то уже держит
Три обязательных элемента — и почему они не опциональны:
- TTL: под умер вместе с блокировкой → без TTL она бы зависла навсегда;
- Уникальное значение-владелец (UUID пода): иначе под B снимет блокировку, которую держит под A;
- Снятие через Lua (атомарный check-and-delete):
Почему не GET+DEL: между ними блокировка может истечь и быть захвачена другим — и вы удалите чужую.if redis.call("GET", KEYS[1]) == ARGV[1] then return redis.call("DEL", KEYS[1]) else return 0 end # EVAL "скрипт" 1 lock:cron-report "pod-abc-f3e2"
Проблема «работа дольше TTL»
Задача идёт 40 секунд при TTL 30 — блокировка истекла, второй под захватил её, оба работают. Решения:
- Watchdog-продление: владелец в фоне продлевает TTL (например, каждые 10с
EXPIRE lock 30). В Java Redisson это автоматический watchdog; - Честная оценка длительности + запас;
- Идемпотентность задачи как страховка от двойного исполнения — всегда;
- Fencing-токен: при захвате получать монотонный номер (INCR) и отдавать его ресурсу (БД проверяет «моя ли это версия») — защита от «зомби-владельца».
Лидерство подов (leader election)
Схема «heartbeat-лидерства» — самая употребимая:
# Каждый под в цикле раз в 5с:
ok = SET leader:service "pod-{uuid}" NX EX 15
if ok:
роль = лидер # выполнять cron-работу
elif GET leader:service == "pod-{uuid}":
EXPIRE leader:service 15 # я ещё лидер — продлеваю
else:
роль = follower
- Переключение лидера — в пределах TTL после смерти текущего;
- Двойного лидерства окно минимально (пара секунд при lag), поэтому критичные к дублям действия дополнительно идемпотентны;
- Один Redis-инстанс достаточно: это «смягчённая» координация, не банковское исключение.
Redlock и честный разговор о гарантиях
Redlock — алгоритм захвата блокировки на N независимых Redis-инстансах (обычно 5): захват успешен, если взяли кворум. Затраты выше, и вокруг его строгости идут известные споры (статья Kleppmann «How to do distributed locking»): при серьёзных сбоях (длительная GC-пауза, расхождение часов) Redlock не даёт формальных гарантий взаимного исключения.
Практический вывод: блокировка на Redis — эффективная защита от «честных» дублей в доверенной сети, но не криптографическая гарантия. Всегда стройте вторую линию: идемпотентность операций, оптимистичные проверки в БД, fencing-токены там, где цена ошибки высока.
Семафоры и прочая координация
- Семафор на K исполнителей: K ключей
sem:1..K; воркер перебираетSET sem:{i} uuid NX EX 60, пока не захватит один. Или ZSet-семафор:ZADD sem now uuidприZCARD < Kпосле очистки просроченных; - Распределённый счётчик квот: INCR + EXPIRE (rate-limit, урок 5.3);
- Барьер синхронизации:
INCR arrived; воркеры ждут, покаGET arrived == N(для тестов и батчей).
Практика
- Реализуйте захват/освобождение блокировки с Lua-разблокировкой. Смоделируйте «зомби»: удерживайте блокировку дольше TTL из второго процесса и убедитесь, что «чужой DEL» не проходит.
- Соберите лидерство из двух скриптов: ровно один пишет
GET leader:*в ответ на «кто лидер». Убейте лидера — замерьте время переключения. - Сделайте семафор на 3 исполнителя и нагрузите 10 воркерами: одновременно работающих должно быть не больше трёх.
Мини-квиз
1. Почему TTL блокировки обязателен? — Умерший владелец не освободит её; TTL — встроенный «срок аренды».
2. Что такое fencing-токен? — Монотонный номер владения блокировкой; ресурс отвергает операции с устаревшим номером.
3. Окно двойного лидерства при heartbeat-схеме — почему не ноль? — Репликация/задержки и TTL: старый лидер может ещё не знать, что истёк его срок.