在获取 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 性能,内核开发者做了极其疯狂的微操设计:
- 阵列化(Array):系统默认会创建 8 把独立的 WAL 插入锁(宏定义
NUM_XLOGINSERT_LOCKS = 8)。前台业务写日志时,只需要随机挑这 8 把锁里的 1 把去锁定即可,冲突概率瞬间降低 8 倍! checkpointer的霸道包场:当checkpointer去拿起跑线 LSN 时,它会写一个for循环,瞬间把这 8 把锁全部用排他模式(Exclusive)死死锁住! 只有 8 把锁全部拿下,它才敢去读取那个 LSN 坐标。- 防伪共享(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 的前台业务岂不是瞬间卡顿了?
这就回到了我上一次推演强调的**“持有时间极短”**:
checkpointer锁死这 8 把锁。- 它仅仅是去读一个内存变量
Insert->CurrBytePos,然后做几步简单的加减乘除,计算出确切的 LSN。 - 它绝对不进行任何磁盘 I/O!
- 算完之后,立刻释放这 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(缓冲区块描述符)】对象上!
为了让你彻底看清它的物理隔离边界,请死死记住以下“三不原则”:
- 绝不加在整个内存上:它没有锁整个
shared_buffers数组。如果有 1600 万个缓冲页,内存里就有 1600 万个BufferDesc对象。它每次只锁当前正在看的那 1 个。 - 绝不加在真实的数据页上:它没有去锁那张 8192 字节(8KB)的真实数据纸张!
- 仅仅锁住“花名册的一行”:
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 数据页的内容】上!
为了让你彻底看清它的物理隔离边界,请死死记住以下“三不原则”:
- 绝不加在整个内存上:它没有锁整个
shared_buffers。如果有 1600 万个缓冲页,系统里就有 1600 万把对应的Buffer Content Lock。它每次只锁当前正在写的这 1 页。 - 绝不加在整张表上:哪怕你这张表有 100 个 G,它也只锁当前被刷下去的这 8KB 纸张,表的其他部分畅通无阻。
- 区别于刚才的“花名册锁”:刚才打标签用的是
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 和 扫描内存中的脏页的过程中,会涉及到哪些锁?? 这些锁 的功能:等您坐沙发呢!