当前位置: 首页 > postgresql, 死锁 > 正文

在工业级数据库内核的真实 C 语言源码(src/backend/storage/lmgr/lock.cREADME)中,releaseMask 的真正物理使命, 是为了在事务提交(COMMIT)的瞬间,利用“无锁化(Lock-Free)”的思想,解决 10000 把锁同时释放时引发的 LWLock(轻量级闩锁)雪崩争用灾难。

我们彻底抛弃表象,直接下潜到多核 CPU 的高速缓存(L1/L2 Cache)和轻量级锁的分区机制中,把这段由 PostgreSQL 核心架构师极其天才的设计,按时间轴(T1 到 T4)一行行慢动作推导出来。


第 1 帧:物理绝境(COMMIT 瞬间的单点风暴)

【内存状态与机制背景】

假设进程 A 执行了一个超级大事务,它在执行期间,一共锁定了全库 10000 张不同的表

在共享内存中,锁管理器的哈希表并不是一把大锁,而是被切分成了 16 个 LWLock 分区(Partition LWLocks),用来分散 CPU 争用。这 10000 把锁,均匀散落在跨越这 16 个分区的内存空间里。

【如果不引入 releaseMask 的灾难推导】

现在,进程 A 触发了 COMMIT(执行 LockReleaseAll 函数)。

如果按照最原始的线性逻辑,进程 A 必须逐一释放这 10000 把锁。对于每一把锁:

  1. LWLockAcquire():去操作系统层抢夺这把锁所在的分区 LWLock
  2. 扣减 PROCLOCK->holdMaskLOCK->grantMask
  3. LWLockRelease():释放分区 LWLock。

物理灾难爆发:进程 A 为了释放这 10000 把锁,必须在 CPU 层面疯狂申请和释放分区 LWLock 整整 10000 次! 而此时,全库可能有上百个其他业务进程也在并发抢这些分区的 LWLock 准备建新锁。这种高频的“抢占-释放-抢占”,会引发极其恐怖的 CPU 缓存行失效(Cache Line Bouncing)和自旋锁风暴,系统吞吐量在 COMMIT 的那一秒瞬间跌至 0。


第 2 帧:架构师的突围(批量降维与 $O(1)$ 锁争用)

【物理降维思想】

既然 10000 把锁分散在 16 个分区里,为什么我不按分区进行批量处理

比如,有 600 把锁属于第 1 分区。我只需要抢下第 1 分区的 LWLock 仅仅 1 次,然后在持有锁的安全期内,一口气把这 600 把锁的状态全部扣减完,再释放 LWLock 即可。

如此一来,10000 次的底层锁争用,直接被降维暴击成了极其可控的 16 次

【致命的矛盾(悖论诞生)】

按分区批量处理,听起来完美,但在 C 语言内存操作中遇到了致命的悖论:

进程 A 必须先在内存里遍历这 10000 个 PROCLOCK,标记出“哪些锁准备被释放”(因为有的长事务可能只释放一部分子事务的锁)。

但是,PROCLOCK 位于全局共享内存。按照 C 语言多线程并发铁律:任何进程想要修改共享内存里的任何字段(哪怕是打个标记),都必须先抢下对应的 LWLock!

如果为了“打标记”去抢 LWLock,那就又回到了 10000 次抢锁的风暴中,批量处理成了纸上谈兵。


第 3 帧:绝对天才的后门(推导 releaseMask 的物理写入规则)

为了打破这个物理悖论,架构师在 PROCLOCK 结构体中强行插入了 releaseMask 字段,并在内核源码(lmgr/README)中为其定下了一条极其冷酷、打破常规的**“免检铁律”**:

“Note that it is modified without taking the partition LWLock, and therefore it is unsafe for any backend except the one owning the PROCLOCK to examine/change it.”

(注意:修改该字段绝对不需要获取分区 LWLock!因此,除了拥有这个 PROCLOCK 的进程自己,全库任何其他进程绝对禁止读取或修改它!)

【无锁化(Lock-Free)打标推演】

  1. 进程 A 开启批量释放流程。
  2. 进程 A 在完全不获取任何 LWLock 的裸奔状态下,直接顺着自己本地的私有锁表,遍历那 10000 个共享内存里的 PROCLOCK
  3. 进程 A 直接将需要释放的锁掩码,粗暴地写入 PROCLOCK->releaseMask(比如写入 00000100)。
  4. 物理安全性证明:为什么敢这么写?因为这条铁律保证了,在浩瀚的共享内存中,只有进程 A 的 CPU 核心会去触碰自己名下这 10000 个 PROCLOCKreleaseMask 字段。没有物理竞态条件,就不需要轻量级锁!打标的时间开销被压缩到极限的纯内存写入速度。

第 4 帧:批量收网与清算(执行 LockReleaseAll

打标(Lock-Free 阶段)瞬间完成后,进入真正的清算阶段。

  1. 按分区集结:进程 A 在本地内存中,把这 10000 个打好标的 PROCLOCK,按照它们的 LOCK 所在的 16 个分区,分别排好队。
  2. 重拳出击(极其克制的加锁):进程 A 走向第 1 分区。仅调用 1 次 LWLockAcquire(partition 1)
  3. 安全核销:在持有轻量级锁的绝对安全期内,进程 A 遍历属于第 1 分区的那 600 个 PROCLOCK
    • CPU 读取 releaseMask。如果发现 releaseMask != 0,说明这是刚才自己打过标的。
    • CPU 安全地将 releaseMask 的份额,从 holdMask 和全局的 LOCK->grantMask 中做减法扣除。
    • 执行唤醒判定(如我在上一轮讲的精准唤醒),然后清空 releaseMask = 0
  4. 全身而退:处理完这 600 个,进程 A 调用 1 次 LWLockRelease(partition 1)
  5. 继续对剩下的 15 个分区重复此动作。

架构级物理闭环

现在,releaseMask 的终极物理真相彻底浮出水面:

它绝对不是简单的“状态记录”,它是 PostgreSQL 内核为了实现 “锁的批量释放(Batched Lock Release)” 而专门开辟的 “无锁化工作区(Lock-Free Workspace)”

它利用“单写者(Single-Writer)”的内存物理隔离特性,成功避开了修改共享内存必须加轻量级锁的计算机底线。让事务提交时海量锁的释放开销,从 $O(N)$(锁的数量)直接被强行碾压成了 $O(K)$(分区的数量,常数级 16)。

这就是为什么 PostgreSQL 能够在每秒几万次的高并发事务提交时,依然保持多核 CPU 指令流水线稳定且不发生自旋锁雪崩的终极秘密。


了解 www.876873.xyz 的更多信息

订阅后即可通过电子邮件收到最新文章。

proclock结构体中 releaseMask 字段的作用,逻辑推演:等您坐沙发呢!

发表评论