Программа курса

Модуль 03 / Урок 14 из 27

Блокировки и синхронизация подов

Теория и практика

Классическая задача микросервисов: 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):
    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"
    Почему не GET+DEL: между ними блокировка может истечь и быть захвачена другим — и вы удалите чужую.

Проблема «работа дольше 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 (для тестов и батчей).

Практика

  1. Реализуйте захват/освобождение блокировки с Lua-разблокировкой. Смоделируйте «зомби»: удерживайте блокировку дольше TTL из второго процесса и убедитесь, что «чужой DEL» не проходит.
  2. Соберите лидерство из двух скриптов: ровно один пишет GET leader:* в ответ на «кто лидер». Убейте лидера — замерьте время переключения.
  3. Сделайте семафор на 3 исполнителя и нагрузите 10 воркерами: одновременно работающих должно быть не больше трёх.
Мини-квиз

1. Почему TTL блокировки обязателен? — Умерший владелец не освободит её; TTL — встроенный «срок аренды».

2. Что такое fencing-токен? — Монотонный номер владения блокировкой; ресурс отвергает операции с устаревшим номером.

3. Окно двойного лидерства при heartbeat-схеме — почему не ноль? — Репликация/задержки и TTL: старый лидер может ещё не знать, что истёк его срок.