报错中的 ShareLock on transaction 14165 不是说会话 2 正在对 t1 的某一行加 ShareLock,而是说它正在对“事务 14165”这个锁对象请求 ShareLock,借此等待事务 14165 结束。
行冲突首先记录在 tuple header 的 xmax 中,真正睡眠等待时转换成 LOCKTAG_TRANSACTION + ShareLock,所以死锁报告显示的是 transaction lock。
涉及的锁和状态
| 层次 | 锁对象或状态 | 锁模式 | 物理位置 | 是否构成本次死锁 |
|---|---|---|---|---|
| 表级 | LOCKTAG_RELATION(t1) | RowExclusiveLock | fast-path 或主锁表 | 否,两个 RowExclusiveLock 相互兼容 |
| 索引级 | LOCKTAG_RELATION(t1_pkey) | 通常也是 RowExclusiveLock | fast-path 或主锁表 | 否 |
| 行级持有信息 | tuple header 的 xmax/infomask | 本例通常是 LockTupleNoKeyExclusive 语义 | 数据页中的 tuple header | 标识哪一个 XID 更新了该行 |
| tuple 等待仲裁 | LOCKTAG_TUPLE | 本例通常映射为 heavyweight ExclusiveLock | 主锁表 LOCK/PROCLOCK | 用于多个等待者排队,不是报错中的 ShareLock |
| 事务 ID 锁 | LOCKTAG_TRANSACTION(14165) | 拥有者持有 ExclusiveLock | 主锁表 LOCK/PROCLOCK | 是 |
| 事务 ID 锁 | LOCKTAG_TRANSACTION(14166) | 拥有者持有 ExclusiveLock | 主锁表 LOCK/PROCLOCK | 是 |
| 事务等待请求 | 对方的 LOCKTAG_TRANSACTION | 等待者请求 ShareLock | 主锁表等待队列 | 是,形成两条等待边 |
UPDATE 首先取得表级 RowExclusiveLock
执行器打开 UPDATE 的目标 relation 时使用:
resultRelation = heap_open(
resultRelationOid,
RowExclusiveLock
);
源码位于 src/gausskernel/runtime/executor/execMain.cpp。
两个会话都持有:
LOCKTAG_RELATION(t1) + RowExclusiveLock
但 RowExclusiveLock 与另一个 RowExclusiveLock 兼容,因此表锁不会造成这个死锁。
因为 RowExclusiveLock 满足 relation fast-path 资格,所以这两个表锁还可能保存在各自的:
PGPROC.fpRelId[]
PGPROC.fpLockBits[]
中,而不一定进入主锁表。
每个 UPDATE 都为自己的 XID 持有 ExclusiveLock
真正更新 tuple 时进入 src/gausskernel/storage/access/heap/heapam.cpp:
TransactionId xid = GetCurrentTransactionId();
如果当前事务还没有分配 XID,GetCurrentTransactionId() 会调用:
AssignTransactionId()
→ GetNewTransactionId()
→ XactLockTableInsert(xid)
XactLockTableInsert() 位于 src/gausskernel/storage/lmgr/lmgr.cpp:
void XactLockTableInsert(TransactionId xid)
{
LOCKTAG tag;
SET_LOCKTAG_TRANSACTION(tag, xid);
LockAcquire(
&tag,
ExclusiveLock,
false,
false
);
}
因此假设:
Session 1 的 XID = 14165
Session 2 的 XID = 14166
主锁表状态是:
Session 1:
LOCKTAG_TRANSACTION(14165)
ExclusiveLock
granted = true
Session 2:
LOCKTAG_TRANSACTION(14166)
ExclusiveLock
granted = true
LOCKTAG_TRANSACTION 不符合 relation fast-path 条件,因此这两个事务锁会直接进入:
LOCK hash
PROCLOCK hash
行锁主要记录在 tuple header,不为每一行长期保留 LOCK
第一次更新成功后,tuple header 大致变成:
id = 1 的旧版本:
xmax = 14165
id = 2 的旧版本:
xmax = 14166
所以:
Session 1 更新 id=1
→ id=1 的 tuple header 记录 XID 14165
Session 2 更新 id=2
→ id=2 的 tuple header 记录 XID 14166
openGauss 不会为每一个被更新的 tuple 长期在共享锁表中保留一个 LOCKTAG_TUPLE,否则大事务更新数百万行时会消耗大量共享锁表空间。
源码设计文档 src/gausskernel/storage/access/heap/README.tuplock 将其称为两级机制:
第一层:
tuple header 的 xmax + infomask
保存行锁拥有者和模式
第二层:
必须等待时,临时使用 LOCKTAG_TUPLE
负责多个等待者的排队和公平性
为什么还有临时 LOCKTAG_TUPLE
当 Session 1 尝试更新 id=2 时,heap_update() 看到:
id=2.xmax = 14166
事务 14166 仍在运行
在真正等待事务 14166 前,它先调用:
LOCK_TUPLE_TUP_LOCK(
relation,
&oldtup.t_self,
mode
);
源码位于 src/gausskernel/storage/access/heap/heapam.cpp。
本例只修改 value,不修改主键 id,在支持增强 tuple lock 的单机场景下通常选择:
mode = LockTupleNoKeyExclusive;
这个 tuple 语义模式映射到 heavyweight lock manager 的:
ExclusiveLock
映射表位于 src/include/access/heapam.h:
LockTupleKeyShare → AccessShareLock
LockTupleShared → RowShareLock
LockTupleNoKeyExclusive → ExclusiveLock
LockTupleExclusive → AccessExclusiveLock
LockTuple() 再构造:
LOCKTAG_TUPLE {
dbid,
relid,
bucketid,
block number,
offset number
}
并调用普通 LockAcquire(),源码位于 src/gausskernel/storage/lmgr/lmgr.cpp。
这个 tuple heavyweight lock 的作用是排队仲裁:
LockTuple()
→ XactLockTableWait()
→ 成为下一位后重新检查 tuple header
→ 完成更新
→ UnlockTuple()
它不是长期行锁的主要载体,也不是错误消息里的 ShareLock。
为什么等待事务时请求 ShareLock
拿到 tuple 排队位置后,Session 1 执行:
XactLockTableWait(xwait, true);
这里:
xwait = tuple header 中的 xmax
也就是 Session 2 的 XID 14166。
源码位于 src/gausskernel/storage/lmgr/lmgr.cpp:
SET_LOCKTAG_TRANSACTION(tag, xid);
LockAcquire(
&tag,
ShareLock,
false,
false
);
LockRelease(
&tag,
ShareLock,
false
);
之所以使用 ShareLock,是因为事务拥有者已经持有:
LOCKTAG_TRANSACTION(xid) + ExclusiveLock
而冲突矩阵中:
ShareLock 与 ExclusiveLock 冲突
因此:
事务仍运行:
ExclusiveLock 未释放
ShareLock 请求被阻塞
事务提交或回滚:
ExclusiveLock 被释放
ShareLock 请求被唤醒并获得
XactLockTableWait() 随即释放 ShareLock 并返回
使用 ShareLock 还有一个重要好处:多个等待同一个事务结束的后端之间可以兼容。
T1 持有 XID 100 的 ExclusiveLock
T2 请求 XID 100 的 ShareLock
T3 请求 XID 100 的 ShareLock
T4 请求 XID 100 的 ShareLock
T2、T3、T4 不应该互相阻塞,它们只需要共同等待 T1。因此使用:
一个 ExclusiveLock 拥有者
多个相互兼容的 ShareLock 等待者
正好表达“大家一起等待同一个事务结束”的语义。
这里的 ShareLock 与“共享读取这一行”没有关系。
本例的具体等待图
第三步中,Session 1 更新 id=2:
id=2.xmax = 14166
→ XactLockTableWait(14166)
→ 请求 ShareLock on transaction 14166
→ 被 Session 2 的 ExclusiveLock 阻塞
第四步中,Session 2 更新 id=1:
id=1.xmax = 14165
→ XactLockTableWait(14165)
→ 请求 ShareLock on transaction 14165
→ 被 Session 1 的 ExclusiveLock 阻塞
形成:
Session 1 / XID 14165
持有 ExclusiveLock on transaction 14165
等待 ShareLock on transaction 14166
│
▼
Session 2 / XID 14166
持有 ExclusiveLock on transaction 14166
等待 ShareLock on transaction 14165
│
└────────回到 Session 1
即:
Session 1 → Session 2 → Session 1
这是只有 hard edge 的硬死锁,不能通过调整等待队列顺序解决。
ShareLock 请求如何进入主锁表等待队列
XactLockTableWait() 调用普通:
LockAcquire()
→ LockAcquireExtended()
→ LockAcquireExtendedXC()
由于 LOCKTAG_TRANSACTION 不符合 relation fast-path,它进入主锁表:
LockHashPartitionLock(hashcode)
→ SetupLockInTable()
→ LOCK hash
→ PROCLOCK hash
→ LockCheckConflicts()
→ 发现对方 ExclusiveLock
→ WaitOnLock()
→ ProcSleep()
→ 插入 LOCK.waitProcs
此时 PGPROC 中会记录:
waitLock → transaction 14165 或 14166 的 LOCK
waitProcLock → 当前等待者对应的 PROCLOCK
waitLockMode = ShareLock
waitStatus = STATUS_WAITING
所以死锁检测器看到的图是事务 ID 锁图,而不是直接扫描 tuple header。
死锁检测源码位置
等待超过 deadlock_timeout 后,ProcSleep() 的定时器触发 CheckDeadLock()。
src/gausskernel/storage/lmgr/proc.cpp 先按顺序获取全部锁表分区 LWLock,然后调用:
t_thrd.storage_cxt.deadlock_state =
DeadLockCheck(t_thrd.proc);
调用点位于 src/gausskernel/storage/lmgr/proc.cpp。
真正的等待图搜索入口是:
src/gausskernel/storage/lmgr/deadlock.cpp:193
核心过程是:
DeadLockCheck()
→ DeadLockCheckRecurse()
→ TestConfiguration()
→ FindLockCycle()
→ FindLockCycleRecurse()
→ 沿 PROCLOCK.holdMask 和 LOCK.waitProcs 构造等待边
→ 再次回到起点,识别死锁环
本例两条边都来自已经授予的事务 ExclusiveLock,属于 hard edge,因此返回:
DS_HARD_DEADLOCK
随后 CheckDeadLock():
RemoveFromWaitQueue()
→ 当前检测进程 waitStatus = STATUS_ERROR
→ 唤醒该进程的 semaphore
WaitOnLock() 发现 ProcSleep() 返回失败后,释放锁表分区 LWLock并调用:
DeadLockReport();
调用位置:
src/gausskernel/storage/lmgr/lock.cpp:1757
错误文本在哪里生成
最终错误在 src/gausskernel/storage/lmgr/deadlock.cpp 中生成。
DETAIL 行来自:
appendStringInfo(
&clientbuf,
"Process %lu waits for %s on %s; "
"blocked by process %lu.",
...
);
源码位于 src/gausskernel/storage/lmgr/deadlock.cpp。
最终报错是:
ereport(
ERROR,
(
errcode(ERRCODE_T_R_DEADLOCK_DETECTED),
errmsg("deadlock detected"),
errdetail_internal("%s", clientbuf.data),
errhint("See server log for query details.")
)
);
源码位于 src/gausskernel/storage/lmgr/deadlock.cpp。
transaction 14165 这样的对象描述则由:
DescribeLockTag()
处理 LOCKTAG_TRANSACTION 时生成。
关于“会话 2 回滚”的准确说法
本次运行中,会话 2 的检测器识别出硬死锁,所以会话 2 的当前事务被选为受害者。具体哪个会话成为受害者与检测时序有关,并不保证永远是最后执行 UPDATE 的会话。
在显式:
BEGIN;
事务块中,死锁 ERROR 会使当前事务进入 aborted 状态并释放事务资源,使另一个会话能够继续。客户端仍通常需要执行:
ROLLBACK;
才能退出失败的事务块。
最终可以把整个例子压缩为:
UPDATE t1
→ 对 t1 持有 RowExclusiveLock
→ 对自己的 XID 持有 ExclusiveLock
→ 把自己的 XID 写入目标 tuple 的 xmax
遇到别人正在更新的 tuple
→ 从 tuple.xmax 得到对方 XID
→ LockTuple() 做等待者仲裁
→ 对对方 XID 请求 ShareLock
→ ShareLock 与对方的 ExclusiveLock 冲突
→ 进入事务锁等待队列
两个会话互相等待对方 XID
→ XID 14165 → XID 14166
→ XID 14166 → XID 14165
→ DeadLockCheck() 发现环
→ DeadLockReport() 报 deadlock detected