3.2 并发控制机制


3.2 并发控制机制

本节摘要:隔离性要靠具体机制兑现,机制不止锁一种。两阶段锁用阻塞换确定性,MVCC 用版本换"读不阻塞写",乐观并发赌冲突稀疏、提交时才验证。三者差异不在对错,而在冲突处理时机与代价结构。读完你能说清每种机制在哪里拦冲突、要付什么账,并据此判断读多写少、热点竞争、长事务各自该选谁,以及死锁和写偏斜为什么会漏网。这三套机制常常组合使用,而不是非此即彼、更不是非黑即白。

上手前先明确

阅读完本节,你应当能够:

  1. 说清两阶段锁的增长与收缩两个阶段,以及严格两阶段锁为什么能保证可串行化
  2. 解释 MVCC 如何靠版本链做到读不阻塞写,以及版本堆积的代价
  3. 复述乐观并发的读、验证、写三阶段,并指出写偏斜为何漏网
  4. 从冲突时机与代价两个维度对比锁、MVCC、乐观并发,给出选型建议
  5. 说明死锁检测的等待图原理与受害者选择逻辑

一、问题与直觉

同一张银行卡,三个操作员同时动手:柜台转出 50 万,手机端扣 12 万理财,后台批处理计提当月利息。三人都先读余额 100 万、各自算新值、再写回。没有协调的话,最后的余额取决于谁的写后到——可能是 50 万,可能是 88 万,也可能把利息算进一个已经被覆盖的旧值上。谁都没写错,但钱对不上了。

这就是并发控制要对付的事。上一节我们定了规矩——隔离性要求并发执行看起来像排队执行;这一节要回答的是"靠什么机制把这条规矩兑现"。它不是简单的加锁或排队,而是要在共享数据上施加一种局部有序的约束,让多个会话对同一份数据的交错访问,在逻辑上等价于某种串行顺序。

兑现的方式不止一种,分歧点在一个核心选择上:什么时候、用什么代价去发现和处理冲突。 有人主张未雨绸缪,冲突还没发生就先锁住;有人主张让子弹飞一会儿,等提交时再验;还有人干脆给每行数据留多个历史版本,让读的人各看各的过去。三派各有道理,各有代价,没有谁能通吃所有负载,这正是本节要拆开看清楚的地方。

二、两阶段锁:用阻塞换确定性

最直白的一派是锁。它的思路朴素:多个事务想动同一份数据,冲突早晚要爆,与其等提交时才回滚,不如一开始就声明意图、由锁管理器仲裁。共享锁允许多个读并存但排斥写,排他锁独占读写。锁住谁、锁多细,都是可调旋钮——表锁省心但并发差,行锁精细但管理开销大。

锁的生命周期由两阶段锁协议约束,这是保证可串行化的理论基石。协议把事务切成两段:

增长阶段只许拿锁不许放,收缩阶段只许放锁不许拿。这个"先全部拿齐、再统一释放"的纪律,保证了并发事务之间不会出现互相穿插的破坏性交错。严格两阶段锁更进一步,要求所有锁一直持有到事务结束,这让未提交事务的修改对别人完全不可见,崩溃恢复后也能靠锁状态重建一致性——持久性的一半底气就来自这里。

锁要落地,还得回答两个问题:锁什么模式、锁多细。模式上,共享锁允许多个读并存但排斥写,排他锁独占读写;再往上还有意向锁——它不锁具体行,只在更高层标记"下面有行被锁了",让别的表级操作一眼就知道该不该退让。粒度上,表锁最省心但并发最差,行锁精细但每把锁都要占内存、要管理;介于两者之间还有页锁和键范围锁。锁越细,冲突域越小,真正需要排队的事务越少,但锁本身的开销越大。这两者永远在互相拉扯,没有一劳永逸的答案。

锁的代价也是真的。最典型的是死锁:两个事务互相等对方手里的锁,谁都不肯先松手。现代系统不再靠简单超时,而是维护一张等待图——节点是事务,边表示"甲在等乙的锁",一旦发现成环,就挑一个受害者强制中止。挑谁也有讲究,通常看谁代价小、等得久、优先级低。这套决策在微秒级内反复发生,锁管理器其实是台高强度的实时仲裁机器。

三、MVCC:用版本换不阻塞

锁的毛病在于"写会挡读、读也会挡写"。MVCC 换个思路:不给数据加锁,而是给数据加时间维度。每一行不再是一个静止的值,而是一条版本链——谁在哪个事务里创建了这个版本、谁又在哪个事务里删掉了它。每个事务启动时领到一个时间戳,读操作只取"在我这个时间点已经提交"的版本,写操作则生成新版本,旧版本继续留给其他快照用。

这套设计的甜头很大:读完全不用等写,写也不用等读。一个跑报表的只读事务可以安心读它的历史快照,另一个在线事务同时在改这些行,谁也不碍谁。这正是 OLTP 与 OLAP 能挤进同一套系统的关键一步。

