行锁在PG中比较特殊,在9.4以前,只有两种类型的行锁, FOR UPDATE 和FOR SHARE,因为只有两种锁,粒度比较大,极大的影响了并发性。所以从9.4开始引入了FOR KEY SHARE和FOR NO KEY UPDATE这两种行锁。目前这四种行锁,其中两个是排他性锁,一次只能被一个事务获取;另外两个则是可由多个事务同时持有的共享锁。
有时候很多人使用SELECT…FOR UPDATE显式地锁定某些行以防止并发更新。但大多数时候,这种行为太过了。只有在打算删除整行或修改主键列或唯一键列时,才应该使用FOR UPDATE锁。否则,最好使用FOR NO KEY UPDATE锁,这样就不会阻塞对引用正在更新的表的表的插入。
PG中行锁保存在当前堆元组的头部,而且行锁实际上是堆页面的一个属性,不是真正意义上的锁。由于不在内存中,所以对内存也不会有影响。但是每个行锁都会修改表,修改后的块必须写回持久存储。所以行锁也有额外的I/O负载。
当我们获取行锁的时候会发生以下流程步骤:
- 首先检查是否有别的事物在该行上持有锁。
- 如果没有,第一个事物则在该行加上一个元组锁,元组锁是在内存中的,该锁的目的是如果多个会话想要锁定同一行,它们必须排队。这个队列由元组锁维护。该锁保护行免受并发修改,直到我们将锁写入行本身。
- 第二个事物检查到其他事务持有该行上的锁,则该事物持有元组锁并进入睡眠状态,等待第一个事务结束。一旦第一个事物结束,阻塞也就结束,我们就可以继续了。但首先,我们检查在等待的过程中是否存在对该行的并发修改。如果是,则该行为取决于当前事务隔离级别,如果是READ COMMITTED隔离级别,那么拿取已提交事物的最新行版本,如果是其它更高的隔离级别,那么会抛出一个序列化错误。
- 如果第一个事物没有提交,第三个事务(以及随后的任何事务)也看到该行被锁定。它阻塞并等待行上的元组锁。
- 以上事物一旦所有阻塞的行锁都消失了,那么我们得到了该行的最新版本,就可以继续了。最后修改行上的xmax和一些提示位,并释放元组锁。
模拟一下以上的流程,看下具体示例:
session 1: 创建测试表,并模拟一个我们正常的更新,一般来说for no key update和普通执行的update命令类似
postgres=# create table test (id int, info text);
CREATE TABLE
postgres=# insert into test values (1,'a'),(2,'b');
INSERT 0 2
postgres=# begin ;
BEGIN
postgres=*# select txid_current(), pg_backend_pid();
txid_current | pg_backend_pid
--------------+----------------
2277673 | 1767
(1 row)
postgres=*# select * from test where id=1 for no key update;
id | info
----+------
1 | a
(1 row)
session 2:
postgres=# begin ;
BEGIN
postgres=*# select txid_current(), pg_backend_pid();
txid_current | pg_backend_pid
--------------+----------------
2277674 | 2447
(1 row)
postgres=*# select * from test where id=1 for no key update;
此处夯住
session 3:
postgres=# begin;
BEGIN
postgres=*# select txid_current(), pg_backend_pid();
txid_current | pg_backend_pid
--------------+----------------
2277675 | 4229
(1 row)
postgres=*# select * from test where id=1 for no key update;
#等待持有tuple锁
如下图,session4分别查一下持有的锁,注意一下tuple锁,session2的granted为t,session3为f
一旦session1,pid为1767的事物提交了,那么就如下图,session2,pid为2447的会释放tuple锁,然后session3,pid为4229的获取tuple锁,granted变为t。
当然可以通过pgrowlocks插件查看锁内容,如下:
postgres=# SELECT * FROM pgrowlocks('test');
locked_row | locker | multi | xids | modes | pids
------------+---------+-------+-----------+-----------------------+--------
(0,1) | 2277674 | f | {2277674} | {"For No Key Update"} | {2447}
(1 row)
以上介绍了行锁的一些概念和原理,下面我们看一下这四类锁的冲突矩阵,以及每种锁影响的操作。
FOR UPDATE:该模式允许修改任何元组字段,甚至删除整个元组
FOR NO KEY UPDATE:对除主(唯一)键外的字段更新,此更改也不影响外键
FOR SHARE:当我们需要读取一行,但不允许其他事务更改它时,使用该模式
FOR KEY SHARE:读该行的键值,但只允许对除键外的其他字段更新。在检查外键约束时会自动使用该锁。
排它模式的两个锁:FOR UPDATE 和 FOR NO KEY UPDATE
#创建测试表
CREATE TABLE accounts(
acc_no integer PRIMARY KEY,
amount numeric
);
INSERT INTO accounts VALUES (1, 100.00), (2, 200.00), (3, 300.00);
#创建插件
CREATE EXTENSION pageinspect;
CREATE EXTENSION pgrowlocks;
#更新表,一个是key update一个是no key update
BEGIN;
UPDATE accounts SET amount = amount + 100.00 WHERE acc_no = 1; --no key update
UPDATE accounts SET acc_no = 20 WHERE acc_no = 2; --key update = for update
#查看标志位
SELECT '(0,'||lp||')' AS ctid,
t_xmax as xmax,
CASE WHEN (t_infomask & 128) > 0 THEN 't' END AS lock_only,
CASE WHEN (t_infomask & 4096) > 0 THEN 't' END AS is_multi,
CASE WHEN (t_infomask2 & 8192) > 0 THEN 't' END AS keys_upd,
CASE WHEN (t_infomask & 16) > 0 THEN 't' END AS keyshr_lock,
CASE WHEN (t_infomask & 16+64) = 16+64 THEN 't' END AS shr_lock
FROM heap_page_items(get_raw_page('accounts',0))
ORDER BY lp;
ctid | xmax | lock_only | is_multi | keys_upd | keyshr_lock | shr_lock
-------+---------+-----------+----------+----------+-------------+----------
(0,1) | 2277687 | | | | |
(0,2) | 2277687 | | | t | |
(0,3) | 0 | | | | |
(0,4) | 0 | | | | |
(0,5) | 0 | | | | |
(5 rows)
#查看锁模式
SELECT * FROM pgrowlocks('accounts');
locked_row | locker | multi | xids | modes | pids
------------+---------+-------+-----------+-------------------+---------
(0,1) | 2277687 | f | {2277687} | {"No Key Update"} | {27249}
(0,2) | 2277687 | f | {2277687} | {Update} | {27249}
(2 rows)
#再看一下select ... for和上面的不同
postgres=# begin;
BEGIN
postgres=*# SELECT * FROM accounts WHERE acc_no = 1 FOR NO KEY UPDATE;
acc_no | amount
--------+--------
1 | 100.00
(1 row)
postgres=*# SELECT * FROM accounts WHERE acc_no = 2 FOR UPDATE;
acc_no | amount
--------+--------
2 | 200.00
(1 row)
#标志位lock_only为true,和前面的实验在标志位设置还是有区别的,说明行只是被锁定,并没有被删除,行还是活动的
postgres=# SELECT '(0,'||lp||')' AS ctid,
t_xmax as xmax,
CASE WHEN (t_infomask & 128) > 0 THEN 't' END AS lock_only,
CASE WHEN (t_infomask & 4096) > 0 THEN 't' END AS is_multi,
CASE WHEN (t_infomask2 & 8192) > 0 THEN 't' END AS keys_upd,
CASE WHEN (t_infomask & 16) > 0 THEN 't' END AS keyshr_lock,
CASE WHEN (t_infomask & 16+64) = 16+64 THEN 't' END AS shr_lock
FROM heap_page_items(get_raw_page('accounts',0))
ORDER BY lp;
ctid | xmax | lock_only | is_multi | keys_upd | keyshr_lock | shr_lock
-------+---------+-----------+----------+----------+-------------+----------
(0,1) | 2277689 | t | | | |
(0,2) | 2277689 | t | | t | |
(0,3) | 0 | | | | |
(0,4) | 0 | | | | |
(0,5) | 0 | | | | |
(5 rows)
postgres=# SELECT * FROM pgrowlocks('accounts');
locked_row | locker | multi | xids | modes | pids
------------+---------+-------+-----------+-----------------------+---------
(0,1) | 2277689 | f | {2277689} | {"For No Key Update"} | {27249}
(0,2) | 2277689 | f | {2277689} | {"For Update"} | {27249}
(2 rows)
共享模式的两个锁FOR SHARE 和 FOR KEY SHARE
#select ... for语句加锁
postgres=# begin;
BEGIN
postgres=*# SELECT * FROM accounts WHERE acc_no = 1 FOR KEY SHARE;
acc_no | amount
--------+--------
1 | 100.00
(1 row)
postgres=*# SELECT * FROM accounts WHERE acc_no = 2 FOR SHARE;
acc_no | amount
--------+--------
2 | 200.00
(1 row)
#查看标志位,在这两种情况下,都设置了keyshr_lock标志位,并且SHARE模式通过多设置一个标志位来表示。
postgres=# SELECT '(0,'||lp||')' AS ctid,
t_xmax as xmax,
CASE WHEN (t_infomask & 128) > 0 THEN 't' END AS lock_only,
CASE WHEN (t_infomask & 4096) > 0 THEN 't' END AS is_multi,
CASE WHEN (t_infomask2 & 8192) > 0 THEN 't' END AS keys_upd,
CASE WHEN (t_infomask & 16) > 0 THEN 't' END AS keyshr_lock,
CASE WHEN (t_infomask & 16+64) = 16+64 THEN 't' END AS shr_lock
FROM heap_page_items(get_raw_page('accounts',0))
ORDER BY lp;
ctid | xmax | lock_only | is_multi | keys_upd | keyshr_lock | shr_lock
-------+---------+-----------+----------+----------+-------------+----------
(0,1) | 2277690 | t | | | t |
(0,2) | 2277690 | t | | | t | t
(0,3) | 0 | | | | |
(0,4) | 0 | | | | |
(0,5) | 0 | | | | |
(5 rows)
#查看行锁
postgres=# SELECT * FROM pgrowlocks('accounts');
locked_row | locker | multi | xids | modes | pids
------------+---------+-------+-----------+-------------------+---------
(0,1) | 2277690 | f | {2277690} | {"For Key Share"} | {27249}
(0,2) | 2277690 | f | {2277690} | {"For Share"} | {27249}
(2 rows)
#因为For Key Share和For no key Update是兼容不冲突的,如下:
postgres=# begin;
BEGIN
postgres=*# UPDATE accounts SET amount = amount + 100.00 WHERE acc_no = 1;
UPDATE 1
#查看标志位,is_multi设置为true
postgres=# SELECT '(0,'||lp||')' AS ctid,
t_xmax as xmax,
CASE WHEN (t_infomask & 128) > 0 THEN 't' END AS lock_only,
CASE WHEN (t_infomask & 4096) > 0 THEN 't' END AS is_multi,
CASE WHEN (t_infomask2 & 8192) > 0 THEN 't' END AS keys_upd,
CASE WHEN (t_infomask & 16) > 0 THEN 't' END AS keyshr_lock,
CASE WHEN (t_infomask & 16+64) = 16+64 THEN 't' END AS shr_lock
FROM heap_page_items(get_raw_page('accounts',0))
ORDER BY lp;
ctid | xmax | lock_only | is_multi | keys_upd | keyshr_lock | shr_lock
-------+---------+-----------+----------+----------+-------------+----------
(0,1) | 12 | | t | | |
(0,2) | 2277690 | t | | | t | t
(0,3) | 0 | | | | |
(0,4) | 0 | | | | |
(0,5) | 0 | | | | |
(0,6) | 2277690 | t | | | t |
#可以看到同一行,持有2个行锁"Key Share"和"No Key Update"
postgres=# SELECT * FROM pgrowlocks('accounts');
locked_row | locker | multi | xids | modes | pids
------------+---------+-------+-------------------+-------------------------------+---------------
(0,1) | 12 | t | {2277690,2277691} | {"Key Share","No Key Update"} | {27249,30530}
(0,2) | 2277690 | f | {2277690} | {"For Share"} | {27249}
(2 rows)
1 单事务锁行时, 各种行锁模式和infomask标志位的关系 :
1.1select … for key share
t_infomask 设置 HEAP_XMAX_LOCK_ONLY 以及 HEAP_XMAX_KEYSHR_LOCK
1.2 select … for share
t_infomask 设置 HEAP_XMAX_LOCK_ONLY 以及 HEAP_XMAX_SHR_LOCK [也就是HEAP_XMAX_KEYSHR_LOCK和HEAP_XMAX_EXCL_LOCK]
1.3 select … for no key update
t_infomask 设置 HEAP_XMAX_LOCK_ONLY 以及 HEAP_XMAX_EXCL_LOCK
1.4 select … for update [也就是key update]
t_infomask 设置 HEAP_XMAX_LOCK_ONLY 以及 HEAP_XMAX_EXCL_LOCK 同时 t_infomask2 设置 HEAP_KEYS_UPDATED
1.5 update命令不更新主键
如:[UPDATE accounts SET amount = amount + 100.00 WHERE acc_no = 1;]
t_infomask没有设置相关标志位
1.6 update命令更新主键
如:[UPDATE accounts SET acc_no = 20 WHERE acc_no = 2;]
t_infomask2设置了HEAP_KEYS_UPDATED
标志位见源码src/include/access/htup_details.h
/*
* information stored in t_infomask:
*/
#define HEAP_HASNULL 0x0001 /* has null attribute(s) */
#define HEAP_HASVARWIDTH 0x0002 /* has variable-width attribute(s) */
#define HEAP_HASEXTERNAL 0x0004 /* has external stored attribute(s) */
#define HEAP_HASOID_OLD 0x0008 /* has an object-id field */
#define HEAP_XMAX_KEYSHR_LOCK 0x0010 /* xmax is a key-shared locker */
#define HEAP_COMBOCID 0x0020 /* t_cid is a combo CID */
#define HEAP_XMAX_EXCL_LOCK 0x0040 /* xmax is exclusive locker */
#define HEAP_XMAX_LOCK_ONLY 0x0080 /* xmax, if valid, is only a locker */
/* xmax is a shared locker */
#define HEAP_XMAX_SHR_LOCK (HEAP_XMAX_EXCL_LOCK | HEAP_XMAX_KEYSHR_LOCK)
#define HEAP_LOCK_MASK (HEAP_XMAX_SHR_LOCK | HEAP_XMAX_EXCL_LOCK | \
HEAP_XMAX_KEYSHR_LOCK)
#define HEAP_XMIN_COMMITTED 0x0100 /* t_xmin committed */
#define HEAP_XMIN_INVALID 0x0200 /* t_xmin invalid/aborted */
#define HEAP_XMIN_FROZEN (HEAP_XMIN_COMMITTED|HEAP_XMIN_INVALID)
#define HEAP_XMAX_COMMITTED 0x0400 /* t_xmax committed */
#define HEAP_XMAX_INVALID 0x0800 /* t_xmax invalid/aborted */
#define HEAP_XMAX_IS_MULTI 0x1000 /* t_xmax is a MultiXactId */
#define HEAP_UPDATED 0x2000 /* this is UPDATEd version of row */
#define HEAP_MOVED_OFF 0x4000 /* moved to another place by pre-9.0
* VACUUM FULL; kept for binary
* upgrade support */
#define HEAP_MOVED_IN 0x8000 /* moved from another place by pre-9.0
* VACUUM FULL; kept for binary
* upgrade support */
#define HEAP_MOVED (HEAP_MOVED_OFF | HEAP_MOVED_IN)
#define HEAP_XACT_MASK 0xFFF0 /* visibility-related bits */
*
* information stored in t_infomask2:
*/
#define HEAP_NATTS_MASK 0x07FF /* 11 bits for number of attributes */
/* bits 0x1800 are available */
#define HEAP_KEYS_UPDATED 0x2000 /* tuple was updated and key cols
* modified, or tuple deleted */
#define HEAP_HOT_UPDATED 0x4000 /* tuple was HOT-updated */
#define HEAP_ONLY_TUPLE 0x8000 /* this is heap-only tuple */
#define HEAP2_XACT_MASK 0xE000 /* visibility-related bits */
查看标志位SQL
#可以根据需求调整t_infomask或者t_infomask2值,比如(t_infomask & 128) > 0 THEN 't' END AS lock_only
#比如宏定义#define HEAP_XMAX_LOCK_ONLY 0x0080,0x0080为16进制转为10进制为128,t_infomask按位和128求与大于0,则为真
SELECT '(0,'||lp||')' AS ctid,
t_xmax as xmax,
CASE WHEN (t_infomask & 128) > 0 THEN 't' END AS lock_only,
CASE WHEN (t_infomask & 4096) > 0 THEN 't' END AS is_multi,
CASE WHEN (t_infomask2 & 8192) > 0 THEN 't' END AS keys_upd,
CASE WHEN (t_infomask & 16) > 0 THEN 't' END AS keyshr_lock,
CASE WHEN (t_infomask & 16+64) = 16+64 THEN 't' END AS shr_lock
FROM heap_page_items(get_raw_page('accounts',0))
ORDER BY lp;
SELECT '(0,'||lp||')' AS ctid,
CASE lp_flags
WHEN 0 THEN 'unused'
WHEN 1 THEN 'normal'
WHEN 2 THEN 'redirect to '||lp_off
WHEN 3 THEN 'dead'
END AS state,
t_xmin as xmin,
t_xmax as xmax,
(t_infomask & 64) > 0 AS HEAP_XMAX_EXCL_LOCK,
(t_infomask & 128) > 0 AS HEAP_XMAX_LOCK_ONLY,
(t_infomask & 256) > 0 AS xmin_commited,
(t_infomask & 512) > 0 AS xmin_aborted,
(t_infomask & 1024) > 0 AS xmax_commited,
(t_infomask & 2048) > 0 AS xmax_aborted,
t_ctid
FROM heap_page_items(get_raw_page('accounts',0));
参考:
https://www.cybertec-postgresql.com/en/row-locks-in-postgresql/
https://www.cybertec-postgresql.com/en/whats-in-an-xmax/
https://habr.com/en/companies/postgrespro/articles/503008/