=> CREATE FUNCTION row_locks(relname text, pageno integer)
RETURNS TABLE(
ctid tid, xmax text,
lock_only text, is_multi text,
keys_upd text, keyshr text,
shr text
)
AS $$
SELECT (pageno,lp)::text::tid,
t_xmax,
CASE WHEN t_infomask & 128 = 128 THEN 't' END,
CASE WHEN t_infomask & 4096 = 4096 THEN 't' END,
CASE WHEN t_infomask2 & 8192 = 8192 THEN 't' END,
CASE WHEN t_infomask & 16 = 16 THEN 't' END,
CASE WHEN t_infomask & 16+64 = 16+64 THEN 't' END
FROM heap_page_items(get_raw_page(relname,pageno))
ORDER BY lp;
$$ LANGUAGE sql;
现在在 accounts 表上开启一个事务,以更新第一个帐户的余额 (键保持不变) 和第二个帐户的 ID (键更新了):
=> BEGIN;
=> UPDATE accounts SET amount = amount + 100.00 WHERE id = 1;
=> UPDATE accounts SET id = 20 WHERE id = 2;
=> CREATE VIEW locks_accounts AS
SELECT pid,
locktype,
CASE locktype
WHEN 'relation' THEN relation::regclass::text
WHEN 'transactionid' THEN transactionid::text
WHEN 'tuple' THEN relation::regclass||'('||page||','||tuple||')'
END AS lockid,
mode,
granted
FROM pg_locks
WHERE locktype in ('relation','transactionid','tuple')
AND (locktype != 'relation' OR relation = 'accounts'::regclass)
ORDER BY 1, 2, 3;
让我们开启第一个事务并更新一行:
=> BEGIN;
=> SELECT txid_current(), pg_backend_pid();
txid_current | pg_backend_pid
−−−−−−−−−−−−−−+−−−−−−−−−−−−−−−−
122863 | 30723
(1 row)
=> UPDATE accounts SET amount = amount + 100.00 WHERE id = 1;
该事务已完成工作流的所有四个步骤,现在正持有表上的锁:
=> SELECT * FROM locks_accounts WHERE pid = 30723;
pid | locktype | lockid | mode | granted
−−−−−−−+−−−−−−−−−−−−−−−+−−−−−−−−−−+−−−−−−−−−−−−−−−−−−+−−−−−−−−−
30723 | relation | accounts | RowExclusiveLock | t
30723 | transactionid | 122863 | ExclusiveLock | t
(2 rows)
启动第二个事务并尝试更新同一行。该事务将挂起,在等待锁:
=> BEGIN;
=> SELECT txid_current(), pg_backend_pid();
txid_current | pg_backend_pid
−−−−−−−−−−−−−−+−−−−−−−−−−−−−−−−
122864 | 30794
(1 row)
=> UPDATE accounts SET amount = amount + 100.00 WHERE id = 1;
图 13.2
第二个事务只进行到第二步。因此,除了锁定表及其自身 ID 之外,它还添加了两个锁,这两个锁也在 pg_locks 视图中反映了出来:第一步获取的元组锁和第二步请求的第二个事务 ID 的锁:
=> SELECT * FROM locks_accounts WHERE pid = 30794;
pid | locktype | lockid | mode | granted
−−−−−−−+−−−−−−−−−−−−−−−+−−−−−−−−−−−−−−−+−−−−−−−−−−−−−−−−−−+−−−−−−−−−
30794 | relation | accounts | RowExclusiveLock | t
30794 | transactionid | 122863 | ShareLock | f
30794 | transactionid | 122864 | ExclusiveLock | t
30794 | tuple | accounts(0,1) | ExclusiveLock | t
(4 rows)
第三个事务将停在第一步。它尝试获取元组上的锁,并在此处停下:
=> BEGIN;
=> SELECT txid_current(), pg_backend_pid();
txid_current | pg_backend_pid
−−−−−−−−−−−−−−+−−−−−−−−−−−−−−−−
122865 | 30865
(1 row)
=> UPDATE accounts SET amount = amount + 100.00 WHERE id = 1;
=> SELECT * FROM locks_accounts WHERE pid = 30865;
pid | locktype | lockid | mode | granted
−−−−−−−+−−−−−−−−−−−−−−−+−−−−−−−−−−−−−−−+−−−−−−−−−−−−−−−−−−+−−−−−−−−−
30865 | relation | accounts | RowExclusiveLock | t
30865 | transactionid | 122865 | ExclusiveLock | t
30865 | tuple | accounts(0,1) | ExclusiveLock | f
(3 rows)
第四个以及所有后续尝试更新这一行的事务在这方面与第三个事务没有区别:所有事务都将等待相同的元组锁。
=> BEGIN;
=> SELECT txid_current(), pg_backend_pid();
txid_current | pg_backend_pid
−−−−−−−−−−−−−−+−−−−−−−−−−−−−−−−
122866 | 30936
(1 row)
=> UPDATE accounts SET amount = amount + 100.00 WHERE id = 1;
=> SELECT * FROM locks_accounts WHERE pid = 30865;
pid | locktype | lockid | mode | granted
−−−−−−−+−−−−−−−−−−−−−−−+−−−−−−−−−−−−−−−+−−−−−−−−−−−−−−−−−−+−−−−−−−−−
30865 | relation | accounts | RowExclusiveLock | t
30865 | transactionid | 122865 | ExclusiveLock | t
30865 | tuple | accounts(0,1) | ExclusiveLock | f
(3 rows)
=> BEGIN;
=> UPDATE accounts SET amount = amount - 10.00 WHERE id = 2;
UPDATE 1
现在,第一个事务尝试增加第二个账户中的金额,但发现相应的行已被锁定:
=> UPDATE accounts SET amount = amount + 100.00 WHERE id = 2;
然后第二个事务尝试更新第一个帐户,但也被锁定了:
=> UPDATE accounts SET amount = amount + 10.00 WHERE id = 1;
这种循环等待永远无法自行解决。由于无法在一秒内获得资源,第一个事务启动死锁检测,并被服务器中止:
ERROR: deadlock detected
DETAIL: Process 30423 waits for ShareLock on transaction 122877;
blocked by process 30723.
Process 30723 waits for ShareLock on transaction 122876; blocked by
process 30423.
HINT: See server log for query details.
CONTEXT: while updating tuple (0,2) in relation "accounts"
=> CREATE FUNCTION inc_slow(n numeric)
RETURNS numeric
AS $$
SELECT pg_sleep(1);
SELECT n + 100.00;
$$ LANGUAGE sql;
第一个 UPDATE 命令将更新所有元组。执行计划依赖于对整个表的顺序扫描。
=> EXPLAIN (costs off)
UPDATE accounts SET amount = inc_slow(amount);
QUERY PLAN
−−−−−−−−−−−−−−−−−−−−−−−−−−−
Update on accounts
−> Seq Scan on accounts
(2 rows)
为了确保堆页面基于 amount 列以升序存储行,我们需要截断表并重新插入行:
=> TRUNCATE accounts;
=> INSERT INTO accounts(id, client, amount)
VALUES
(1,'alice',100.00),
(2,'bob',200.00),
(3,'charlie',300.00);
=> ANALYZE accounts;
=> SELECT ctid, * FROM accounts;
ctid | id | client | amount
−−−−−−−+−−−−+−−−−−−−−−+−−−−−−−−
(0,1) | 1 | alice | 100.00
(0,2) | 2 | bob | 200.00
(0,3) | 3 | charlie | 300.00
(3 rows)
顺序扫描将以相同的顺序更新行 (但对于大表来说并非总是如此)。
开始更新:
=> UPDATE accounts SET amount = inc_slow(amount);
同时,我们将在另一个会话中禁止顺序扫描:
=> SET enable_seqscan = off;
因此,规划器为下一个 UPDATE 命令选择了索引扫描。
=> EXPLAIN (costs off)
UPDATE accounts SET amount = inc_slow(amount)
WHERE amount > 100.00;
QUERY PLAN
−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−
Update on accounts
−> Index Scan using accounts_amount_idx on accounts
Index Cond: (amount > 100.00)
(3 rows)
第二行和第三行满足条件;由于索引是降序的,因此行将以相反的顺序被更新。
让我们开始下一个更新:
=> UPDATE accounts SET amount = inc_slow(amount)
WHERE amount > 100.00;
ERROR: deadlock detected
DETAIL: Process 30794 waits for ShareLock on transaction 122883;
blocked by process 30723.
Process 30723 waits for ShareLock on transaction 122884; blocked by
process 30794.
HINT: See server log for query details.
CONTEXT: while updating tuple (0,2) in relation "accounts"