向量时钟给并发写入排序
每台机器给每个参与方记一个计数。比较向量就能看出因果先后还是并发冲突,从而决定直接合并还是要格外小心。 单一时间戳并不够 单个时间戳没法给同时发生的事件定出先后——因为时间并不等于因果,两台各自写了点东西的节点哪怕盖上相同的时钟,依然说不清谁先谁后。向量时钟的解法是给每个参与者一个自己的计数器:进程发生动作时给自己那格加一,收到消息时把发送方的向量并入自己的。于是每个事件都带上一列持续成长的计数,比较两个向量,就能分辨「谁确实发生在谁之前」,或者「二者根本无法比较」。 比较向量看清因果 比较向量时逐个元素看:如果 A 的每一项都小于等于 B 的相应项,那么 A 发生在 B 之前;否则这两个向量就是并发的。因为每逢两条路径交汇、先后关系就被收进向量,所以向量同时记下了直接与跨越多级的先后,这是单个标量永远做不到的。每当一条消息从 A 抵达 B,B 的向量里便同时写着「B 自己的进展」和「A 当时所知道的一切」,于是因果靠账本,而不是靠猜。 并发就是一次冲突 「并发」在向量意义上并不指出谁对谁错,它只说两件事既不相依、也需要靠外力定序。定序往往通过比较键值或向量自身来裁决,关键是必须可复现,好让每一份副本都同意。设计上很诚实:向量时钟绝不为本来就无关的事件硬造一个总体顺序,它只是如实地指出「这里本来就没有顺序」;至于把顺序画下去的笔,应该交给上层的业务规则。 坏掉的样子 向量时钟最先坏在长大。每个参与者都要占一项,同一个键被几百个短命客户端写过,向量就带几百个计数,每次读都要背回来,每次写又要送回去。Dynamo 早就记录过这种压力:调和元数据随写者集合一起膨胀;Riak 运维看到的是同一症状,因果上下文过大拖慢读取,还会繁殖出更多兄弟版本。把最老的表项逐出看似解脱,实际是把因果一起删掉。 一旦把 [A:3, B:2, C:5] 里的 C 裁掉,后续比较会把缺席的槽位当成零,于是 C 早就见过的事件又显得像全新事件,本该被包含的写入也会复活成假的兄弟版本。危险在于截断后的向量依然能干净地比较,不会抛出任何错误:因果历史被静默缩短,存储被迫保留两个明明可以用一项保留记录排好顺序的版本。 第二种坏法是把并发误读为先后。只比总和、只比最大值,或者先看向量旁边的墙钟,就会在 [A:2, B:1] 与 [A:1, B:2] 之间硬选一个赢家,但逐项看谁也不支配谁,诚实的答案是并发,不是先也不是后。被丢掉的兄弟版本,就是一次被时钟正确标出却又被弄丢的更新。反方向的错误是把某一项相等当成历史相等:[A:2] 收到两次是同一段历史又重放了一遍,但 [A:2] 对上 [A:2, B:1] 却是严格的先于关系,用更短的向量覆盖更长的向量等于把历史倒转。 ...