Были ли проблемы блокировок (locks) при транзакциях в SQL-запросах?

Ответ

Да, сталкивался. Классический случай — взаимная блокировка (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).

Как мы решали эту проблему:

  1. Установка строгого порядка блокировки записей. Всегда обновляли счета в порядке возрастания user_id. Это гарантировало, что все транзакции запрашивают блокировки в одной последовательности, исключая circular wait.
  2. Сокращение времени транзакции. Выносили логику валидации и вычислений за пределы транзакции, оставляя внутри только минимальные UPDATE/INSERT.
  3. Использование SELECT ... FOR UPDATE SKIP LOCKED для обработки очередей задач, что позволяло избегать блокировок при конкурентном чтении.
  4. Настройка таймаута для запросов (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 ВИСИТ -> ВСЁ, ДЕДЛОК!

База это дело обнаружит, выберет одну несчастную транзакцию и прирежет её, выдав ошибку. Остальная — проскочит.

Как мы с этим боролись, чтобы не было волнения ебать:

  1. Жёсткий порядок блокировок. Это самое главное! Договорились, что всегда будем обновлять счета по возрастанию user_id. Сначала меньший ID, потом больший. Все транзакции начинают хватать замки в одной очереди, и циклического ожидания не возникает. Просто, но работает, как швейцарские часы.

  2. Дробили транзакции. Выносили всю подготовительную херню — валидацию, расчёты — за пределы транзакции. Внутри оставляли голые UPDATE и INSERT, чтобы она быстрее делала своё дело и отпускала замки. Чем дольше висит транзакция, тем выше шанс наколотить другую.

  3. Использовали SELECT ... FOR UPDATE SKIP LOCKED. Это просто магия для очередей задач. Если запись уже кто-то держит, мы её просто пропускаем и берём следующую свободную. Никаких взаимных блокировок, все довольны.

  4. Ставили таймауты. Настроили lock_timeout в Постгресе. Если запрос не может получить блокировку за N миллисекунд — он не висит вечно, а падает с понятной ошибкой. Приложение ловит её и может, например, откатиться и повторить операцию чуть позже. Доверия ебать ноль, но зато система не ложится полностью.