第一步工程绝境:内存爆仓与“锁升级”的诅咒
【他山之石的痛点】 在 MySQL (InnoDB) 或 SQL Server 中,行锁是维护在内存里的(类似一个哈希表)。 假设你要执行一句 UPDATE orders SET status = 'DONE' WHERE create_time < '2025-01-01';,这条语句命中了 5000 万行数据。 如果把 5000 万个行锁对象全塞进内存,数据库瞬间就会 OOM(内存耗尽)崩溃。为了自保,MySQL 会触发**“锁升级(Lock Escalation)”**——强行把这 5000 万个行锁,合并成 1 个表级排他锁。 灾难后果:原本只是改历史数据,结果锁升级导致整张表瞬间瘫痪,所有新订单全部被卡死。
【PG 架构师的终极决断】 PG 的内核大神为了彻底消灭“锁升级”,做出了一个极其暴力且天才的物理决断: “我们绝对不在内存里记录行锁!谁想锁住某一行数据,谁就自己跑到磁盘上的那个 8KB 数据块里,把自己的名字(事务 ID)刻在那行数据的脑门上!”
第二步物理破局:数据块内部的“微型刻字碑”(xmax 与 t_infomask)
既然要把锁刻在数据行上,PG 是怎么做的?这就要深入到物理元组(Tuple)的头部结构了。
每一行数据(Tuple)的头部,都有几个极其关键的隐藏字段,PG 巧妙地**重载(Overload)**了它们来实现行锁:
xmax(删除/锁定事务 ID):- 在普通的 MVCC(多版本)逻辑里,
xmax记录的是“哪个事务删除了这行数据”。 - 但在行锁逻辑里,如果这行数据还没被真删,
xmax记录的就是“当前正在锁定这行数据的事务 ID (XID)!”
- 在普通的 MVCC(多版本)逻辑里,
t_infomask(状态掩码位图):- 这是一个 16 位的二进制掩码。因为
xmax既表示删除又表示锁定,怎么区分呢? - 如果
t_infomask里的HEAP_XMAX_EXCL_LOCK比特位被置为1,就明确宣告:“xmax里的那个事务没有删我,他只是把我锁住了(排他行锁)!”
- 这是一个 16 位的二进制掩码。因为
工程推导结论:当你执行 SELECT * FROM table FOR UPDATE 锁定一千万行数据时,PG 只是去磁盘上把这一千万行数据的元组头部的 xmax 填上你的事务 ID,并拨动一下 t_infomask 掩码。全程不消耗任何额外的“锁内存池”,完美的 0 内存开销!PG 永远不会发生锁升级!
第三步并发绝境:磁盘不能排队,冲突了怎么解决,逻辑推导,顺便解释使用特定 锁的原因
PostgreSQL 架构设计中最矛盾、也最精妙的“跨界缝合点”。
在普通的认知里,锁要么全在内存,要么全在磁盘。但 PG 为了不发生“锁升级”(规避内存撑爆),硬生生把行锁状态(xmax)写到了磁盘物理块上。
那么,致命的工程绝境来了:磁盘(物理数据块)是一个死物,它绝对没有“排队休眠”和“唤醒”的机制。
如果进程 A 在某行数据上写了自己的 xmax,进程 B 也想锁这行,B 读到磁盘块发现冲突了。B 怎么办?总不能原地写个 while(true) 死循环疯狂去读磁盘吧?那会瞬间把 CPU 和 I/O 全部烧毁。
用最严密的物理逻辑,推导内核架构师是如何利用**“重量级锁(Heavyweight Locks)的特殊机制”**,极其优雅地化解这个磁盘排队绝境的,并顺带讲透为什么非得用这种锁!
第一步绝境:寻找内存里的“排队锚点”
【逻辑推演】
- 现状:进程 B 在物理数据块上,看到了进程 A 的事务 ID(假设是
XID 100),确认发生了行锁冲突。 - 诉求:进程 B 必须交出 CPU,找个地方去深度休眠(Sleep),直到进程 A 结束事务再醒来。
- 破局点:既然不能在磁盘数据块上排队,那进程 B 只能去共享内存里排队。
- 寻找目标:进程 B 去共享内存里找什么呢?找那行数据吗?不行,因为 PG 根本不在内存里记录具体的“行”。
- 终极转换:进程 B 灵机一动——我不需要等待“这行数据”,我只需要等待“事务 100”结束! 只要事务 100 结束了,这行数据的锁自然就解开了。
第二步解局:向“重量级锁管理器”借道排队
既然把等待目标从“具体的物理行”巧妙转换成了“抽象的事务 ID”,接下来的操作就是教科书级的底层握手:
- 内核的潜规则:在 PG 中,每一个正在运行的事务,只要一启动,就会自动在重量级锁管理器(哈希网)中,对自己专属的**“事务 ID (TransactionId)”申请一把独占的排他锁(ExclusiveLock)**。只要事务没提交/回滚,这把锁绝不松手。
- 进程 B 的动作:进程 B 离开磁盘数据块,扭头冲进共享内存的重量级锁哈希表。它向系统申请一把**“针对
XID 100的共享锁(ShareLock)”**。 - 完美休眠:因为进程 A(XID 100)正死死握着自己事务 ID 的排他锁,所以进程 B 申请共享锁必定失败。系统极其自然地把进程 B 塞进了等待队列,进程 B 顺利交出 CPU 进入休眠。
- 交接与苏醒:十分钟后,进程 A 终于执行了
COMMIT。它释放了对自己事务 ID 的排他锁。重量级锁管理器立刻唤醒排队等候的进程 B。进程 B 醒来,再次去读磁盘数据块,发现xmax已经失效,开开心心地把自己的事务 ID 写了进去!
第三步深度剖析:为什么必须用“重量级锁”?(使用特点与原因)
面试官极有可能会追问:“既然是为了排队,那为什么进程 B 不去申请轻量级锁(LWLock)或者自旋锁(Spinlock)来等待事务 A 结束呢?”
这就要回到我们之前推导过的锁的使用特点与物理边界了:
原因 1:等待时间的“极其不可控性”(必须支持深度休眠)
- 对比:轻量级锁(LWLock)保护的是内存结构的微秒级修改,等几微秒就能拿到。
- 真相:进程 B 等待进程 A 的事务结束,这个时间可能是 1 毫秒,也可能是 1 个小时(比如 A 正在跑一个极慢的复杂报表,或者发生了网络超时)。如果用轻量级锁或自旋锁去等一个事务,会导致 CPU 长期处于系统态空转,瞬间引发全站瘫痪。
- 特性匹配:重量级锁天生就是为“长周期等待”设计的。 它的排队机制直接调用操作系统的底层信号量,让进程进入极其安稳的深度休眠,绝对不消耗哪怕 1% 的 CPU 算力。
原因 2:致命的“行级死锁”隐患(必须有死锁检测兜底)
- 业务场景:
- 进程 A 先锁了行 1(写入
xmax),再去锁行 2。 - 进程 B 先锁了行 2(写入
xmax),再去锁行 1。
- 进程 A 先锁了行 1(写入
- 物理绝境:这构成了极其经典的交叉死锁。如果仅仅在磁盘上互相看对方的
xmax,这两个进程会永远休眠下去,直到世界末日。 - 特性匹配:我们推导过,轻量级锁绝对没有死锁检测器,而重量级锁拥有全库唯一的“死锁检测机制(Deadlock Detector)”!当进程 B 将等待请求转化为“对事务 A 发起重量级锁申请”时,它同时也将这段等待关系注入了共享内存的等待有向图(WFG)中。1 秒钟后,死锁检测器醒来,顺着重量级锁的链表一查,瞬间发现 A 等 B、B 等 A 的闭环,立刻斩杀其中一个,化解了底层的物理死锁危机!
闭环总结
这段逻辑推导,揭示了 PG 并发控制中最强悍的组合拳:
- 磁盘承载容量:用物理行头(
xmax)记录锁,实现了 0 内存占用,彻底消灭了锁升级。 - 内存承载队列:将行级冲突转化为对“事务 ID”的重量级锁等待,实现了高效的 CPU 休眠,彻底消灭了 I/O 盲等。
- 算法承载安全:利用重量级锁管理器自带的死锁检测器,彻底消灭了业务乱序导致的永久死锁。
这套“物理占坑 + 逻辑排队”的混搭设计,完美平衡了容量、性能与安全性,这就是 PG 行锁的终极工程底蕴。
第四步复杂场景绝境:一群人共享一行怎么办?(MultiXact 机制) 逻辑推导
这绝对是 PostgreSQL 内核设计中把“计算机科学基本定理”运用到极致的一个模块。计算机科学里有一句名言:“任何问题都可以通过增加一个间接层(Layer of Indirection)来解决。”
在并发绝境下,PG 的 MultiXact(多重事务机制)就是这句名言的完美工程实践。
我们再次切入纯物理和架构的推导模式。假设你是内核架构师,面对“共享锁”的合法业务诉求,我们来看看你是如何被逼出 MultiXact 这套机制的。
第一步绝境:32 位空间的“物理死局”
【合法业务诉求】
- 进程 A 执行
SELECT * FROM orders WHERE id=100 FOR SHARE;(申请读共享锁)。 - 进程 B 也执行
SELECT * FROM orders WHERE id=100 FOR SHARE;。在数据库逻辑上,读锁与读锁是绝对兼容的。进程 A 和 B 理应同时持有这行数据的锁。
【物理架构的当头一棒】
我们在上一节推导过,为了不撑爆内存,PG 把行锁写在了物理元组头部的 xmax 字段里。
致命的物理限制:xmax 在 C 语言的结构体定义中,是一个严格的 32 位整数(4 个字节)。
它只能存下 1 个事务 ID!
如果进程 A 把自己的 XID 100 写进去了。进程 B 来了,它想写自己的 XID 101,怎么办?
- 覆盖 A?不行,A 的锁就丢失了。
- 扩充物理块?不行,如果把
xmax改成变长数组,几百个人并发锁同一行,那这行数据的体积会无限膨胀,导致严重的数据块物理分裂(Page Split)。
面对“1 个坑位”与“N 个合法持锁人”的绝对物理矛盾,架构陷入死局。
第二步破局:引入“间接指针”与 SLRU 外部存储
既然无法在原地的 4 个字节里塞下 N 个人名,内核架构师做出了一个极具魄力的决定:把这 4 个字节变成一个“指针(引用)”!
【架构重构动作】
- 发明新实体(MultiXact ID):内核创造了一种全新的 ID 系统,叫
MultiXactId(多重事务 ID,简称 MXID)。它也是 32 位的。 - 重载元组掩码(
t_infomask标记):为了区分xmax里存的到底是普通 XID 还是 MXID,内核在元组掩码里加了一个极其重要的比特位标记:HEAP_XMAX_IS_MULTI。- 如果这个位是
0,说明xmax里是单人锁。 - 如果这个位是
1,说明xmax里存的是一个MultiXact ID!
- 如果这个位是
- 建立外部映射表(系统级哈希字典):在 PG 的底层磁盘上(具体在
pg_multixact目录下),内核专门开辟了一块由 SLRU(简单最近最少使用算法)管理的特殊存储区。它负责记录:1 个 MXID = [XID 100, XID 101, XID 102...] 的数组。
第三步动态推演:MultiXact 的真实流转过程
我们让系统跑起来,看看这套间接映射是怎么完美解决并发共享的:
- 第一个人(进程 A, XID 100)到达:
- A 发现元组没被锁。它直接把
100写进xmax。此时是普通单人锁,没有任何额外开销。
- A 发现元组没被锁。它直接把
- 第二个人(进程 B, XID 101)到达:
- B 发现
xmax是 100,且锁类型兼容(都是 FOR SHARE)。 - B 不能直接覆盖。于是 B 触发 “MultiXact 升级”。
- B 向系统申请一个新的 MXID(假设是
MXID 50)。 - B 去
pg_multixact系统区里,把MXID 50映射为[XID 100, XID 101]。 - B 回到物理元组,把
xmax改写为50,并将t_infomask设置为HEAP_XMAX_IS_MULTI。
- B 发现
- 第三个人(进程 C, XID 102)到达:
- C 看到
t_infomask发现这是一个 MXID。它读取xmax里的50。 - C 顺藤摸瓜去系统区查
MXID 50,发现里面有 100 和 101 两位老哥。 - C 申请一个新的 MXID(比如
MXID 51),把映射更新为[XID 100, XID 101, XID 102]。 - C 回到物理元组,把
xmax改写为51。
- C 看到
- 排他者(进程 D,执行 UPDATE)到达:
- D 想执行写操作,它看到
xmax是MXID 51。 - D 去系统区展开这个数组,发现有 3 个人拿着共享锁。
- 根据我们在上一节讲的“借道排队”机制,D 会极其憋屈地向这 3 个 XID 分别申请重量级锁,直到这 3 个人全部 COMMIT 释放锁,D 才能被唤醒执行!
- D 想执行写操作,它看到
第四步:高级 DBA 的致命考点(MultiXact 的工程代价与故障)
在面试中,讲出原理只能算及格。你必须讲出这个架构设计在生产环境中带来的**“致命副作用”**,这才是高级岗位的试金石!
当你把行锁从 1 对 1 变成了 1 对 N 的映射,系统付出了极其惨痛的代价:
1. 性能断崖(SLRU 争用瓶颈)
pg_multixact 这块外部存储区域是在内存的 SLRU 缓冲池里的,且受极其严格的轻量级锁保护。如果你的业务存在极其变态的读共享并发(比如几万个人疯狂 SELECT ... FOR SHARE 同一行数据),会导致底层疯狂生成 MXID 并频繁修改映射表,引发极其恐怖的 MultiXactOffsetControlLock 锁争用,把系统 CPU 彻底吃干。
2. 运维核弹(MultiXact Wraparound 回卷故障)
这是 PG 历史上最臭名昭著的故障之一!
MXID 只有 32 位,最多容纳 40 亿个。如果 MXID 用尽了,系统就会发生“回卷(Wraparound)”。PG 为了防止过去的映射关系被覆盖导致数据错乱,一旦 MXID 消耗量达到预警线,且底层的 autovacuum 进程来不及清理时,整个数据库实例会强制宕机拒绝写入!
(作为 DBA,这要求我们必须时刻监控 autovacuum_multixact_freeze_max_age 这个要命的参数)。
逻辑闭环总结
这段推导,把 PG 内核架构师在绝境下的挣扎与破局刻画得淋漓尽致:
- 死局:4 字节
xmax无法存储 N 个并发持锁人的 XID。 - 破局:引入间接层(MultiXact ID),将 4 字节变更为指向外部数组的“指针”。
- 代价:带来了额外的 I/O 开销、SLRU 锁争用,以及恐怖的 ID 回卷宕机风险。
第五步:高级 DBA 的实战修罗场(架构的致命代价) 逻辑推导,不要举例
我们彻底摒弃所有的场景假设和举例,单纯从计算机科学原理、物理存储介质、以及系统资源(CPU、内存、磁盘 I/O、网络)的流转逻辑出发,来严密推导 PostgreSQL 将行锁“物理化”所必然引发的四大系统级灾难。
这是一次纯粹的架构级逻辑演算。
前置公理(架构设计的物理基础)
公理 1:PostgreSQL 的行锁机制,其本质是修改物理数据块(Page)内部的元组头部字段(xmax 与 t_infomask)。
公理 2:在关系型数据库中,任何对物理数据块的修改,都必须严格遵循 WAL(Write-Ahead Logging,预写式日志) 协议,以保证 ACID 特性中的持久性(Durability)和崩溃恢复(Crash Recovery)能力。
公理 3:计算机底层存储空间(如 32 位整数的寻址空间)是绝对有限的。
基于以上三大公理,推导开始:
推导一:逻辑读突变为物理写(写放大灾难 Write Amplification)
【逻辑链路】
- 触发条件:应用程序发起携带排他/共享意向的只读查询(
SELECT ... FOR UPDATE/SHARE)。 - 物理映射:根据公理 1,该查询逻辑上是“读”,但在物理底层,引擎必须向对应的内存缓冲页(Buffer Page)写入当前事务 ID。
- 状态翻转:原本干净的内存缓冲页,因为这一次锁状态的写入,其标识位被修改,状态从“干净页(Clean Page)”突变为“脏页(Dirty Page)”。
- 终极代价(I/O 耗尽):数据库的后台刷脏进程(BgWriter / Checkpointer)被设定为必须将所有脏页定期刷入物理磁盘。【推导结论】:纯粹的逻辑读操作,在 PG 架构下必然引发底层的物理磁盘写操作。在极高并发下,这会导致底层存储系统的 IOPS(每秒输入输出量)被锁机制的写放大效应彻底榨干。
推导二:高可用架构的连锁反噬(WAL 风暴与延迟)
【逻辑链路】
- 触发条件:承接推导一,物理数据块发生了状态修改。
- 协议强制:根据公理 2,为了保证哪怕系统断电也能恢复这个“锁状态”,引擎在修改内存页之前,必须先生成对应的 WAL 日志记录,并强制
fsync刷入磁盘的 WAL 文件中。 - 架构传导(主备同步):在工业级的主从高可用(Streaming Replication)架构下,主库生成的所有 WAL 日志,必须通过网络全量传输给只读备库。
- 备库重放代价:备库接收到这些仅仅是为了“加锁”而产生的 WAL 日志后,必须消耗 CPU 去解析并重放它们,这会与备库本地的只读查询产生物理块级别的锁冲突。【推导结论】:行锁的物理化,必然导致 WAL 日志生成的指数级膨胀(WAL Storm)。这不仅会瞬间消耗主库的磁盘顺序写带宽,还会直接打满主备之间的网络带宽,并最终引发只读备库的数据同步延迟(Replication Lag)。
推导三:间接层引入的锁降维瓶颈(SLRU 争用)
【逻辑链路】
- 触发条件:并发事务对同一物理行申请共享锁,触发 MultiXact 机制。
- 架构转移:引擎必须去外部的
pg_multixact存储区生成新的映射数组。 - 物理瓶颈:
pg_multixact数据并非直接存在于主内存(Shared Buffers),而是存放在一片极小的专用缓冲池中,由 SLRU(简单最近最少使用算法) 管理。 - 并发互斥:多核 CPU 同时操作 SLRU 缓冲池时,为了保证数据结构的正确性,必须频繁获取底层的轻量级锁(
MultiXactOffsetControlLock和MultiXactMemberControlLock)。【推导结论】:为了解决磁盘字段容量不足而引入的外部间接层(MultiXact),成功将行锁冲突转化为底层的内存轻量级锁争用。当并发量突破临界值时,CPU 算力将完全消耗在自旋锁的抢占上,导致系统吞吐量呈现断崖式下跌。
推导四:绝对空间的物理枯竭(Wraparound 宕机危机)
【逻辑链路】
- 触发条件:系统持续运转,事务 ID(XID)和多重事务 ID(MXID)被不断消耗并写入元组的
xmax字段。 - 物理极限:根据公理 3,XID 和 MXID 在内核 C 源码中均被定义为 32 位无符号整数。其物理绝对上限为 $2^{32}$(约 42.9 亿)。
- 逻辑死局:由于这是环形空间,一旦新分配的 ID 超过 42.9 亿,它将绕回 0。在 MVCC 逻辑判定中,未来的事务会被系统错误地判定为“过去的事务”,导致灾难性的全局数据可见性错乱。
- 内核兜底机制:为了避免数据彻底损毁,内核设置了硬性阈值。当 ID 消耗量逼近上限,且后台清理进程(
autovacuum)由于 I/O 瓶颈未能及时清理旧 ID(Freeze 操作)时,内核将触发终极自我保护机制。【推导结论】:行锁依赖 32 位 ID 的物理设计,必然导致系统存在一个定时炸弹。在清理速度赶不上消耗速度的极限工况下,数据库实例将强制切断所有写入连接,直接陷入停机状态(Database shutdown to prevent wraparound)。
高级 DBA 的工程破局策略
这四大灾难,是 PG/openGauss 内核为了“消灭锁升级”所必须承受的物理代价。作为高级 DBA,面对这种底层基因带来的缺陷,解决之道只有一条极其冷酷的逻辑:在架构层面实施降维打击。
既然物理行锁必然带来脏页、WAL风暴和 ID 枯竭,那么在面临极高频的并发扣减(如秒杀、库存扣减、防并发)场景时,DBA 必须强制要求研发重构代码:
废弃 SELECT ... FOR UPDATE,全面切换至“咨询锁(Advisory Locks)”。
- 逻辑优势:咨询锁直接调用我们在前面模块推导过的“重量级锁哈希网(
LockMethodLockHash)”。 - 物理收益:它仅仅在共享内存中创建一个 64 字节的
PROCLOCK结构体。0 物理块修改、0 脏页产生、0 WAL 日志记录、不消耗任何 XID/MXID。 完美避开了上述所有的四大物理灾难!
pg数据库 咨询锁 ,逻辑的推导一遍
这是极其精彩的“架构闭环”!前面我们花了巨大的篇幅,推导了“重量级锁(保护表结构)”和“物理行锁(保护行数据)”。但这两种锁都是为了保护数据库内部的物理实体而生的。
到了真实的千万级并发业务线(比如电商秒杀扣减、分布式定时任务防重跑),业务端需要的往往只是一个**“逻辑上的互斥信号”**,而不是真的要去锁住磁盘上的哪块物理数据。
如果用物理行锁去承载这种“逻辑互斥”,就会引发我们在上一节推导出的四大物理灾难(写放大、WAL风暴、SLRU争用、XID耗尽)。
作为内核架构师,面对这种**“业务逻辑互斥需求”与“数据库物理存储限制”之间的巨大裂痕,我们现在用纯工程逻辑,一步步推导出“咨询锁(Advisory Locks)”**的诞生。
第一步绝境:分布式并发互斥的“架构割裂”
【业务侧的诉求】
假设业务集群有 50 台应用服务器,它们同时收到请求,要对同一个用户 ID 执行极其复杂的“资格校验+发券”逻辑。
这 50 个线程必须排队。且此时数据库里甚至连这个用户的记录都还没有(无行可锁)。
【架构师的困境】
- 方案 A(强用数据库物理锁):为了用行锁,研发被迫先在数据库里
INSERT一条占位假数据,然后再SELECT ... FOR UPDATE。- 物理代价:疯狂产生无用的磁盘脏页和 WAL 日志,极其愚蠢地榨干底层 I/O。
- 方案 B(引入外部组件 Redis/ZooKeeper):在数据库外部搭建一套分布式锁集群。
- 物理代价:引入了额外的网络 RTT(往返时延)。
- 致命缺陷(ACID 割裂):Redis 的锁超时释放了,但数据库里的长事务还在执行。外部互斥语义与底层数据库的事务生命周期彻底脱节,导致极其隐蔽的并发脏数据。
第二步破局:内核哈希网的“降维开放”(API 化)
既然外部网络有延迟且事务脱节,内部物理行锁又会产生致命 I/O。那数据库内核里,有没有现成的、纯内存的、极速排队的、且带死锁检测的组件?
有!就是我们之前花大篇幅推导过的、位于共享内存中的那张“重量级锁哈希网(LockMethodLockHash)”!
【内核改造推导】
- 打破物理绑定:原本哈希表里的 Key(
LOCKTAG)必须是一个真实的表 OID 或事务 ID。 - 开放虚拟命名空间:内核架构师修改了 C 语言源码,在
LOCKTAG的枚举类型里,新增了一个虚拟类型LOCKTAG_ADVISORY。 - 暴露给业务层:内核提供一个特殊的 SQL 函数 API(例如
pg_advisory_lock( bigint ))。当业务端传入一个 64 位的整数(比如把 “发券_UserID” 哈希成一个 BIGINT),内核直接把这个整数当作 Key,扔进共享内存的哈希表里。
【破局收益】
业务端现在拥有了一把纯内存级别的分布式锁!它直接在底层的 PROCLOCK 结构体上进行位运算和链表排队。
它绝对不触碰物理数据块,不产生任何脏页,不消耗任何 XID!
第三步生命周期绝境:谁来释放这把锁?(推导锁的变种)
锁创建出来了,但工程落地时,生命周期管理是最大的灾难。
【场景 A:事务级防并发】
业务逻辑是:开启事务 $\rightarrow$ 获取锁 $\rightarrow$ 扣减库存 $\rightarrow$ 提交/回滚事务。
- 逻辑推导:这种锁必须和底层的
Transaction严格绑定。只要事务COMMIT或因为任何异常ROLLBACK,底层内存网必须自动沿着PGPROC链表,瞬间销毁对应的PROCLOCK结构体。 - 诞生了:事务级咨询锁(Transaction-Level Advisory Locks)。
- 对应函数:
pg_try_advisory_xact_lock()。 - 优点:研发永远不需要写
UNLOCK逻辑,绝对不会发生因为代码异常导致的永久死锁或内存泄露。
- 对应函数:
【场景 B:跨事务的长效会话】
业务逻辑是:分布式后台的 Master 节点选举。只要这个数据库连接(Session)不断开,它就一直霸占 Master 身份,期间它会提交无数个小事务。
- 逻辑推导:这种锁绝对不能在事务提交时被释放,它必须和
Session(物理连接进程)绑定。只有当业务端显式发送UNLOCK指令,或者这个 TCP 连接意外断开(进程回收)时,内核才能销毁锁。 - 诞生了:会话级咨询锁(Session-Level Advisory Locks)。
- 对应函数:
pg_try_advisory_lock()和pg_advisory_unlock()。
- 对应函数:
第四步:物理层面的“绝对压制”(DBA 实战价值)
我们把咨询锁(Advisory Lock)和传统物理行锁(Row Lock)放在千万级并发的显微镜下,进行最后一次纯物理视角的对比:
架构师闭环与面试绝杀连招
当面试官在考察你“如何在 PG/openGauss 中处理海量并发的秒杀/防重击”时,这套逻辑推导就是你的终极杀招:
“在面对秒杀等极高并发互斥场景时,我绝对不会允许业务使用
SELECT FOR UPDATE。因为通过底层物理机制推演,高频的行锁必然导致物理数据块的xmax和t_infomask被疯狂修改,进而引发严重的写放大、WAL 风暴和 SLRU 争用。我的方案是,要求研发必须在 SQL 层面调用
pg_try_advisory_xact_lock()。这在底层架构上,等同于将业务层的逻辑互斥,直接降维透传给了内核共享内存中的重量级锁哈希网(
LockMethodLockHash)。它利用底层的
PROCLOCK结构体进行微秒级排队,不仅完美继承了内核极其高效的深度休眠和死锁检测机制,而且在物理层面上实现了绝对的零脏页、零 WAL 日志、零 XID 消耗。这是关系型数据库在不引入外部组件(如 Redis)的前提下,处理高并发互斥的极限工程解法。”
至此,从自旋锁、轻量级锁、重量级锁、物理行锁,一路推导至降维打击的“咨询锁”,PG 数据库的并发控制内核防线已经被我们完整重构。
了解 www.876873.xyz 的更多信息
订阅后即可通过电子邮件收到最新文章。
postgresql数据库锁体系 之 行锁:等您坐沙发呢!