Для простоты создадим таблицу без первичного ключа.
=> CREATE DATABASE locks_rows;
CREATE DATABASE
=> \c locks_rows
You are now connected to database "locks_rows" as user "postgres".
=> CREATE TABLE accounts(acc_no integer, amount numeric);
CREATE TABLE
=> INSERT INTO accounts VALUES (1,1000.00),(2,2000.00),(3,3000.00);
INSERT 0 3
Создадим представление над pg_locks как в демонстрации:
=> CREATE VIEW locks AS SELECT pid, locktype, CASE locktype WHEN 'relation' THEN relation::REGCLASS::text WHEN 'virtualxid' THEN virtualxid::text WHEN 'transactionid' THEN transactionid::text WHEN 'tuple' THEN relation::REGCLASS::text||':'||tuple::text END AS lockid, mode, granted FROM pg_locks;
CREATE VIEW
Первая транзакция обновляет и, соответственно, блокирует строку:
=> BEGIN;
BEGIN
=> SELECT txid_current(), pg_backend_pid();
txid_current | pg_backend_pid --------------+---------------- 215338 | 29324 (1 row)
=> UPDATE accounts SET amount = amount + 100.00 WHERE acc_no = 1;
UPDATE 1
Вторая транзакция делает то же самое:
=> \c locks_rows
You are now connected to database "locks_rows" as user "postgres".
=> BEGIN;
BEGIN
=> SELECT txid_current(), pg_backend_pid();
txid_current | pg_backend_pid --------------+---------------- 215339 | 29457 (1 row)
=> UPDATE accounts SET amount = amount + 100.00 WHERE acc_no = 1;
И третья:
=> \c locks_rows
You are now connected to database "locks_rows" as user "postgres".
=> BEGIN;
BEGIN
=> SELECT txid_current(), pg_backend_pid();
txid_current | pg_backend_pid --------------+---------------- 215340 | 29531 (1 row)
=> UPDATE accounts SET amount = amount + 100.00 WHERE acc_no = 1;
Блокировки для первой транзакции:
=> SELECT * FROM locks WHERE pid = 29324;
pid | locktype | lockid | mode | granted -------+---------------+----------+------------------+--------- 29324 | relation | pg_locks | AccessShareLock | t 29324 | relation | locks | AccessShareLock | t 29324 | relation | accounts | RowExclusiveLock | t 29324 | virtualxid | 6/5 | ExclusiveLock | t 29324 | transactionid | 215338 | ExclusiveLock | t (5 rows)
Блокировки для второй транзакции:
=> SELECT * FROM locks WHERE pid = 29457;
pid | locktype | lockid | mode | granted -------+---------------+------------+------------------+--------- 29457 | relation | accounts | RowExclusiveLock | t 29457 | virtualxid | 2/84 | ExclusiveLock | t 29457 | transactionid | 215339 | ExclusiveLock | t 29457 | transactionid | 215338 | ShareLock | f 29457 | tuple | accounts:1 | ExclusiveLock | t (5 rows)
По сравнению с первой транзакцией:
Блокировки для третьей транзакции:
=> SELECT * FROM locks WHERE pid = 29531;
pid | locktype | lockid | mode | granted -------+---------------+------------+------------------+--------- 29531 | relation | accounts | RowExclusiveLock | t 29531 | virtualxid | 4/100415 | ExclusiveLock | t 29531 | transactionid | 215340 | ExclusiveLock | t 29531 | tuple | accounts:1 | ExclusiveLock | f (4 rows)
Транзакция ожидает получение блокировки типа tuple для обновляемой строки.
Общую картину текущих ожиданий можно увидеть в представлении pg_stat_activity. Для удобства можно добавить и информацию о блокирующих процессах:
=> SELECT pid, wait_event_type, wait_event, pg_blocking_pids(pid) FROM pg_stat_activity WHERE backend_type = 'client backend';
pid | wait_event_type | wait_event | pg_blocking_pids -------+-----------------+---------------+------------------ 29457 | Lock | transactionid | {29324} 29531 | Lock | tuple | {29457} 29324 | | | {} (3 rows)
=> ROLLBACK;
ROLLBACK
=> ROLLBACK;
UPDATE 1 ROLLBACK
=> ROLLBACK;
UPDATE 1 ROLLBACK
Воспроизведем взаимоблокировку трех транзакций.
=> BEGIN;
BEGIN
=> UPDATE accounts SET amount = amount - 100.00 WHERE acc_no = 1;
UPDATE 1
=> BEGIN;
BEGIN
=> UPDATE accounts SET amount = amount - 100.00 WHERE acc_no = 2;
UPDATE 1
=> BEGIN;
BEGIN
=> UPDATE accounts SET amount = amount - 100.00 WHERE acc_no = 3;
UPDATE 1
=> UPDATE accounts SET amount = amount + 100.00 WHERE acc_no = 2;
=> UPDATE accounts SET amount = amount + 100.00 WHERE acc_no = 3;
=> UPDATE accounts SET amount = amount + 100.00 WHERE acc_no = 1;
=> COMMIT;
=> COMMIT;
=> COMMIT;
ERROR: deadlock detected DETAIL: Process 29324 waits for ShareLock on transaction 215342; blocked by process 29457. Process 29457 waits for ShareLock on transaction 215343; blocked by process 29531. Process 29531 waits for ShareLock on transaction 215341; blocked by process 29324. HINT: See server log for query details. CONTEXT: while updating tuple (0,2) in relation "accounts" ROLLBACK
UPDATE 1 COMMIT
UPDATE 1 COMMIT
Вот какую информацию о взаимоблокировке можно получить из журнала:
postgres$ tail -n 10 /home/postgres/logfile
2019-08-12 17:25:55.279 MSK [29324] ERROR: deadlock detected 2019-08-12 17:25:55.279 MSK [29324] DETAIL: Process 29324 waits for ShareLock on transaction 215342; blocked by process 29457. Process 29457 waits for ShareLock on transaction 215343; blocked by process 29531. Process 29531 waits for ShareLock on transaction 215341; blocked by process 29324. Process 29324: UPDATE accounts SET amount = amount + 100.00 WHERE acc_no = 2; Process 29457: UPDATE accounts SET amount = amount + 100.00 WHERE acc_no = 3; Process 29531: UPDATE accounts SET amount = amount + 100.00 WHERE acc_no = 1; 2019-08-12 17:25:55.279 MSK [29324] HINT: See server log for query details. 2019-08-12 17:25:55.279 MSK [29324] CONTEXT: while updating tuple (0,2) in relation "accounts" 2019-08-12 17:25:55.279 MSK [29324] STATEMENT: UPDATE accounts SET amount = amount + 100.00 WHERE acc_no = 2;
Команда UPDATE блокирует строки по мере их обновления. Это происходит не одномоментно.
Поэтому если одна команда будет обновлять строки в одном порядке, а другая - в другом, они могут взаимозаблокироваться. Это может произойти, если для команд будут построены разные планы выполнения, например, одна будет читать таблицу последовательно, а другая - по индексу.
Получить такую ситуацию непросто даже специально, в реальной работе она маловероятна. Проиллюстрировать ее проще всего с помощью курсоров, поскольку это дает возможность управлять порядком чтения.
=> BEGIN;
BEGIN
=> DECLARE c1 CURSOR FOR SELECT * FROM accounts ORDER BY acc_no FOR UPDATE;
DECLARE CURSOR
=> BEGIN;
BEGIN
=> DECLARE c2 CURSOR FOR SELECT * FROM accounts ORDER BY acc_no DESC -- в обратную сторону FOR UPDATE;
DECLARE CURSOR
=> FETCH c1;
acc_no | amount --------+--------- 1 | 1100.00 (1 row)
=> FETCH c2;
acc_no | amount --------+--------- 3 | 3000.00 (1 row)
=> FETCH c1;
acc_no | amount --------+--------- 2 | 1900.00 (1 row)
=> FETCH c2;
Вторая команда ожидает блокировку...
=> FETCH c1;
Произошла взаимоблокировка. И через некоторое время:
acc_no | amount --------+--------- 3 | 3000.00 (1 row)
ERROR: deadlock detected DETAIL: Process 29531 waits for ShareLock on transaction 215344; blocked by process 29457. Process 29457 waits for ShareLock on transaction 215345; blocked by process 29531. HINT: See server log for query details. CONTEXT: while locking tuple (0,8) in relation "accounts"
=> COMMIT;
=> COMMIT;