在获取 wal buffer中lsn的时候,申请什么 锁?? 这个锁加到什么对象上面?

🗡️ 核心真相:获取 LSN 到底申请了什么锁?

checkpointer 在 $a$ 时刻去获取起跑线 LSN(也就是 REDO_LSN)时,它申请的根本不是一个 Spinlock(自旋锁),而是一组极其特殊的 LWLock(轻量级锁)!

在 C 源码中,这个动作调用的底层函数叫做:

WALInsertLockAcquireExclusive() (全局独占 WAL 插入锁)

  • 宏观错觉:很多人以为获取一个数字(LSN)只要自旋锁看一眼就行了。
  • 物理真相:如果只是看一眼 LSN,确实可以用自旋锁。但 checkpointer 不仅要看 LSN,它还必须保证**“在划定起跑线的这一微秒内,绝对没有任何前台业务正在往 WAL Buffer 的内存空间里拷贝那一半的数据!”**(防止切出一个断裂的残缺日志)。
  • 因此,它必须用 LWLock 的排他模式(Exclusive),把所有正在写日志的动作瞬间“冻结”。

🎯 致命爆破:这个锁加到了“什么对象”上面?

这是区分普通 DBA 和原厂内核开发者的绝对分水岭!

  • 石器时代(PG 9.4 之前):这个锁加在一个全局唯一的 WALInsertLock 对象上。这就导致几万个并发都要抢这一把锁,多核 CPU 互相踩踏,性能极差。
  • 现代架构(PG 9.4+ 的极致微操):在现代的 PostgreSQL 共享内存中,这个锁被加到了一个名叫 WALInsertLocks 的数组对象上!

为了压榨 CPU 性能,内核开发者做了极其疯狂的微操设计:

  1. 阵列化(Array):系统默认会创建 8 把独立的 WAL 插入锁(宏定义 NUM_XLOGINSERT_LOCKS = 8)。前台业务写日志时,只需要随机挑这 8 把锁里的 1 把去锁定即可,冲突概率瞬间降低 8 倍!
  2. checkpointer 的霸道包场:当 checkpointer 去拿起跑线 LSN 时,它会写一个 for 循环,瞬间把这 8 把锁全部用排他模式(Exclusive)死死锁住! 只有 8 把锁全部拿下,它才敢去读取那个 LSN 坐标。
  3. 防伪共享(CPU Cache Line Padding):这是内核源码里最变态的细节。这 8 把锁在内存中不是紧挨着的。它们被定义为 WALInsertLockPadded 结构体,强行塞入了空白字节,使得每一把锁的长度刚好等于 CPU 的 L1/L2 缓存行大小(通常是 64 或 128 字节)
    • 为什么? 因为如果两把锁在同一个缓存行里,CPU 1 修改锁 A,会导致 CPU 2 里的锁 B 缓存立刻失效(这叫伪共享 False Sharing),引发严重的底层总线风暴。PG 通过强制物理空间隔离,完美避开了 CPU 底层的硬件瓶颈!

⏱️ 锁了 8 把轻量级锁,为什么还没卡死业务?

指挥官,你可能会问:既然 checkpointer 这么霸道,把 8 把轻量级写锁全锁死了,那 5 万 TPS 的前台业务岂不是瞬间卡顿了?

这就回到了我上一次推演强调的**“持有时间极短”**:

  1. checkpointer 锁死这 8 把锁。
  2. 它仅仅是去读一个内存变量 Insert->CurrBytePos,然后做几步简单的加减乘除,计算出确切的 LSN。
  3. 它绝对不进行任何磁盘 I/O!
  4. 算完之后,立刻释放这 8 把锁。

整个过程依然只消耗几个微秒(Microseconds)。因为没有慢速的 I/O 牵绊,前台业务在这个微秒级的冻结中,仅仅是在 CPU 的等待队列里稍微排了一下队,宏观上根本感知不到任何延迟!




在扫描内存中的 脏页的时候,申请什么 锁?? 这个锁加到什么对象上面?

🎯 申请什么锁?(The Lock)

在扫描内存找脏页的那几毫秒里,checkpointer 申请的是**“缓冲头自旋锁(Buffer Header Spinlock)”**!

  • 源码级代号:在 C 语言源码中,获取这个锁的宏叫 LockBufHdr()
  • 现代 PG 的极致变态进化(PG 9.6+):如果你去面试顶级内核研发,必须抛出这个大招!在早期的 PG 中,这确实是一个传统的 Spinlock。但在现代 PG 中,为了压榨多核 CPU 极限,内核开发者把这个锁**“原子化”**了!
    • 它不再是一个独立的锁对象,而是变成了 state(一个 32 位的无符号整数)里面的一个二进制位(Bit Flag)——BM_LOCKED
    • 申请锁的动作,实际上是 CPU 发起的一条底层的 CAS(Compare-And-Swap,比较并交换)原子指令,强行把这个 BM_LOCKED 位设为 1。这比传统的自旋锁还要轻、还要快!耗时仅在几纳秒级别。

🎯 这个锁加到了“什么对象”上面?(The Object)

绝对、百分之百地加在了单个【BufferDesc(缓冲区块描述符)】对象上!

为了让你彻底看清它的物理隔离边界,请死死记住以下“三不原则”:

  1. 绝不加在整个内存上:它没有锁整个 shared_buffers 数组。如果有 1600 万个缓冲页,内存里就有 1600 万个 BufferDesc 对象。它每次只锁当前正在看的那 1 个
  2. 绝不加在真实的数据页上:它没有去锁那张 8192 字节(8KB)的真实数据纸张!
  3. 仅仅锁住“花名册的一行”BufferDesc 就是我们之前比喻的“花名册”。它只有几十个字节大小,里面只存了元数据(比如:页码是几、有没有 BM_DIRTY 脏标记、有没有人正在用)。

