Что делать, чтобы избежать одновременного выполнения задачи несколькими инстансами, кроме кэша
Что проверяют
Понимаете ли вы разницу между синхронизацией внутри одной JVM и координацией нескольких подов. Вопрос почти всегда идёт после рассказа про кластер из нескольких инстансов сервиса с одинаковым планировщиком, и проверяют, не предложите ли вы synchronized там, где он бесполезен.
Как отвечать
Начните с того, что общий ресурс тут один: база или внешний координатор. Варианты по убыванию частоты:
- Блокировка в базе. Отдельная таблица блокировок с уникальным ключом задачи и временем аренды: кто вставил строку, тот и работает, остальные падают по уникальному индексу и молча уходят. В Spring это готовый ShedLock поверх
@Scheduled. - Пессимистичная блокировка строки.
SELECT ... FOR UPDATEна запись задачи, а для разбора очередиFOR UPDATE SKIP LOCKED, чтобы инстансы брали разные задачи, а не стояли в очереди друг за другом. - Advisory lock в PostgreSQL (
pg_try_advisory_lock): не требует таблицы и снимается сам при обрыве сессии. - Внешний координатор. ZooKeeper с эфемерными узлами или etcd, выбор лидера: лидер выполняет задачу, остальные ничего не делают, пока он жив.
- Очередь. Kafka отдаёт партицию ровно одному консьюмеру группы, так что задача выполняется в одном месте по построению. Quartz в кластерном режиме хранит триггеры в базе и сам не даёт двойного запуска.
Отдельно скажите про аренду с истечением и про идемпотентность самой задачи.
Где спотыкаются
Предлагают synchronized или ReentrantLock. Монитор живёт в куче одной JVM, соседний инстанс о нём не знает, поэтому от параллельного запуска это не защищает вообще.
Берут блокировку без срока жизни. Инстанс упал, держа её, и задача не запускается больше никогда. Нужна аренда с истечением и продлением, а вместе с ней и понимание, что владелец может потерять блокировку посреди работы, например из-за долгой паузы сборщика мусора.
Пишут блокировку в Redis через SETNX без токена владельца и снимают её обычным DEL. Истёкшую блокировку в этот момент уже держит другой процесс, и вы снимаете чужую. Снятие делают Lua-скриптом со сверкой токена.