分布式事务没那么高深,一次公司团购就能讲清楚
早几天的TiDB培训课上,主讲人在介绍事务机制时提到了三个CF:default、lock、write和主锁。说实话,那一块很多人听得云里雾里。我之前刚好了解过一点TiDB的事务是基于Percolator协议实现的,搞懂了Percolator,TiDB的事务也就通了。
如果让我用一句话总结Percolator协议:
Percolator的核心思想是:先把新数据写好,同时加上锁,但由于还没有写入提交记录,外部事务暂时还看不到这些修改;等确认所有参与者都准备好了,再写入提交记录,让修改正式生效。如果中途出问题,就根据“总负责人”的状态决定全部回滚,还是帮别人继续完成提交。
注意这里有个容易被误解的点:数据在Prewrite阶段其实已经写入了,只是还没有“提交记录”让它变得可见。就好比试卷已经答完交上去了,但成绩还没公布——在公布之前,所有人都不能说这门课已经通过了。
用“多人团购”理解Percolator
假设公司组织一次团购电脑,有三个人参与:小王、小李、小张。
老板说:“只有三个人都确认付款,公司才统一下单。”
这就是一个分布式事务。
先理解一个核心概念:“试卷封存”
在正式开始之前,我们先用一个例子理解Percolator最巧妙的设计。
想象一场考试:
- 考试结束后,学生已经把试卷交上来了(数据已经写好了)
- 但成绩单还没有公布(还没有提交记录)
- 在成绩公布之前,任何人都不能说这门课已经通过了,也不能查询最终成绩
- 只有老师正式公布成绩(写入Write Record)之后,这份试卷才真正“生效”
这就是Percolator的核心设计:
Data Column负责存数据,Write Column负责标记“数据已提交”。两者分开,才能实现“先准备、后生效”。
对应到TiDB,这三个列族就是:
- Data Column(default CF):存真正的数据
- Lock Column(lock CF):存锁信息
- Write Column(write CF):存提交记录,指向Data Column
下面我们开始正式的故事。
第一阶段:先报名(Prewrite)
每个人先在群里回复:
“我准备好了,我的钱已经留出来了。”
注意:钱还没有真正扣,只是“先锁住这笔钱,不允许拿去买别的东西”。
同时,系统已经把每个人的扣款数据准备好了(相当于填好了转账单),只是还没有正式生效。
例如:
- 小王:准备好了,钱已锁定,转账单已填好(锁住10000元,Data已写入)
- 小李:准备好了,钱已锁定,转账单已填好(锁住10000元,Data已写入)
- 小张:准备好了,钱已锁定,转账单已填好(锁住10000元,Data已写入)
这就是Prewrite(预写)。在Percolator中:
- 写入Data Column:把新值保存下来
- 写入Lock Column:加上锁,防止别人修改
- Lock里记录着Primary的位置:每个人都知道“有事找谁”
虽然数据已经写好了,但因为还没有Write Record(提交记录),其他事务仍然读取不到这些修改。
为什么不能直接付款?
如果小王付款了,小李手机没电,小张请假了——结果只有一个人买了电脑,整个团购失败。
数据库也一样:如果A修改成功、B修改失败,数据就不一致了。所以先全部准备,再统一生效。
第二阶段:宣布成交(Commit)
等所有人都说“我准备好了”,老板宣布:“现在正式付款!”
于是:
- 小王扣钱(写入小王对应的Write Record)
- 小李扣钱(写入小李对应的Write Record)
- 小张扣钱(写入小张对应的Write Record)
整个事务完成。
注意提交顺序:一定是先提交负责人(小王),再提交其他人。这个顺序是Primary Lock能正常工作的关键。
如果有人中途掉线怎么办?
假设:
- 小王:准备好了
- 小李:准备好了
- 小张:失联
这时候怎么办?
实际系统不会立刻取消团购,而是会等待一段时间(TTL),因为小张可能只是临时网络不好。
等待超时后,系统会去检查负责人小王:
情况一:小王已经付款了(Primary Lock已经被Write Record替换)→ 说明整个团购已经成功了,小李和小张继续完成付款,把他们的锁也转成正式记录。(这叫Roll-forward,论文原文叫“roll forward”)
情况二:小王还没付款(Primary Lock还在)→ 说明整个团购还没正式成交,全部取消,小李和小张的锁也一起清理掉。(这叫Rollback)
所以Percolator恢复事务时,只需要检查Primary。 这就是Primary Lock的意义。
Primary Lock:为什么要有负责人?
Percolator最经典的设计就是:不是每个人都是负责人。
老板指定小王为负责人(Primary),小李和小张是普通成员(Secondary)。
这里有个关键细节:小李和小张的锁里,都记录着“负责人是小王”这个信息——就像每个人报名时都备注了一句“有事找小王”。这样无论谁掉线,大家都知道该去问谁。
为什么这么设计?
因为以后如果出了问题,大家只需要问负责人到底有没有付款,不用问所有人。
Timestamp + MVCC:为什么需要时间戳?
继续举例。
上午9:00,小王看到库存10台。9:05,别人已经买走8台。9:06,小王付款。
怎么办?
Percolator的做法是:不是看现在库存,而是看“小王开始下单时看到的世界”。
事务开始时,系统给小王发了一个开始时间戳 start_ts,比如100。
小王读取数据时,流程是这样的:
- 先检查Lock:看看这个单元格有没有被人锁住。如果有锁,说明别人正在并发修改,小王得等一等。
- 如果没有锁,就去查Write Column:找到时间戳最大、且不超过start_ts(100)的那条Write Record。
- 根据Write Record里记录的指针,去Data Column读取真正的数据。
也就是说,真正决定哪个版本可见的,是Write Column,而不是Data Column。
这就是为什么TiDB有三个CF:default(存数据)、lock(存锁)、write(存提交记录,指向default里具体版本)。
可以理解成:每个人进入超市时,都会拿到一张“快照照片”,购物过程中看到的货架始终保持进入时的样子,不会因为别人后来买走商品而变化。
为什么要MVCC?
假设你去图书馆借一本书,管理员正在更新目录。
如果没有MVCC,你看到的是第一页旧版本、第二页新版本——整个目录乱了。
MVCC相当于管理员说:“你今天看到的是上午9点的完整目录。” 哪怕后台已经更新到9:05、9:10,你仍然看到9:00完整一致的数据。
这就是Snapshot(快照隔离)。
为什么要两阶段提交(2PC)?
很多人第一次学都会问:为什么这么麻烦?
原因一句话:宁愿慢一点,也不能一半成功、一半失败。
例如银行转账:张三扣款成功,李四到账失败——钱是不是没了?
所以银行都会:先冻结 → 确认 → 一起生效
Percolator的Prewrite阶段,实际上会做两个检查:
- 检查Lock:有没有别人正在修改这个单元格?
- 检查Write:有没有别人已经提交了一个时间戳大于start_ts的新版本?
如果Write已存在且commit_ts > start_ts,说明有人在我开始之后提交了更新,发生了写写冲突,事务必须回滚。
这个检查非常关键——TiDB面试里经常问。
检查通过后,才会写入Lock和Data,进入下一阶段。
Observer:Percolator真正的创新
Percolator论文其实不是为了事务而发明事务。
Google真正想解决的是:网页更新 → 搜索索引立即更新
所以事务提交以后,自动通知索引程序:这个网页变了!(论文里叫Notification)
索引程序只更新这一篇网页,不用重新扫描整个互联网。
可以理解成:
以前:每次有一本新书 → 图书管理员重新整理整个图书馆现在:新书到了 → 只把新书放到书架 → 更新目录
效率提高了很多。
不过需要注意:这个通知不是强一致性的——触发通知的写操作和通知本身触发的处理,不在同一个事务里。也就是说,不能指望“要么都做,要么都不做”,它只是用来串联计算流程的,不是用来保证数据完整性的。
还有一个有意思的优化:如果同一个页面短时间内被反复触发,Observer可能只执行一次,把多次通知“折叠”成一次处理,避免浪费计算资源(论文里叫“message collapsing”)。这就好比快递员一天给同一个地址送三趟货,不如合并成一趟送。
最后,用一句话总结Percolator的五个关键思想:
MVCC:每个事务都看到自己开始时的一份“世界快照”,互不干扰。读取时先检查Lock,再查Write Column确定可见版本,最后去Data Column取数据。
TSO(时间戳):给每个事务发一个全局唯一、递增的“号码牌”,确定先后顺序。
Prewrite + Commit(两阶段提交):Prewrite阶段写Data + Lock,同时检查Lock冲突和Write冲突;Commit阶段写入Write Record让数据生效。
Primary Lock:给事务指定一个“总负责人”,恢复时只需要看负责人是否已提交:
- 负责人已提交 → 帮其他人也提交(Roll-forward)
- 负责人未提交 → 全部回滚(Rollback)
Observer(Notification):数据提交后自动通知下游处理,但它是非事务性的,可能被折叠合并,主要用于组织增量计算流程。
最后再加一句:
Percolator并没有发明2PC或MVCC,它真正的创新在于把MVCC、Primary Lock、两阶段提交和Observer组合在一起,使Google能够在Bigtable(一个不支持跨行事务的分布式存储系统)上,实现支持ACID事务的一致性增量计算。
这才是Percolator论文真正的贡献。
如果把Percolator比作一个生活场景,它就像组织一次大型团队活动:
先让所有人举手确认参加,每个人的确认单上都写着“有事找负责人”,同时活动方案已经拟好但尚未公布(Prewrite + Data已写 + Lock指向Primary),指定一个负责人(Primary),负责人确认后统一公布执行方案(Commit写入Write Record),活动结束后通知相关部门更新名单(Observer)。
如果活动过程中有人掉线,大家只看负责人有没有完成——完成了就帮其他人补上(Roll-forward),没完成就全部取消(Rollback)。
这样既保证了所有人的行动一致,又避免了因为个别人掉线导致整个流程混乱。
希望这篇文章能帮你更好地理解Percolator协议,下次再听到TiDB的事务机制时,脑海里浮现出公司团购的画面就好懂了。