故事
苏州拙政园有株并蒂莲,一茎两花,同根而生。园艺师说,这种花没法人工培育,只能等它自己冒出来——两朵花从同一个节上长出,共享一脉养分,却各自向着阳光打开花瓣。最奇的是花期:若一花先放,另一朵必在三五日内跟着开;若两朵同时鼓蕾,则同日绽放,不差一个时辰。老园丁管这叫"有先有后,或齐头并进",反正不会一朵盛极而衰、另一朵才刚刚吐蕊。
一七五三年,两淮盐运使卢见曾在扬州修了一座小园,特意从苏州移来一缸并蒂莲。他有个癖好:每逢花开,必邀宾客作诗,按官阶高低依次题写,诗成裱成长卷。这一年花开两朵,卢见岩先饮三杯,提笔写了"双蕖映日";他的门客顾某接着写"藕丝不断"。轮到第三位时,长卷上忽然多出一行字——是卢见曾的幕宾、此刻正在南京公干的王某托人快马送来,写的是"同根各表"。
卢见岩大怒。按他的规矩,诗必须依次写,南京来的这一笔插在了第三位之前,乱了次序。但更微妙的是:王某怎么知道前两句是什么?他的"同根各表"分明是看了"双蕖映日"和"藕丝不断"之后才有的回应——他依赖那两句的存在,所以必须排在那两句之后。问题是,第三位的诗稿此刻还在研墨,王某的快马却先到了。
园子里的书记官犯了难。按到达先后,王某的诗该排在第三;按因果依赖,它必须在第一、第二之后,但第三之前还是之后?卢见岩最后拍板:王某既然看了前两句才动笔,那就紧挨着第二句,把原来的第三位往后挤——因果比先后更重要。
这缸并蒂莲第二年没开。移盆伤了根。
—
一九八三年,Leslie Lamport在《时间、时钟与事件排序》里写了同样的困境。两个进程,A先写x=1,B读到x=1后写y=2——那么y=2这件事,因果上依赖x=1。任何观察者如果看到了y=2,必须也能看到x=1;反过来,如果两个写入互不读取、互不依赖,那就是并发的,谁前谁后都可以。
Lamport给这种关系起名叫happens-before,用一个小箭头表示:A→B,意思是"B知道A,所以B在A之后"。这个箭头不依赖物理时钟,纯粹从事件的读写依赖里推导出来。分布式系统里没有全局时钟,但每个节点都能本地记录"我先看见了什么,后做了什么"——这些本地记录拼起来,就是一张因果网。
但因果一致有个裂缝:并发写入怎么办?
A写x=1,B同时写x=2,两者互不读取对方——这是并发,没有happens-before关系。因果一致性只说"有因果的必须按因果排",没说"并发的该怎么收拢"。于是A的副本里x=1,B的副本里x=2,两边各自为政,永远对不上。
一九九五年,Bayou系统的工程师在笔记本同步场景里第一次系统性地处理这个问题。他们的场景很具体:两个人各带一台笔记本去开会,离线编辑同一份文档,回到办公室再同步。如果两人改的是不同段落,合并就行;如果改了同一句,就得有个规则——不是"谁先到听谁的",而是"按同一个规则把两种改法捏成一种"。
他们用的规则叫last-writer-wins,简单粗暴:给每个写入打时间戳,大的覆盖小的。但这要求时钟同步,离线场景做不到。更好的办法是语义合并:比如两人各往集合里加一个元素,合并后集合同时包含两个元素——这不是覆盖,是收敛。
Bayou的论文里有个精妙的观察:因果一致性保证"有因果的不会乱",而收敛保证"并发的不会散"。两者合起来,就是Causal+:+号那个收敛规则,把并发写入的裂缝补上了。
—
二〇一一年,Riak数据库的开发者把这套思想搬进了分布式KV系统。他们的做法是:每个键值附带一个向量时钟,记录"哪些节点在什么时候(逻辑上)写过它"。读取时,如果系统发现两个版本互不覆盖(即向量时钟不可比较),就把两个版本都返回给客户端,让客户端自己合并——这叫sibling。客户端可以按业务语义写合并函数:购物车场景里,两个sibling合并就是并集;计数器场景里,合并就是求和。
但把合并责任推给客户端,等于承认系统层面无法统一。二〇一三年,Walter系统走得更远:它在存储层内置了事务级因果+,保证同一个事务内的写入要么全可见、要么全不可见,同时并发事务的冲突写入按预定规则自动收敛。代价是牺牲一部分可用性——收敛规则需要全局协调,协调就需要等待。
最干净的实现或许是COPS(Cluster of Order-Preserving Servers)。它的设计分两层:本地数据中心内,用链式复制保证线性一致;跨数据中心时,只保证因果+——每个写入携带其依赖集,远程副本收到后,必须等依赖集里的全部写入都到位,才允许这一写入可见。这就在全球尺度上实现了"有因果的不乱,并发的能收",同时避免了跨洋协调的昂贵延迟。
—
并蒂莲的园艺师们其实早就懂这个。
他们发现,若一枝上冒出两个花芽,必须尽快决定留哪个、掐哪个——若等它们都长大了再处理,两朵花争夺养分,往往都开不好。这叫早收敛:在分歧还小的时候就按规则统一,代价最小。若两朵花已经并蒂而生,那就只能共享资源,各开各的,花期由同一套物候规则约束——暖日同长,寒夜同缩,最终同日绽放。这叫规则收敛:不强制统一形态,但用共享的环境变量保证结果一致。
分布式系统里的Causal+,不过是把这两件事写进了协议:happens-before对应"谁先谁后,按因果定";收敛规则对应"并发时按同一套物候长"。
卢见岩的诗会长卷后来散失了。但并蒂莲还在拙政园开着,一茎两花,有先有后,或齐头并进。
—
概念解析
Causal+一致性是因果一致性的强化版本:它不仅要求尊重happens-before关系(若事件B依赖事件A,则所有节点必须先看到A再看到B),还要求并发写入可收敛——即没有因果关系的并发操作,必须能通过确定性的规则合并为一致的结果。
happens-before是分布式系统中最基础的事件序关系。Lamport定义:同一进程内,A发生在B之前则A→B;若A是写入、B是读取该值,则A→B;若A→B且B→C,则A→C。这个关系的闭包构成偏序——不是所有事件都可比较,并发事件即不可比较者。
因果一致性保证:若A→B,则任何看到B的节点必已看到A。这比线性一致弱(不保证全局实时序),比最终一致强(保证有因果的不乱)。但纯因果一致对并发写入无约束,可能永久分叉。
收敛(Convergence)补上了这个裂缝。常见收敛机制包括:Last-Write-Wins(时间戳/向量时钟比较)、CRDT(无冲突复制数据类型,内置合并语义)、应用层合并函数。Causal+要求系统提供至少一种收敛保证,使并发写入最终达成一致状态。
与相关模型的关系。 Linearizability([✓#19 时间缝隙里的贼])要求所有操作构成全局实时全序,是最强的一致性。Sequential一致性保留全序但允许与物理时钟偏离。Causal+进一步放宽:只要求因果序,并发可任意排——但加了收敛约束,防止永久分叉。Eventual一致性([✓#2 万香谱])最弱,只要求"若无新写入则最终一致",不保证任何实时或因果序。
工程权衡。 Causal+在现代系统中越来越重要,因为它在"可用性"和"可理解性"之间取得了独特平衡:比线性一致更易实现跨地域部署,比最终一致更能保证用户直觉("我发了评论后刷新,必能看到自己的评论")。Spanner用TrueTime区间逼近因果序,COPS用显式依赖集实现,Riak/Redis CRDT用数据类型内置语义——路径不同,目标都是同一株并蒂莲:有先有后不乱,齐头并进能收。