甜头背后是三笔账。第一是空间膨胀——旧版本不能随手删,得等后台的清理进程回收,一旦有长事务赖着不走,垃圾版本会越堆越多,拖垮查询。第二是可见性判断的开销——每次读都要比事务号、查提交状态,高并发下事务号分配器和提交日志本身就成了热点。第三是快照隔离的老毛病——它挡得住脏读和不可重复读,却挡不住写偏斜,因为两个事务的快照各自独立,谁也不知道对方想写什么。

版本链的判读规则可以概括成一句:一个版本对某事务可见,当且仅当创建它的那个事务已经提交、且提交发生在该事务的快照时间之前,同时它还没被删除、或删除它的那个事务在该快照之后才提交。这句话听着绕,落到底层就两个隐藏字段的事——一个记"谁创建了这个版本",一个记"谁删掉了这个版本",读的时候沿着指针往前找第一个满足条件的版本即可。这套机制之所以优雅,是因为它把"该不该看见"从一个全局的锁裁决,化约成了每个事务在本地就能算清楚的版本比较。

四、乐观并发:赌冲突稀疏

乐观并发连版本都懒得维护,它押注"冲突很少发生"。事务全程不加锁、不建版本,先在私有工作区里读读写写,把读集和写集记下来;提交前进入验证阶段,检查自己读过的数据有没有被别人抢先改掉;验证通过才把写集原子刷入共享数据。读、验证、写,三段分明。

它的威力在零运行时同步开销——冲突不发生时,几乎没有额外成本。但它把复杂性全压到了验证环节:怎么高效维护所有活跃事务的读写集、怎么精确界定验证的时间窗口,都不是省油的灯。更致命的是,它和快照隔离一样对写偏斜睁一只眼闭一只眼:两个事务读不同的行、写不同的行,读集没有交集,验证通过,联合起来却破坏了"一格只能订一次"这种跨行约束。它只看得见数据项级的冲突,看不见业务规则级的冲突。

验证环节本身也分派别。最朴素的做法是给每个事务盖两个时间戳——启动时间和提交时间,验证时只要检查"有没有一个在我启动之后提交的事务,写了我读过的东西",有就回滚。更激进的做法连读集都不全存,只存一个版本号,提交时比对版本有没有变。这些变体都在同一个天平上微调:验证做得越细,漏网越少,但验证本身越贵。乐观并发的全部艺术,就是把这笔账算到刚好——让正常路径几乎不花钱,让冲突路径尽快失败、尽快重试。

五、三套机制怎么比、怎么选

把三派放在一起,差异其实很清晰,都在"冲突时机"和"代价结构"这两条轴上。

图:并发控制机制对比

图:并发控制机制对比

真到了选型,我一般这么判断:热点数据竞争激烈、冲突概率高,用锁,因为它把冲突挡在事前,虽然会排队,但不会让事务白干一场后回滚重来;读多写少、报表与在线交易混跑,用 MVCC,读的流畅是它最大的红利;冲突极稀疏、且业务能接受偶尔重试的场景,乐观并发最省,因为平时几乎没有开销。反过来,长事务、高冲突、热点集中的场景,乐观并发和快照隔离都容易翻车,锁反而踏实。

现实里的系统很少只押一家。主流数据库几乎都是"MVCC 打底、锁补强"的组合拳:读走快照,写走锁,只有真正冲突的那一小撮操作才需要争抢。这么做的好处是各取所长——绝大多数路径享受 MVCC 的无阻塞,极少数危险交叉点再由锁或依赖检测兜底。所以我们讨论锁、MVCC、乐观并发,不是要从中三选一,而是要读懂每种机制擅长什么,才能在排查线上问题时,一眼看出当前瓶颈到底是锁排队、版本膨胀,还是验证风暴。

机制 冲突时机 阻塞 主要代价 合适负载
两阶段锁 事前预防 写挡读写 锁管理与死锁 热点竞争
MVCC 事中隔离 读不阻塞写 版本膨胀与清理 读多写少
乐观并发 事后清算 全程无锁 验证与冲突重试 冲突稀疏

⚠️ 常见坑:在冲突频繁的热点数据上用乐观并发。验证通过率会断崖式下跌,大量事务白跑一遍再回滚重试,吞吐反而比老老实实加锁更差。

💡 关键直觉:并发控制的本质,是把"逻辑上的串行等价"映射到"物理冲突的最小化路径"上。选机制不是选对错,是选在哪一步、花多大代价去处理冲突。

温故知新

  • 并发控制兑现隔离性,核心是选对冲突处理的时机与代价
  • 两阶段锁分增长与收缩两段,严格两阶段锁同时撑起可串行化与持久性
  • 死锁靠等待图检测成环,再挑受害者中止,是锁的伴生成本
  • MVCC靠版本链做到读不阻塞写,代价是旧版本膨胀与可见性判断开销
  • 乐观并发分读、验证、写三段,赌冲突稀疏,写偏斜会漏网
  • 选型看负载:热点用锁,读多用 MVCC,冲突稀疏用乐观并发

下一节我们离开"并发下怎么保持对",去回答更难的一问:机器断电、进程崩溃之后,已经提交的数据怎么从一堆半成品里原样找回来。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U