Часто коллеги приходят с идеей сделать что-то через распределенный лок. В этом посте расскажу, почему эта задача сложнее, чем кажется, какие подводные камни встречаются и возможные альтернативы.
Небольшое интро.
Распределенный лок помогает сервисам "поделить" какой-то ресурс. Ресурсом может быть задача, обработка файла или какой-то сущности. Сам лок не контролирует доступ к ресурсу, это лишь способ договориться. Кто захватил лок, тот и работает с ресурсом.
В чем сложность работы с распределенным локом?
В комбинации "сложный алгоритм + общение по сети". Работа с локом — это не просто "взял-отпустил". Полный цикл выглядит так:
Создать лок -> Попытаться захватить -> Если не получилось: попробовать ещё раз или встать в очередь -> Отпустить -> Удалить
Каждый участник в любой момент может отвалиться, а запрос - задержаться. В итоге получаем мешок вопросов, которые нужно обдумать:
Что делать, если сервис взял лок, но умер?
Что будет, если один сервис отправит 2 команды захватить лок?
Может ли сервис отпустить лок, который он не держит?
Что делать, если порядок запросов нарушится, и сначала на лок пришла команда "отпустить", а потом "взять"?
Кто будет создавать локи?
Будет ли атомарно работать связка "создать и захватить лок"?
Сколько сервис будет пытаться захватить лок? С какими интервалами?
Если сервис встаёт в очередь к локу - сколько времени ждать? можно ли выйти из очереди?
Кто и когда будет удалять локи?
Многие вопросы снимаются инструментами, но не все. Race condition в распределенных системах встречается сплошь и рядом. Взять хотя бы недавний сбой Амазона, где всё началось с того, что 2 сервера одновременно накатывали апдейт и помешали друг другу.
Ещё одна проблема с локами - тестирование. В большинстве случаев разработчик проверит вручную пару кейсов с помощью Thread.sleep. Но это детский сад, конечно. Написать автоматизированные тесты для распределенных локов очень сложно.
Поэтому даже если система маленькая, всё крутится на одном сервере и сетевые проблемы сведены к минимуму, рекомендую рассмотреть альтернативы. Например
✔️ Сделать задачу идемпотентной и безопасной для многократного выполнения
✔️ Провернуть Inversion of control. Ресурсы распределяются по исполнителям, а не исполнители борются за ресурсы
Оба подхода можно протестировать, и общая логика часто упрощается. Берите на заметку, квинтэссенция многолетнего опыта:)
Но если сердце не видит преград, и сделать лок хочется, вот пара заметок:
💫 Лок можно реализовать на Postgres (SELECT … FOR UPDATE), Redis, Zookeeper, Kubernetes. Гляньте библиотеку ShedLock
💫 ID держателя лока удобно записывать в лок. ID каждого участника должен быть постоянным
💫 Вместо плясок с TTL можно положиться на связь сервиса и лока. В Zookeeper есть ephemeral nodes, которые исчезают, если связь с сервисом пропадает. Транзакция в Postgres может не сразу обнаружить разрыв соединения, но тоже в итоге откатится
Что почитать:
🔥 Cтатья Alibaba Cloud 2024 года. Обзор решений на джаве и их нюансов
🔥 Статья Клепмана (автор книги с кабанчиком) про недостатки распределенного лока в Redis . Статья старая (2016 год) и специфичная, но полезна для полноты картины