7.2 一致性保证与快照隔离


7.2 一致性保证与快照隔离:时间刻度的裁决规则

本节摘要:一致性听起来像哲学词,在引擎里却是一组可以逐条执行的不等式。本节把裁决规则全部摊开:序列号是唯一的时间、快照是一个刻度、每次读取都取刻度之下的最新版本、写冲突是区间重叠。随后直面这套规则的缺口——写偏斜为什么合法、「读取后加锁检查」怎么补,最后用一道长快照吃掉磁盘的对账案例,补全承诺在空间账上的价格。

把时间降维成一个整数

引擎没有时钟服务,不问墙上的钟。它的时间是那个贯穿全书的序列号:每次写入领取一个严格递增的整数,写进日志、写进内存表、带着沉入每一个磁盘文件。这个整数不反映物理时刻,却完整刻画了所有写入的先后——「一致性」的全部裁决,都建立在它之上。

裁决规则一共三条,短到可以直接背下来,也值得用一张表钉在墙上:

规则 内容 一句话记法
读可见性 只返回序列号不超过读刻度的最大版本 刻度之下才有真相
写先后 两个写入的先后就是序列号的先后 号码即顺序,没有平局
冲突判定 读过的键在刻度之后被别人改过即为冲突 时间线重叠就得重开

规则一:读取(无论带不带快照)只返回序列号不超过读刻度的最大版本——不带快照时,刻度就是「当前」,于是读到的是最新已提交值;带上快照,刻度被冻结,读到的是那一刻的值。规则二,写先后:两个写入的先后就是号码的先后,不存在平局。规则三,冲突判定:一个事务读过的键,若在自己读取刻度之后被别的已提交事务改过,就是冲突——两条时间线在同一键上重叠了。

快照读在代码里的完整生命周期只有三行,纪律全在第三行:

read_options.snapshot = db->GetSnapshot(); // 领一个时间刻度 db->Get(read_options, "account:A", &value); // 只看刻度之下的版本 db->ReleaseSnapshot(read_options.snapshot); // 用完立刻归还

这三条规则有个共同的性质:全是整数比较。没有锁参与、没有协调者、没有分布式协议,单机一致性被降维成了算术。第 3 章说版本管理「把一致性下沉成文件系统原子性」,本节是那个论断的语义面。

规则的推论也值得点一句:既然裁决全是整数比较,那么「为什么看到这个值」永远可以给出证明式的解释——取现场任一次读取,把刻度与各版本序列号摆出来,答案唯一且可复算。这种可解释性在排错时极其有用:一致性争议不需要复现时序,只需要复算不等式。

快照隔离的完整合同

把三条规则合起来,就是快照隔离的完整合同:事务开启时领取一个读刻度,此后所有读取定格在这个刻度;写入在提交时做冲突核查,撞车则失败重试(悲观路径则在动手前就用锁避免了撞车)。

这份合同承诺什么、不承诺什么,要逐条念清楚。它承诺不脏读——刻度之外的字节一概不可见;承诺可重复读——同一刻度读一万遍结果相同;承诺不丢更新——同键并发写必有一方重试或等待。它不承诺的一类场景,名字叫写偏斜。

写偏斜值得用一个最小例子讲透。账户甲与账户乙,约束是「两者余额之和不得为负」。事务一读甲和乙,各自充足,只扣甲;事务二同时读甲和乙,也各自充足,只扣乙。两个事务写的键不相交——冲突核查形同虚设——双双提交,总和却双双跌破约束。规则层面无人违约:两个事务都老老实实读了刻度内的值、都只写了不冲突的键。缺的不是执行,是约定——快照隔离保证你看到的是一致的历史,不保证你基于历史做的推理在提交时仍然成立。

补丁接口也现成:读取后加锁检查。把「我只是读读」的键升级成「我读,并且提交前不许别人改」——引擎为这些键记录依赖,提交时核查它们有没有变过。补上这一手,写偏斜的缺口被堵住,代价是锁面扩大、冲突率上升——向串行化又走近一步,账单同步变厚。是否为某个业务打这个补丁,取决于那条业务约束值多少钱,引擎把定价权留给业务。

承诺的空间账单:一道长快照的对账

一致性的成本不只在延迟上。用一道真实复盘把它在空间账上的价格算出来。

某风控服务每天凌晨跑一个对账任务:开一个快照,做两小时的全量校验。某月账单异常——磁盘占用从四百 GB 涨到七百 GB,压缩持续满负荷却压不下去。对账流程:第一步看空间放大指标,一度冲到一点八;第二步看压缩统计,发现最底层文件的清理量骤降;第三步定位到那个两小时的长快照——快照活着多久,它刻度之前的旧版本就得留多久,压缩在最底层的清退动作被这个刻度整体冻结了两小时,每日写入高峰积累的旧版本全部滞留。

修复分三档。第一档,任务改造:把全量校验改成按分段快照滚动——每段十分钟一个快照,用完立即释放,旧版本的滞留窗口从两小时缩到十分钟。第二档,纪律固化:快照必须挂接生命周期管理,超时未释放自动作废并告警,快照最长存活时间写进运维规范。第三档,监控补位:「最老快照年龄」接入面板,超过阈值即预警——它是最直接的空间放大先行指标。整改后磁盘占用回落到四百二十 GB,压缩恢复常态。

这个案例给本章收了个漂亮的尾:一致性的每一档承诺都有两张账单,延迟账单当场结清,空间账单月底寄到。 只盯着读延迟调优的团队,迟早会在磁盘曲线上收到这份迟到的账单。

可见性裁决的五问五答

规则讲完,用问答把边界踩实——这五问在生产讨论里出现频率最高。

一问:不带快照的两次读取,之间别人写了一次,两次结果一样吗?不一样,也不该一样——不带快照的读刻度是「当前」,每次读都取当下最新。想要可重复读,开快照。

二问:事务里先读后写同一个键,写完立刻读,读到的是刚写的吗?在事务内部读到自己的写——这是读写一致性,任何合格的事务实现都承诺;但事务外的读者要等提交后才看得到。

三问:两个事务同时把计数器从一百加一,结果会是一百零一吗?不会,也不会丢——同键并发写必有一方冲突重试或锁上排队,终局是两百。丢失更新在合同覆盖范围内。

四问:快照开着,底层压缩把旧文件清了怎么办?清不掉——压缩在归并时检查所有活跃刻度,快照引用的版本一概保留。反过来的代价本章已经算过:快照活多久,旧版本留多久。

五问:写偏斜补丁要到处打吗?不要。补丁的锁面扩大是实打实的吞吐税,只给「读到并约束跨键关系」的关键路径打——账户间约束、容量配额这类真正的业务不变量;普通的状态更新不值得。

六问:这些规则与第 4 章的读路径是什么关系?同一套——读路径按刻度逐层裁决,事务只是给刻度加了生命周期与冲突核查;机制从未换过,换的是使用姿势。

本节要点

  • 一致性在引擎里是三条整数不等式:读可见性、写先后、冲突即区间重叠,没有锁也没有协议;
  • 快照隔离承诺不脏读、可重复读、不丢更新,不承诺「基于历史的推理提交时仍成立」;
  • 写偏斜是快照隔离的合法漏洞:各自写不相交的键、合起来击穿约束;补丁是读取后加锁检查,代价是锁面扩大;
  • 长快照冻结最底层的清退,是空间放大最隐蔽的推手;快照要短、要限时、要监控最老年龄;
  • 承诺有两张账单:延迟账当场结,空间账月底寄——两本都要看。

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