Какие подходы существуют для распределённых блокировок
Что проверяют
Понимаете ли вы, что synchronized и ReentrantLock работают внутри одной JVM, а для нескольких экземпляров сервиса нужен внешний арбитр со своими отказами. Следом почти всегда спрашивают, что будет, если владелец завис в долгой GC-паузе, а срок блокировки истёк.
Как отвечать
Разделите подходы по тому, кто хранит блокировку:
- база данных:
SELECT ... FOR UPDATEили advisory locks в PostgreSQL (pg_try_advisory_xact_lock), которые снимаются вместе с транзакцией. Новой инфраструктуры не нужно, но блокировка занимает соединение из пула; - оптимистичная блокировка: колонка версии и
@Versionв JPA. Конфликт обнаруживается при записи черезOptimisticLockException, операцию повторяют. Хороша при редких конфликтах; - Redis: атомарная
SET key token NX PX 30000и снятие Lua-скриптом, который удаляет ключ, только если значение равно вашему токену. В Java берут Redisson сRLock, где watchdog продлевает срок, пока клиент жив; - ZooKeeper или etcd: хранилища с консенсусом. В ZooKeeper блокировка строится на эфемерных последовательных узлах, в Java через Curator и
InterProcessMutex. Надёжнее Redis, но дороже в эксплуатации; - готовые обёртки: ShedLock для
@Scheduled,LockRegistryиз Spring Integration.
Главная мысль: истёкший срок не значит, что старый владелец остановился. Поэтому хранилище должно проверять fencing token, растущий номер, и отвергать запись от устаревшего владельца. А часто лучше обойтись без блокировки: идемпотентные операции, уникальный ключ в базе или ключ партиции Kafka, чтобы события одной сущности обрабатывал один потребитель.
Где спотыкаются
Делают SETNX, а затем отдельно EXPIRE. Если процесс упадёт между командами, ключ останется навсегда.
Снимают блокировку простым DEL без проверки владельца и удаляют чужую, взятую после истечения своей.
Выдают Redlock за решение всех проблем. Он зависит от часов и пауз и без fencing token корректность не гарантирует.