一、 四种行锁
1. 简介与兼容性分析
pg采用元组级常规锁+xmax结合的方式实现行锁。我们曾经提到过常规锁是有很多类TAG的(typedef enum LockTagType),其中 LOCKTAG_TUPLE就是用来对元组加锁的。
不单纯用元组级常规锁,是为了避免事务修改行过多时,锁表急剧增大导致性能劣化,并且锁表在共享内存中的大小是有限的。因此,行锁是不存储在内存中的。
pg中通常有两种方式会用到行锁:
- 对行执行update,delete操作
- 显式指定行锁(例如select for update)
在新版本中,显式加行锁共有4种写法
行锁类型 | 简介 |
FOR UPDATE | FOR UPDATE会锁定SELECT检索到的行,就好像它们要被更新。 这可以阻止它们被其他事务锁定、修改或者删除,直到当前事务结束。 DELETE和修改主键、唯一键值的UPDATE会获得这种锁模式。 |
FOR NO KEY UPDATE | 行为与FOR UPDATE类似,不过获得的锁较弱,不会阻塞SELECT FOR KEY SHARE。 不修改主键、唯一键值的UPDATE会获得这种锁模式。 |
FOR SHARE | 行为与FOR NO KEY UPDATE类似,不过它在每个检索到的行上获得一个共享锁而不是排他锁。 读该行,不允许对行进行更新 |
FOR KEY SHARE | 行为与FOR SHARE类似,不过锁更弱,不会被SELECT 读该行键值,但允许对除键外的其他字段更新。在外键检查时使用该锁 |
4种行锁兼容性如下
详细
4种行锁兼容性如下
详细可参考 官方文档第13章 13.3.2. Row-Level Locks
2. 对应源码
它们对应的源码在lockoptions.h文件
/*
* Possible lock modes for a tuple.
*/
typedef enum LockTupleMode
{
/* SELECT FOR KEY SHARE */
LockTupleKeyShare,
/* SELECT FOR SHARE */
LockTupleShare,
/* SELECT FOR NO KEY UPDATE, and UPDATEs that don't modify key columns */
LockTupleNoKeyExclusive,
/* SELECT FOR UPDATE, UPDATEs that modify key columns, and DELETE */
LockTupleExclusive
} LockTupleMode;
如前所述,pg行锁是由常规锁+xmax结合实现的,因此它需要建立常规锁与LockTupleMode之间的映射关系。
/*
* Each tuple lock mode has a corresponding heavyweight lock, and one or two
* corresponding MultiXactStatuses (one to merely lock tuples, another one to
* update them). This table (and the macros below) helps us determine the
* heavyweight lock mode and MultiXactStatus values to use for any particular
* tuple lock strength.
*
* Don't look at lockstatus/updstatus directly! Use get_mxact_status_for_lock
* instead.
*/
static const struct
{
LOCKMODE hwlock; // 对应的常规锁模式
int lockstatus; // 显式指定的锁模式
int updstatus; // 隐式指定的锁模式
}
tupleLockExtraInfo[MaxLockTupleMode + 1] =
{
{ /* LockTupleKeyShare */
AccessShareLock, // 1级表锁
MultiXactStatusForKeyShare,
-1 /* KeyShare does not allow updating tuples */
},
{ /* LockTupleShare */
RowShareLock, // 2级表锁
MultiXactStatusForShare,
-1 /* Share does not allow updating tuples */
},
{ /* LockTupleNoKeyExclusive */
ExclusiveLock, // 7级表锁
MultiXactStatusForNoKeyUpdate,
MultiXactStatusNoKeyUpdate
},
{ /* LockTupleExclusive */
AccessExclusiveLock, // 8级表锁
MultiXactStatusForUpdate,
MultiXactStatusUpdate
}
};
其实这些行锁模式的兼容性是怎么来的,就是根据对应的表锁兼容性来的。例如一个delete操作,它在表上加的是3级锁,但在行上加的是8级锁。因此在表上该操作是兼容的(可以同时对一个表进行delete),但在行上是冲突的(不可以同时delete同一行)。
二、 xmax的设置
通过前面的源码可以看到,tupleLockExtraInfo中除了保存常规锁对应的锁模式,还保存了大量MultiXact中的锁模式,MultiXact又是个什么呢?
1. MultiXactId
通常如果只有一个事务增加行锁,那么直接将行的xmax设为事务id,并在infomask中设置对应锁类型即可。
但select…for…语句中可以加行级共享锁,即可以有多个事务对一个元组加共享锁,这时就没法通过将行的xmax设为事务id来表示了。为此,pg将多个事务组成一个mXactCacheEnt(multixact.c文件),并为其指定唯一的MultiXactId,此时在xmax处保存的就是MultiXactId。
typedef struct mXactCacheEnt
{
MultiXactId multi;
int nmembers;
dlist_node node;
MultiXactMember members[FLEXIBLE_ARRAY_MEMBER];
} mXactCacheEnt;
为了区分xmax设置的是事务id还是MultiXactId,在使用MultiXactId时会在元组上增加HEAP_XMAX_IS_MULTI标记。
2. 模拟案例
会话1
create table t1(a int primary key,b int);
insert into t1 values(1,1);
select xmin,xmax,* from t1; -- 事务id 761
会话2
Begin;
select * from t1 for key share; -- 事务不提交
会话1
select xmin,xmax,* from t1; -- 当前xmax中保存的是事务id(762)
会话3
Begin;
select * from t1 for share; -- 事务不提交(事务id 763)
会话1
select xmin,xmax,* from t1; -- 当前xmax中保存的是MultiXactId
怎么知道这个是MultiXactId?可以通过控制文件查看
pg_controldata –D $PGDATA | grep Multi
3. 行锁标记位
- 如果元组的xmax是事务id,需要通过infomask标记位区分元组的加锁情况。
源代码在htup_details.h,其实有很多种状态,这里只简单列一些
#define HEAP_XMAX_KEYSHR_LOCK 0x0010 /* for key share子句对应的锁 */
#define HEAP_XMAX_EXCL_LOCK 0x0040 /* xmax is exclusive locker,排他锁标记位 */
/* xmax is a shared locker */
#define HEAP_XMAX_SHR_LOCK (HEAP_XMAX_EXCL_LOCK | HEAP_XMAX_KEYSHR_LOCK)
#define HEAP_XMAX_LOCK_ONLY 0x0080 /* xmax, if valid, is only a locker,显式加行锁 */
#define HEAP_XMAX_IS_MULTI 0x1000 /* t_xmax is a MultiXactId */
#define HEAP_KEYS_UPDATED 0x2000 /* tuple was updated and key cols */
详细可参考 https://mp.weixin.qq.com/s/nDBYbJpoBVqZxpYYaxPZ6Q
- 如果元组的xmax是MultiXactId,则每种子句都对应一种锁模式(它们的对应关系通过tupleLockExtraInfo也可以看出来)
/*
* Possible multixact lock modes ("status"). The first four modes are for
* tuple locks (FOR KEY SHARE, FOR SHARE, FOR NO KEY UPDATE, FOR UPDATE); the
* next two are used for update and delete modes.
*/
typedef enum
{
MultiXactStatusForKeyShare = 0x00,
MultiXactStatusForShare = 0x01,
MultiXactStatusForNoKeyUpdate = 0x02,
MultiXactStatusForUpdate = 0x03,
/* an update that doesn't touch "key" columns */
MultiXactStatusNoKeyUpdate = 0x04,
/* other updates, and delete */
MultiXactStatusUpdate = 0x05
} MultiXactStatus;
为了保存MultiXactId和事务的映射关系,pg使用两个SLRU进行分层映射,它们位于$PGDATA/pg_multixact目录下,分别是offsets目录和members目录。
参考
《PostgreSQL技术内幕:事务处理深度探索》第2章
https://www.modb.pro/db/70021