0
0
1
0
博客/.../

用“多人团购”理解Percolator协议

 TiDBer_wangwenjing  发表于  2026-09-07

分布式事务没那么高深,一次公司团购就能讲清楚

早几天的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中:

  1. 写入Data Column:把新值保存下来
  2. 写入Lock Column:加上锁,防止别人修改
  3. 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。

小王读取数据时,流程是这样的:

  1. 先检查Lock:看看这个单元格有没有被人锁住。如果有锁,说明别人正在并发修改,小王得等一等。
  2. 如果没有锁,就去查Write Column:找到时间戳最大、且不超过start_ts(100)的那条Write Record
  3. 根据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阶段,实际上会做两个检查

  1. 检查Lock:有没有别人正在修改这个单元格?
  2. 检查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的事务机制时,脑海里浮现出公司团购的画面就好懂了。

0
0
1
0

版权声明:本文为 TiDB 社区用户原创文章,遵循 CC BY-NC-SA 4.0 版权协议,转载请附上原文出处链接和本声明。

评论
暂无评论