Как организовать блокировки на стороне сервиса или клиента, а не на уровне базы данных
Что проверяют
Понимаете ли вы, что блокировка в памяти одной JVM перестаёт работать, как только сервис запущен в двух экземплярах, и чем её тогда заменить. Следом почти всегда спрашивают, что будет, если держатель распределённой блокировки завис дольше её TTL.
Как отвечать
Разведите два уровня. Внутри одного процесса:
synchronizedиReentrantLock: второй даётtryLockс таймаутом, прерываемое ожидание и честную очередь;- блокировка по ключу: замок не на весь сервис, а на заказ или пользователя.
ConcurrentHashMap.compute,Stripedиз Guava или карта замков с аккуратной очисткой; - без блокировок:
AtomicReference,LongAdder, последовательная обработка через очередь, например партиция Kafka по ключу.
Между экземплярами сервиса:
- Redis:
SET lock:order:42 <uuid> NX PX 30000, а снимать только свою блокировку Lua-скриптом со сверкой значения. Удобнее Redisson:RLockс watchdog, который продлевает аренду, пока держатель жив; - ZooKeeper или etcd: эфемерные узлы и аренды, в Java через
InterProcessMutexиз Curator. Надёжнее Redis, но дороже в эксплуатации; - ShedLock: когда нужно, чтобы
@Scheduledвыполнялся только на одной реплике.
На стороне клиента работает оптимистичная конкуренция без удержания замка. Клиент получает ETag, отправляет изменение с If-Match, при расхождении сервер отвечает 412 Precondition Failed, и клиент перечитывает данные. Для повторов запросов нужен ключ идемпотентности.
Главная оговорка: блокировка с TTL не даёт полной гарантии. После долгой паузы GC держатель может продолжить работу, уже потеряв замок. Защита: fencing token, монотонный номер, который проверяет само хранилище при записи.
Где спотыкаются
Предлагают synchronized для сервиса из нескольких подов. Он защищает только потоки внутри одной JVM.
Снимают блокировку простым DEL, не проверив владельца, и удаляют чужой замок, взятый после истечения своего.
Освобождают ресурс в finally без признака, что он был захвачен: semaphore.release() после прерванного acquire() добавляет лишнее разрешение.