其他锁
上一篇已经介绍了常规锁,这篇文章继续补充 PG 中其他几类常见锁。
spinlock
spinlock 即自旋锁,PG 内部直接通过 CPU 的 TAS/CAS 指令实现。
因此它只有互斥这一种模式。典型用法如下,用来原子性地修改多个变量:
SpinLockAcquire(&CheckpointerShmem->ckpt_lck);
CheckpointerShmem->ckpt_failed++;
CheckpointerShmem->ckpt_done = CheckpointerShmem->ckpt_started;
SpinLockRelease(&CheckpointerShmem->ckpt_lck);
如果只是修改单个变量,通常直接使用 PG 内部的原子操作即可(src/backend/port/atomics.c)。我之前也写过一篇介绍:GP 代码中的原子操作。其中一部分原子操作在底层也是借助 spinlock 实现的。
lwlock
LWLock 即 lightweight lock,作用类似编程语言里的 mutex。它对用户不可见,也不像常规锁那样自带死锁检测和事务结束自动释放机制,开发者必须在代码中手动获取和释放 LWLock,并自行保证正确性。
代码中的实现结构是:
typedef struct LWLock
{
uint16 tranche; /* tranche ID*/ //->锁类别
pg_atomic_uint32 state; /* state of exclusive/nonexclusive lockers */
proclist_head waiters; /* list of waiting PGPROCs */ //->也有等待队列
} LWLock;
在 src/include/storage/lwlocklist.h 中可以看到一系列预定义的 LWLock。
其内部实现细节仍然推荐参考树杰《事务处理》一书中的锁章节;而一个很典型的使用场景,则是buffer pool 中的并发控制。
row lock
为了实现更细粒度的并发控制,必须引入行锁。它是 PG 中非常重要的一类锁,其实现依赖 tuple header 里的 xmax + infomask 标记。PG 在行锁上定义了 4 种模式,本质上是在共享 / 互斥的基础上,再区分是否涉及 key update。常见使用场景如下:
begin; select ... FOR [KEY] SHARE; // 直接申请 FOR 后写明的模式
begin; select ... FOR [NOKEY] UPDATE; // 同上
update ... ; // 通常是 FOR NO KEY UPDATE;若修改了可被外键引用的唯一索引键列,则会提升到 FOR UPDATE
delete ... ; // FOR UPDATE
insert ... ; // 一般不需要显式行锁,主要依赖 MVCC
select ... ; // 普通 SELECT 不加行锁,主要依赖 MVCC
行锁的加锁过程并不只是“给 tuple 打一个标记”,而是还需要 tuple lock 的配合。简化后的过程大致如下:
- 如果
xmax和 hint bits 表明该行当前正被排他方式锁住,先获取一个重量级的 tuple lock。 - 如有需要,再去等待当前持锁事务释放冲突锁;如果
xmax中存的是 multixact,也可能需要等待多个事务。 - 把自己的事务 ID 写入 tuple header 中的
xmax,并设置对应的标记位。 - 如果第 1 步获取了 tuple lock,这里再把它释放掉。
tuple lock 本质上是一种常规锁。引入它的重要目的之一,是识别和保护“第一个等待者”,避免长时间饥饿。
更简洁的伪代码表示:
LockTuple() // acquire tuple lock
XactLockTableWait() // wait on another txn (if required)
mark tuple // set xmax + infomask => locked by me
UnlockTuple() // release tuple lock
另外,PG 中很多死锁和性能问题都和行锁有关;实现细节可以结合 PG14 book 中关于行锁的章节一起看。
others
除此之外,还有 object lock、advisory lock、page lock 等更细分的类型。PG14 book 里专门有一章介绍它们,可以按需查阅。
Questions:
- 对于
PageLock(见LockPage()函数),直观上似乎每次访问 page / buffer 都该用它保护,形成“表锁 -> page 锁 -> 行锁”的层次;但实际真是这样吗?实际并不是。就 PostgreSQL 主线代码里常见的使用场景来看,
PageLock主要出现在 GIN 这类少数路径中;而 buffer pool 则是通过 LWLock 来保护的。