⏱️ 慢动作物理缝合:这几毫秒到底发生了什么?

让我们把 checkpointer 的这几毫秒动作,和前台业务的并发修改,在一个微观的沙盘里进行终极对撞:

  • 第 1 纳秒checkpointer 走到 1 号 BufferDesc 前,执行 LockBufHdr()(CPU CAS 原子指令,将 BM_LOCKED 设为 1)。1 号花名册被锁死。
  • 第 2 纳秒:它看了一眼状态:发现有 BM_DIRTY(脏页)标记。
  • 第 3 纳秒:前台业务恰好想来读取或修改 1 号数据页。前台业务必须先看 1 号 BufferDesc,结果发现 BM_LOCKED 是 1。前台业务只能在 CPU 上稍微空转自旋(Spin)一下。
  • 第 4 纳秒checkpointer 利用按位或(Bitwise OR)指令,给 1 号描述符追加打上 BM_CHECKPOINT_NEEDED 标签。
  • 第 5 纳秒checkpointer 执行 UnlockBufHdr(),清除 BM_LOCKED 位。锁释放!
  • 第 6 纳秒:刚才被稍微挡住了一瞬间的前台业务,立刻拿到了 1 号 BufferDesc,畅通无阻地去修改那 8KB 的真实数据了。
  • 与此同时:在 checkpointer 锁住 1 号描述符的这 5 纳秒里,其他的几万个前台业务正在疯狂修改 2 号、3 号、100 万号描述符。因为锁是加在单个 BufferDesc 对象上的,它们之间毫无物理冲突!



内存中的 脏页被检查点进程刷盘的时候,申请什么 锁?? 这个锁加到什么对象上面?

🎯 申请什么锁?(The Lock)

在真正执行刷盘操作(把 8KB 内存数据写入硬盘)时,checkpointer 申请的是 轻量级锁(LWLock),而在 C 源码中它有一个专属的名字:Buffer Content Lock(缓冲区块内容锁)

但这里隐藏着 PostgreSQL 乃至整个关系型数据库设计中最伟大、最不可思议的一个微操: checkpointer 在申请这把锁时,使用的是【Shared Mode(共享模式 / 读锁)】,绝对不是排他锁(Exclusive Lock)!

🎯 这个锁加到了“什么对象”上面?(The Object)

绝对、百分之百地加在了【当前正在刷盘的这 1 个、且仅仅是这 1 个 8KB 数据页的内容】上!

为了让你彻底看清它的物理隔离边界,请死死记住以下“三不原则”:

  1. 绝不加在整个内存上:它没有锁整个 shared_buffers。如果有 1600 万个缓冲页,系统里就有 1600 万把对应的 Buffer Content Lock。它每次只锁当前正在写的这 1 页
  2. 绝不加在整张表上:哪怕你这张表有 100 个 G,它也只锁当前被刷下去的这 8KB 纸张,表的其他部分畅通无阻。
  3. 区别于刚才的“花名册锁”:刚才打标签用的是 BufferHeaderLock(自旋锁,锁元数据);现在刷盘用的是 Buffer Content Lock(轻量级锁,锁真实的 8KB 数据)。

⏱️ 慢动作物理对撞:为什么必须是“单页 + 共享锁”?

让我们把 checkpointer 刷盘的这几毫秒,和前台 5 万并发业务,在一个 8KB 的沙盘里进行终极对撞。你就会明白原厂架构师是何等的天才:

  • 第 1 毫秒(钉死目标)checkpointer 准备刷 1 号数据页。它先对这页做一个 Pin(钉住)操作,告诉内存管理器:“我要用这页,不准把它踢出内存。”
  • 第 2 毫秒(上共享锁)checkpointer 申请 1 号页的 Buffer Content Lock (Shared)
    • 为什么是共享读锁?因为 checkpointer 只需要把内存里的 8KB “读”出来,然后交给操作系统的 write() 函数。它自己并不修改这 8KB。
  • 第 3 毫秒(极致并发的奇迹发生)
    • 🟢 前台业务 A 想“查询(SELECT)”这 8KB 数据:完全可以!因为业务 A 申请的也是“共享读锁”。读锁与读锁完美兼容。哪怕底层硬盘正在嘎吱嘎吱地转,前台的查询业务根本不知道发生了什么,丝滑般读走数据!
    • 🔴 前台业务 B 想“修改(UPDATE)”这 8KB 数据:不行!修改必须申请“排他写锁(Exclusive)”。此时业务 B 发现 checkpointer 正在拿着共享锁刷盘,业务 B 就会非常懂事地进入操作系统休眠队列(Sleep),安静地等这几毫秒。
    • 为什么要让业务 B 休眠?因为如果 checkpointer 正在把 8KB 往硬盘上写(写了一半),业务 B 突然改了内存里的后半截,写到硬盘上的数据就会出现致命的“页撕裂(Torn Page)”和 Checksum 校验和错误!必须挂起写业务。
  • 第 8 毫秒(释放与唤醒):8KB 数据成功落入磁盘。checkpointer 瞬间释放共享锁。在休眠队列里等了 6 毫秒的业务 B 瞬间被 CPU 唤醒,拿到排他锁,继续修改这 8KB。
  • 与此同时:在这 8 毫秒里,其他的几万个前台业务正在疯狂修改 2 号、3 号、100 万号数据页。因为锁是加在单个 8KB 对象上的,它们之间毫无物理冲突!

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

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

检查点进程 在获得 wal buffer中的 lsn 和 扫描内存中的脏页的过程中,会涉及到哪些锁?? 这些锁 的功能:等您坐沙发呢!

发表评论