关于 PolarDB PostgreSQL 版
PolarDB PostgreSQL 版是一款阿里云自主研发的云原生关系型数据库产品,100% 兼容 PostgreSQL,高度兼容Oracle语法;采用基于 Shared-Storage 的存储计算分离架构,具有极致弹性、毫秒级延迟、HTAP 、Ganos全空间数据处理能力和高可靠、高可用、弹性扩展等企业级数据库特性。同时,PolarDB PostgreSQL 版具有大规模并行计算能力,可以应对 OLTP 与 OLAP 混合负载。
前置问题
- PG有哪几种行锁,分别用于哪几种场景?锁定语句是什么?
- 当加入行锁时,是否是对每一行都在内存中加了一把实体锁?
- 如果不是,那要如何检测行与行之间的冲突?冲突实体是谁?
- PG行锁的实现为什么要有multixact日志?
- 行锁是在何时,以何种形式被释放的?
- 常见一种行锁的死锁构成
行锁的种类
行锁,顾名思义,即对tuple粒度的数据库对象形成锁定。目前PG支持以下四种行锁,以及其分别使用的场景
种类 | 使用场景 | 锁定语句 |
LockTupleKeyShare | 插入外键表时,提前锁定主表的主键 | SELECT ... FOR KEY SHARE |
LockTupleShare | 读取后禁止更新 | SELECT ... FOR SHARE |
LockTupleNoKeyExclusive | 非PK、UK列更新的行锁定 | SELECT ... FOR NO KEY UPDATE OR UPDATE SET(非PK列、UK列) |
LockTupleExclusive | 涉及到PK、UK更新、或Delete锁定 | SELECT ... FOR UPDATE OR UPDATE SET(PK、UK列) |
即,可以通过SELECT ... FOR提前形成锁定,或者在UPDATE时形成锁定。
冲突矩阵如下:
对于FOR KEY SHARE场景,以下面的例子进行举例:
create table testc(i int primary key, j int);
insert into testc values(1, 1);
-- session1
select j from testc for key share;
-- session2
update testc set j = 2; -- ok
update testc set i = 2; -- locked
此处特意强调的是外键使用场景:即如果我们在准备插入外键表时,如果不希望主表的主键列发生变化,则可以用FOR KEY SHARE提前锁定主表的主键列不要变化。
需要特别提醒的是,如果只有SELECT,则不会形成任何锁定,即无论对应Tuple如何变化,都不会形成冲突。
锁定形成
PostgreSQL不会在内存里保存任何关于已修改行的信息。 不过,锁住一行会导致一次磁盘写,例如, SELECT FOR UPDATE将修改选中的行以标记它们被锁住,并且因此会导致磁盘写入。 --Doc PG13
修改任何行的信息均不会在内存中进行保留,即形成行锁时不会在内存中对每一行加入一把实体锁,那具体一行的锁定又是如何引入的呢?
先看代码路径:
compute_new_xmax_infomask(xmax, old_infomask, tuple->t_data->t_infomask2
heap_lock_tuple()
{
LockBuffer(*buffer, BUFFER_LOCK_EXCLUSIVE);
result = HeapTupleSatisfiesUpdate(tuple, cid, *buffer);
xmax = HeapTupleHeaderGetRawXmax(tuple->t_data);
old_infomask = tuple->t_data->t_infomask;
compute_new_xmax_infomask(xmax, old_infomask, tuple->t_data->t_infomask2,
GetCurrentTransactionId(), mode, false,
&xid, &new_infomask, &new_infomask2);
tuple->t_data->t_infomask |= new_infomask;
tuple->t_data->t_infomask2 |= new_infomask2;
if (HEAP_XMAX_IS_LOCKED_ONLY(new_infomask))
HeapTupleHeaderClearHotUpdated(tuple->t_data);
HeapTupleHeaderSetXmax(tuple->t_data, xid);
MarkBufferDirty(*buffer);
LockBuffer(*buffer, BUFFER_LOCK_UNLOCK);
}
基于heap_lock_tuple的核心路径,不难发现,locktuple的过程实际上是重新设置了一把tuple的xmax和infomask。
其中,compute_new_xmax_infomask确定了lock tuple后新的xmax和infomask计算。且看一种最简单的情况:HEAP_XMAX_INVALID(insert语句产生的tuple)
compute_new_xmax_infomask()
{
if (old_infomask & HEAP_XMAX_INVALID)
{
new_infomask |= HEAP_XMAX_LOCK_ONLY;
switch (mode)
{
case LockTupleKeyShare:
new_xmax = add_to_xmax;
new_infomask |= HEAP_XMAX_KEYSHR_LOCK;
break;
case LockTupleShare:
new_xmax = add_to_xmax;
new_infomask |= HEAP_XMAX_SHR_LOCK;
break;
case LockTupleNoKeyExclusive:
new_xmax = add_to_xmax;
new_infomask |= HEAP_XMAX_EXCL_LOCK;
break;
case LockTupleExclusive:
new_xmax = add_to_xmax;
new_infomask |= HEAP_XMAX_EXCL_LOCK;
new_infomask2 |= HEAP_KEYS_UPDATED;
break;
default:
new_xmax = InvalidTransactionId;/* silence compiler */
elog(ERROR, "invalid lock mode");
}
}
即发生锁定时,会实际修改tuple上的xmax和infomask。
冲突检测
如上所述,当发生锁定时,是通过修改tuple的xmax和infomask形成锁定。那冲突是如何检测出来的,又是如何wait lock的。
当另一会话取得Buffer锁定,并获取到相同tuple后,会首先检测上面的xmax字段,从而获取tuple当前的处理状态:
HeapTupleSatisfiesUpdate()
{
if (TransactionIdIsInProgress(HeapTupleHeaderGetRawXmax(tuple)))
return TM_BeingModified;
}
由于我们更新了xmax,且事务尚未结束,故另一会话会判定该tuple为TM_BeingModified状态,随后就会去检测Tuple的infomask。
heap_lock_tuple()
{
if (result == TM_BeingModified ||
result == TM_Updated ||
result == TM_Deleted)
{
TransactionId xwait;
uint16infomask;
uint16infomask2;
boolrequire_sleep;
ItemPointerData t_ctid;
/* must copy state data before unlocking buffer */
xwait = HeapTupleHeaderGetRawXmax(tuple->t_data);
infomask = tuple->t_data->t_infomask;
infomask2 = tuple->t_data->t_infomask2;
ItemPointerCopy(&tuple->t_data->t_ctid, &t_ctid);
LockBuffer(*buffer, BUFFER_LOCK_UNLOCK);
}
if (mode == LockTupleKeyShare)
else if (mode == LockTupleShare)
else if (mode == LockTupleNoKeyExclusive)
else
...
}
随后,基于infomask,判定出行锁类型,以及自身需要锁类型,进而根据冲突矩阵选择是否需要wait lock。
执行wait lock时,会通过XactLockTableWait执行等待。
void
XactLockTableWait(TransactionId xid, Relation rel, ItemPointer ctid,
XLTW_Oper oper, int polar_wait_timeout)
{
for (;;)
{
Assert(TransactionIdIsValid(xid));
Assert(!TransactionIdEquals(xid, GetTopTransactionIdIfAny()));
SET_LOCKTAG_TRANSACTION(tag, xid);
(void) polar_lock_acquire(&tag, ShareLock, false, false, true, NULL, polar_wait_timeout);
LockRelease(&tag, ShareLock, false);
if (!TransactionIdIsInProgress(xid))
break;
}
}
可见,XactLockTableWait最终等待的是xmax这个事务id,本质上即是等待加锁的事务结束,从而完成锁冲突和wait lock。
当事务结束后,即便tuple上依然保留xmax,但xmax对应事务不在活跃,因此不会再继续执行wait lock。
多事务加行锁--multixact
依然看锁冲突矩阵:
可见,锁冲突矩阵上允许存在多个锁互不冲突的情况,即允许多个事务对同一tuple加入行锁。但是,tuple上不可能记录当前所有事务的所有锁类型,因此必要有相关的机制来去记录这项信息,即multixact。
compute_new_xmax_infomask()
{
if (TransactionIdIsInProgress(xmax))
{
/* otherwise, just fall back to creating a new multixact */
new_status = get_mxact_status_for_lock(mode, is_update);
new_xmax = MultiXactIdCreate(xmax, old_status,
add_to_xmax, new_status);
GetMultiXactIdHintBits(new_xmax, &new_infomask, &new_infomask2);
}
}
当compute_new_xmax_infomask判定对应tuple的xmax还活跃时,说明存在并发加锁的场景,此时需要产生multixact日志。
基于multixact加入的行锁,其xmax与infomask有所变化:
区分项 | 单事务行锁 | 多事务行锁 |
xmax | 事务id | multi xact id |
infomask | HEAP_XMAX_KEYSHR_LOCK HEAP_XMAX_SHR_LOCK HEAP_XMAX_EXCL_LOCK HEAP_KEYS_UPDATED | HEAP_XMAX_IS_MULTI |
multi xact id实际上对应的是一个transaction id的集合,及其加锁状态集合。例如:
multi xact id | 事务集合 | 状态集合 |
2 | 108 | KEY SHARE |
109 | NO KEY UPDATE | |
3 | 1110 | SHARE |
118 | KEY SHARE |
PG通过上述方式,将行锁信息转移到了multi xact日志中,从而实现多事务行锁。
行锁释放
如之前,“冲突检测”部分提到,行锁冲突的实体本质上是事务id锁冲突,因此行锁的释放实际上是等待前一事务结束时,事务id放锁。
常见行锁死锁构成
最常见的一种行锁死锁,是下面的这种:
create table test(i int);
insert into test values(1);
insert into test values(2);
-- session 1 已开事务,
select i from test where i = 1 for share; -- ok
-- session 2 已开事务
select i from test where i = 2 for no key update; -- ok
-- session 1
select i from test where i = 2 for share; -- wait lock
-- session 2
select i from test where i = 1 for no key update; -- dead lock
DETAIL: Process 123195 waits for ShareLock on transaction 878; blocked by process 123186.
Process 123186 waits for ShareLock on transaction 879; blocked by process 123195.
Process 123195: select i from test where i = 1 for no key update;
Process 123186: select i from test where i = 2 for share;
本质上死锁实体为两个事务锁的死锁。发生在两个事务均希望持有对方的事务锁。
后续问题
- RO是否可以加入for key share或者for share lock?如果不可以,是为什么?
- 行锁是否有加锁上限?
- 一行上的锁冲突是否有上限?如果有,冲突上限是多少?
- PG的行锁实现最大的优势是什么?最明显的劣势是什么?你还知道哪些DB的行锁的实现?
- heap tuple lock为什么一定要写日志?




