Ответ
Да, сталкивался. Классический случай — взаимная блокировка (deadlock) при конкурентном обновлении связанных записей в неправильном порядке.
Пример сценария deadlock в PostgreSQL:
-- Транзакция 1: Пользователь A переводит деньги пользователю B
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE user_id = 1; -- Блокирует строку user_id=1
-- Транзакция 2 (параллельно): Пользователь B переводит деньги пользователю A
BEGIN;
UPDATE accounts SET balance = balance - 50 WHERE user_id = 2; -- Блокирует строку user_id=2
-- Теперь deadlock:
-- Транзакция 1 пытается заблокировать строку user_id=2, но она уже заблокирована Транзакцией 2.
UPDATE accounts SET balance = balance + 100 WHERE user_id = 2; -- Транзакция 1 ЖДЕТ
-- Транзакция 2 пытается заблокировать строку user_id=1, но она уже заблокирована Транзакцией 1.
UPDATE accounts SET balance = balance + 50 WHERE user_id = 1; -- Транзакция 2 ЖДЕТ -> DEADLOCK
СУБД обнаружит deadlock и принудительно прервет одну из транзакций (в PostgreSQL с ошибкой SQLSTATE 40P01).
Как мы решали эту проблему:
- Установка строгого порядка блокировки записей. Всегда обновляли счета в порядке возрастания
user_id. Это гарантировало, что все транзакции запрашивают блокировки в одной последовательности, исключая circular wait. - Сокращение времени транзакции. Выносили логику валидации и вычислений за пределы транзакции, оставляя внутри только минимальные
UPDATE/INSERT. - Использование
SELECT ... FOR UPDATE SKIP LOCKEDдля обработки очередей задач, что позволяло избегать блокировок при конкурентном чтении. - Настройка таймаута для запросов (
lock_timeoutв PostgreSQL), чтобы долгая блокировка не подвешивала приложение, а вызывала контролируемую ошибку, которую можно было обработать (например, повторить запрос).
Ответ 18+ 🔞
А, ну это классика, ёпта! Прямо в точку. Взаимная блокировка — это когда два процесса, как два упрямых барана, друг другу дорогу не уступают и стоят, пока база данных одного из них не прибьёт.
Вот смотри, как это выглядит в коде, реальный пример из жизни:
-- Транзакция 1: Пользователь А шлёт сотку пользователю Б
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE user_id = 1; -- Захватил строку юзера 1
-- Транзакция 2 (параллельно, в другом потоке): Пользователь Б шлёт полтинник пользователю А
BEGIN;
UPDATE accounts SET balance = balance - 50 WHERE user_id = 2; -- Захватил строку юзера 2
-- А теперь начинается цирк, ёперный театр!
-- Транзакция 1 тянется к строке юзера 2, но она же уже в лапах у второй транзакции!
UPDATE accounts SET balance = balance + 100 WHERE user_id = 2; -- Транзакция 1 ВИСИТ
-- Транзакция 2 тянется к строке юзера 1, а она у первой! Пиздец.
UPDATE accounts SET balance = balance + 50 WHERE user_id = 1; -- Транзакция 2 ВИСИТ -> ВСЁ, ДЕДЛОК!
База это дело обнаружит, выберет одну несчастную транзакцию и прирежет её, выдав ошибку. Остальная — проскочит.
Как мы с этим боролись, чтобы не было волнения ебать:
-
Жёсткий порядок блокировок. Это самое главное! Договорились, что всегда будем обновлять счета по возрастанию
user_id. Сначала меньший ID, потом больший. Все транзакции начинают хватать замки в одной очереди, и циклического ожидания не возникает. Просто, но работает, как швейцарские часы. -
Дробили транзакции. Выносили всю подготовительную херню — валидацию, расчёты — за пределы транзакции. Внутри оставляли голые
UPDATEиINSERT, чтобы она быстрее делала своё дело и отпускала замки. Чем дольше висит транзакция, тем выше шанс наколотить другую. -
Использовали
SELECT ... FOR UPDATE SKIP LOCKED. Это просто магия для очередей задач. Если запись уже кто-то держит, мы её просто пропускаем и берём следующую свободную. Никаких взаимных блокировок, все довольны. -
Ставили таймауты. Настроили
lock_timeoutв Постгресе. Если запрос не может получить блокировку за N миллисекунд — он не висит вечно, а падает с понятной ошибкой. Приложение ловит её и может, например, откатиться и повторить операцию чуть позже. Доверия ебать ноль, но зато система не ложится полностью.