暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

PolarDB PostgreSQL 版行锁与 pg_multixact

原创 内核开发者 2025-01-21
310

关于 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为什么一定要写日志?
「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论