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

«Были ли проблемы блокировок (locks) при транзакциях в SQL-запросах?» — вопрос из категории Базы данных, который задают на 24% собеседований PHP Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

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