Что будет, если транзакция попытается залочить запись, залоченную другой транзакцией

Если транзакция попытается залочить запись, которая уже была залочена другой транзакцией, то поведение зависит от типа блокировки, настроек СУБД, а также от того, как долго продолжается блокировка и какие действия предпринимает система. В большинстве случаев блокирующийся запрос будет ожидать освобождения ресурса. Ниже рассмотрим этот процесс максимально подробно, на примере реляционных баз данных (например, PostgreSQL, MySQL, Oracle).

🔒 Что такое блокировка (lock) записи?

Блокировка записи означает, что СУБД предотвращает другим транзакциям изменение или чтение определённых строк таблицы до завершения текущей транзакции. Это делается для обеспечения согласованности данных в многопользовательской среде.

🔁 Поведение при попытке залочить уже заблокированную запись

✅ Ожидание блокировки (Lock Wait)

Когда транзакция T2 пытается получить блокировку строки, которая уже заблокирована транзакцией T1, СУБД помещает T2 в очередь ожидания. Она будет ждать:

  • Пока T1 не выполнит COMMIT или ROLLBACK

  • Или пока не истечёт таймаут ожидания (lock_timeout, innodb_lock_wait_timeout и др.)

Пример на PostgreSQL:

\-- Транзакция 1
BEGIN;
SELECT \* FROM accounts WHERE id = 1 FOR UPDATE;
\-- блокировка строки id=1
\-- Транзакция 2 (в другом сеансе)
BEGIN;
SELECT \* FROM accounts WHERE id = 1 FOR UPDATE;
\-- будет ждать завершения Транзакции 1

⏱ Таймаут блокировки

Если в течение определённого времени транзакция не может получить блокировку, будет выброшено исключение.

В PostgreSQL:

SET lock_timeout = '5s';
Если блокировка не освобождается в течение 5 секунд, запрос завершится с ошибкой:
ERROR: canceling statement due to lock timeout

В MySQL:

SET innodb_lock_wait_timeout = 10;

Если ожидание длится дольше 10 секунд:

ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

🧠 Почему важно понимать поведение блокировок

Если не учитывать конкуренцию за ресурсы, возможно:

  • Падение производительности: транзакции «зависают» в ожидании

  • **Появление взаимных блокировок (deadlock)
    **

💥 Deadlock (взаимная блокировка)

Если две или более транзакции блокируют друг друга, возникает «взаимная блокировка». СУБД обнаруживает её и отменяет одну из транзакций, чтобы разорвать цикл.

Пример:

  1. T1 блокирует строку A и хочет заблокировать строку B

  2. T2 блокирует строку B и хочет заблокировать строку A

Это приводит к взаимному ожиданию, и одна из транзакций будет аварийно завершена:

ERROR: deadlock detected
DETAIL: Process 123 waits for ShareLock on transaction 456; blocked by process 789.

🔐 Типы блокировок (на примере PostgreSQL)

  1. **Row-level lock:
    **

    • SELECT ... FOR UPDATE блокирует строки для записи

    • SELECT ... FOR SHARE позволяет чтение, но блокирует запись

  2. **Table-level lock:
    **

    • LOCK TABLE блокирует всю таблицу (например, для DDL операций)
  3. **Advisory Lock (логическая блокировка):
    **

    • Блокировка по произвольному ключу (например, pg_advisory_lock)

🚦 Стратегии борьбы с конфликтами блокировок

  1. **Минимизировать время удержания блокировок:
    **

    • Делать меньше работы внутри транзакции

    • Избегать долгих вычислений между BEGIN и COMMIT

  2. **Последовательный порядок доступа к ресурсам:
    **

    • Все транзакции должны блокировать ресурсы в одинаковом порядке
  3. **Установка таймаутов:
    **

    • Настроить ограничения ожидания блокировки
  4. **Обработка ошибок блокировок:
    **

    • Автоматическая повторная попытка транзакции при deadlock
  5. **Оптимистичная блокировка:
    **

    • Проверка изменений по version полю (например, lock_version)

🔄 Жизненный цикл при блокировке строки

  1. **Транзакция A начинает
    **
  2. Делает SELECT ... FOR UPDATE по строке X

  3. Система блокирует строку X

  4. Транзакция B делает SELECT ... FOR UPDATE по X

  5. B ждёт завершения A

  6. Если A завершает COMMIT, то B получает блокировку

  7. Если A долго не завершает, B либо ждёт, либо падает по таймауту

📊 Визуализация ожидания (в PostgreSQL):

SELECT blocked_locks.pid AS blocked_pid,

blocking_locks.pid AS blocking_pid,

blocked_activity.query AS blocked_query,

blocking_activity.query AS blocking_query

FROM pg_catalog.pg_locks blocked_locks

JOIN pg_catalog.pg_stat_activity blocked_activity

ON blocked_activity.pid = blocked_locks.pid

JOIN pg_catalog.pg_locks blocking_locks

ON blocking_locks.locktype = blocked_locks.locktype

AND blocking_locks.DATABASE IS NOT DISTINCT FROM blocked_locks.DATABASE

AND blocking_locks.relation IS NOT DISTINCT FROM blocked_locks.relation

AND blocking_locks.page IS NOT DISTINCT FROM blocked_locks.page

AND blocking_locks.tuple IS NOT DISTINCT FROM blocked_locks.tuple

AND blocking_locks.transactionid IS NOT DISTINCT FROM blocked_locks.transactionid

AND blocking_locks.classid IS NOT DISTINCT FROM blocked_locks.classid

AND blocking_locks.objid IS NOT DISTINCT FROM blocked_locks.objid

AND blocking_locks.objsubid IS NOT DISTINCT FROM blocked_locks.objsubid

AND blocking_locks.pid != blocked_locks.pid

JOIN pg_catalog.pg_stat_activity blocking_activity

ON blocking_activity.pid = blocking_locks.pid;

Если транзакция пытается залочить уже залоченную запись, она будет ожидать освобождения блокировки или получит исключение по таймауту или deadlock, в зависимости от ситуации. Управление блокировками — ключевой аспект при проектировании конкурентных и отказоустойчивых систем.