在单机程序中,判断"哪个操作先发生"很简单——看时间戳。但在分布式系统中,不存在可靠的全局时钟。
假设三个节点 A、B、C 管理同一个 key:
color = "red",时间戳 10:00:00.000color = "blue",时间戳 10:00:00.001⚠️ LWW 把并发写入当成顺序写入处理——丢数据是必然的。
Leslie Lamport 在图灵奖论文 "Time, Clocks, and the Ordering of Events in a Distributed System" (Communications of the ACM, 1978) 中提出:
"事件的概念比时间更基本——我们需要的不是物理时间,而是因果顺序。"
核心思想:在分布式系统中,我们不需要(也不可能拥有)全局物理时钟。我们需要的是知道哪个事件因果上先于哪个事件。
Lamport 定义了 happens-before 关系(记作 →),三条规则:
同一进程内,事件 a 在事件 b 之前发生 → a → b
直觉:单线程代码执行是顺序的。
如果事件 a 是发送消息 m,事件 b 是接收 m → a → b
直觉:信息不能超光速传播。你不可能在信寄出之前收到信。
如果 a → b 且 b → c,则 a → c
直觉:因果链是可以传递的。如果 A 影响了 B,B 影响了 C,那 A 也影响了 C。
并发:如果既非 a → b 也非 b → a,则 a 和 b 是并发的,记作 a || b。
并发 ≠ 同时。并发意味着两个事件之间没有因果联系,而不是它们"同时发生"。即使 A 在物理时间 10:00 写入、B 在 10:01 写入,只要 B 不知道 A 的写入,它们就是并发的。
这就是为什么物理时间戳无法替代因果追踪——物理时间 10:01 > 10:00 不代表 10:01 的操作因果依赖于 10:00 的操作。
Lamport 时钟是最简单的逻辑时钟——一个单调递增的整数计数器。
LC = LC + 1send(m, LC)LC = max(LC, msg_LC) + 1class LamportClock:
"""Lamport 逻辑时钟"""
def __init__(self):
self.counter = 0
def event(self) -> int:
"""进程内事件"""
self.counter += 1
return self.counter
def send(self) -> tuple:
"""发送消息:先递增,附带时间戳"""
self.counter += 1
return (self.counter,) # (timestamp,)
def receive(self, msg_timestamp: int) -> int:
"""接收消息:取 max + 1"""
self.counter = max(self.counter, msg_timestamp) + 1
return self.counter
# 使用示例
clock_a = LamportClock()
clock_b = LamportClock()
# A 发生本地事件
t1 = clock_a.event() # A: 1
# A → B 发送消息
ts = clock_a.send() # A: 2, 消息附带 ts=2
t2 = clock_b.receive(ts[0]) # B: max(0, 2) + 1 = 3
print(f"A={clock_a.counter}, B={clock_b.counter}") # A=2, B=3
如果 a → b,则 LC(a) < LC(b)
但逆命题不成立:LC(a) < LC(b) 不意味着 a → b。两个并发事件也可能有不同的 Lamport 时间戳。
给定两个 Lamport 时间戳 LC(a)=3 和 LC(b)=5,你无法判断:
只能确定:如果 a → b,则 LC(a) < LC(b)。但 LC(a) < LC(b) 不代表任何因果关系。
这就是为什么我们需要向量时钟。
向量时钟由 Colin Fidge (1988) 和 Friedemann Mattern (1989) 独立提出。核心思想:用 N 维向量代替单一整数,每个维度对应一个进程。
N 个进程的系统中,向量时钟 VC 是长度为 N 的整数数组 [v₀, v₁, ..., v_{N-1}]。
进程 i 的向量时钟记为 VC_i,其中 VC_i[j] 表示进程 i 所知道的进程 j 的最新事件数。
VC_i[i] = VC_i[i] + 1send(m, VC_i)for k in range(N):
VC_j[k] = max(VC_j[k], msg_VC[k])
VC_j[j] = VC_j[j] + 1
from typing import List
class VectorClock:
"""向量时钟实现"""
def __init__(self, node_id: int, total_nodes: int):
self.id = node_id
self.vc = [0] * total_nodes
def event(self) -> List[int]:
"""进程内事件:递增自己的分量"""
self.vc[self.id] += 1
return self.vc.copy()
def send(self) -> List[int]:
"""发送消息:先递增,附带整个 VC"""
self.vc[self.id] += 1
return self.vc.copy()
def receive(self, msg_vc: List[int]) -> List[int]:
"""接收消息:逐分量取 max,再递增自己"""
for k in range(len(self.vc)):
self.vc[k] = max(self.vc[k], msg_vc[k])
self.vc[self.id] += 1
return self.vc.copy()
def __repr__(self):
return f"VC{self.id}={self.vc}"
# --- 使用示例 ---
# 3 个节点
vc_a = VectorClock(0, 3) # A=节点0
vc_b = VectorClock(1, 3) # B=节点1
vc_c = VectorClock(2, 3) # C=节点2
# A 本地事件
vc_a.event() # VC_A = [1, 0, 0]
# A 发消息给 B
msg = vc_a.send() # VC_A = [2, 0, 0]
vc_b.receive(msg) # VC_B = max([0,0,0],[2,0,0]) + [0,1,0] = [2,1,0]
# C 本地事件(与 A、B 并发)
vc_c.event() # VC_C = [0, 0, 1]
print(vc_a) # VC0=[2, 0, 0]
print(vc_b) # VC1=[2, 1, 0]
print(vc_c) # VC2=[0, 0, 1]
给定两个向量时钟 VC_a 和 VC_b:
VC_a < VC_b ⟺ ∀k: VC_a[k] ≤ VC_b[k] ∧ ∃k: VC_a[k] < VC_b[k]
即:a 的每个分量 ≤ b 的对应分量,且至少有一个分量严格小于。
三种关系:
VC_a < VC_b → a 因果先于 b(a → b)VC_b < VC_a → b 因果先于 a(b → a)VC_a || VC_b(不可比较) → 并发(a || b)点击按钮模拟事件,观察向量时钟如何变化
3 个节点(A=0, B=1, C=2),初始 VC 全为 [0,0,0]:
[1, 0, 0][2, 0, 0],消息携带 [2, 0, 0][2, 2, 0][0, 0, 1]比较:
✅ 向量时钟完美区分了因果关系和并发关系!
| 维度 | Lamport 时钟 | 向量时钟 |
|---|---|---|
| 数据结构 | 单个整数 | N 维整数向量 |
| 空间开销 | O(1) | O(N),N = 进程数 |
| 检测因果 | ❌ 只能单向:a→b ⇒ LC(a)<LC(b) | ✅ 双向:a→b ⇔ VC(a)<VC(b) |
| 检测并发 | ❌ 不可能 | ✅ VC(a) 与 VC(b) 不可比较 = 并发 |
| 提出时间 | 1978 (Lamport) | 1988 (Fidge) / 1989 (Mattern) |
| 适用场景 | 只需要排序,不需要因果关系 | 需要检测并发冲突 |
| 典型应用 | 分布式日志排序 | 冲突检测、因果一致性 |
| 通信开销 | 消息附带 1 个整数 | 消息附带 N 个整数 |
| 扩展性 | 无限节点 | 节点数受限于向量大小 |
Lamport 时钟是向量时钟的"压缩版"——丢弃了因果信息,换来 O(1) 的空间。向量时钟用 O(N) 的空间换来了完整的因果关系追踪能力。
如果你只需要给事件排个序(比如分布式追踪的 span 排序),Lamport 够用。如果你需要检测"这两个写入是并发的还是有序的",必须用向量时钟。
考虑两个事件:
Lamport 时钟只能告诉你 LC(a) < LC(b)。但可能有三种情况:
向量时钟保留了每个维度的信息:VC(a)=[3,0] vs VC(b)=[0,5],一看就知道是并发。Lamport 时钟把这些维度压成了一个数——信息不可逆地丢失了。
向量时钟是工具,因果一致性是目标。根据 Jepsen 的分类,因果一致性(Causal Consistency)是在始终可用的系统中能保证的最强一致性模型。
← 强一致(低可用、高延迟) 弱一致(高可用、低延迟)→
根据 Ahamad, Neiger, Burns 等人 1993 年的论文以及 Mahajan, Alvisi, Dahlin 的形式化:
三人聊天(来自 Jepsen 因果一致性页面):
因果一致性的要求:
根据 Mahajan 等人的证明("Consistency, Availability, and Convergence"),以及 Viotti 和 Vukolić 的综述(arXiv:1512.00168):
因果一致性是始终可用(always-available)系统中能保证的最强一致性模型。更强的模型(如线性一致性)在网络分区时必须牺牲可用性——这正是 CAP 定理的体现。
更精确地说,Real-Time Causal (RTC) 是被证明的严格上界。大多数实际系统(如 COPS, Bolt-on Causal)提供的因果一致性实际上包含 RTC 性质。
Amazon Dynamo(DeCandia et al., 2007, "Dynamo: Amazon's Highly Available Key-value Store")是最著名的向量时钟工业应用。
Amazon 购物车的关键需求:永远可写。在网络分区时,宁可接受多个版本的购物车(sibling),也不拒绝写入。向量时钟用于:
Dynamo 的 VC 不是简单的节点 ID 计数器。由于节点可能动态加入/离开,Dynamo 使用 (node_id, counter) 对的列表:
# Dynamo 风格的向量时钟
# 格式: [(node_id, counter), ...]
#
# 例如:
dynamo_vc_v1 = [("node-A", 1)] # A 写入 1 次
dynamo_vc_v2 = [("node-A", 2)] # A 又写入 1 次(v2 > v1)
dynamo_vc_v3 = [("node-A", 1), ("node-B", 1)] # A 写入后 B 写入(v3 > v1)
# 并发情况:A 和 B 各自独立更新
dynamo_vc_v4 = [("node-A", 2)] # A 独立更新 2 次
dynamo_vc_v5 = [("node-B", 1)] # B 独立更新 1 次
# v4 和 v5 并发!都是 v1 的后续,但彼此不可比较
当多个节点并发写入同一个 key 时,每个并发写入都产生一个 sibling。如果客户端只读不写回(没有执行合并),sibling 会持续累积:
Dynamo 的解决策略:设置 sibling 数量上限(如 100),超过时按 LWW 丢弃旧版本。这是一个无奈的妥协——丢失了因果信息。
根据 Dynamo 论文:
数据来源:DeCandia et al., 2007, "Dynamo: Amazon's Highly Available Key-value Store", SOSP
| 系统 | 向量时钟变体 | 冲突解决 |
|---|---|---|
| Riak | 标准 VC → DVV | Sibling + 客户端合并 |
| DynamoDB | 内部实现(未公开细节) | Last Write Wins(默认) |
| Cassandra | 不使用 VC,用 LWW | 基于时间戳的 LWW |
| Voldemort | 标准 VC | 可插拔冲突解决 |
Dynamo 的"永远可写"哲学非常适合 Agent 系统:当多个 Agent 实例同时修改共享记忆时,你宁可有多个版本需要合并,也不希望任何一个 Agent 的写入被拒绝。向量时钟正是实现这一目标的工具。
标准向量时钟有一个微妙的 bug:在多副本场景下可能丢失因果关系。这个 bug 被 Paulo Sérgio Almeida 等人在 2014 年的论文中揭示,并提出了 DVV 作为修复方案。
场景:1 个 key,3 个 server(S1, S2, S3),1 个 client C:
{S1: 1},值 = "v1"{S1: 1}{S1: 1, S2: 1},值 = "v2"{S1:1, S2:1} > S1 的 VC {S1:1},所以 v1 被 v2 覆盖 ✓{S1:1, S2:1}{S1: 2, S2: 1},值 = "v3"{S1:2, S2:1} vs {S1:1, S2:1},v3 > v2 ✓{S1:1} 来处理 C 的新写入,递增后变成 {S1:2},而 S2 还是 {S1:1, S2:1}——并发!v3 和 v2 被误判为并发,实际上 v3 因果上晚于 v2。⚠️ 根本原因:Server 的 VC 计数器递增同时承担了两个角色——"我处理了多少写请求"和"我看到的最新版本"。当这两个角色混在一起时,因果信息会丢失。
DVV (Dotted Version Vectors) 将这两个角色分离:
(server_id, counter)# DVV 数据结构
class DottedVersionVector:
def __init__(self):
self.dot = None # (server_id, counter) — 标识当前版本
self.vv = {} # {server_id: max_counter} — 已知的所有 dot
def create(self, server_id: str):
"""Server 处理写入时创建新 dot"""
counter = self.vv.get(server_id, 0) + 1
self.dot = (server_id, counter)
self.vv[server_id] = counter
return self
def merge(self, other):
"""合并两个 DVV"""
# 合并 version vectors
for sid, cnt in other.vv.items():
self.vv[sid] = max(self.vv.get(sid, 0), cnt)
# 保留 dot(如果 other.dot 不在合并后的 vv 中,它就是并发 sibling)
return self
def dominates(self, other) -> bool:
"""判断是否因果支配 other"""
if other.dot:
sid, cnt = other.dot
if self.vv.get(sid, 0) >= cnt:
return True # other 的 dot 已被我们知道
return False
Riak 从标准向量时钟迁移到 DVV(Riak 2.0+),解决了 sibling 爆增问题。根据 Riak 文档:
你可能每天都在用向量时钟——只是没意识到。Git 的提交历史就是一个向量时钟实例。
# Git 的 commit hash 就是"事件 ID"
# 每个 commit 记录了 parent(s),这构成了 happens-before 关系
# 线性历史(因果有序)
A -- B -- C -- D
# A → B → C → D
# 分叉(并发)
A -- B -- C (main)
\
D -- E (feature)
# C 和 D 是并发的!C || D
# Merge(合并向量时钟)
A -- B -- C ------ M (main)
\ /
D -- E --- (feature)
# M 的 parent 是 C 和 E,等价于 VC merge
当两个分支修改了同一行代码时,Git 无法自动合并——它不知道哪个修改"更好"。这就是向量时钟检测到并发后的冲突解决步骤。Git 的策略是:
Git 不用 LWW 丢弃数据——它把冲突交给人类处理。这是正确的设计!
向量时钟的本质是分布式版本控制的元数据。Git 用 parent 指针实现了同样的功能,只是表达形式不同:
[3, 0, 2] — 紧凑,但丢失了具体事件信息两者等价:向量时钟是 Git commit DAG 的一种"压缩摘要"。
多 Agent 系统天然是分布式系统——多个 Agent 实例并行工作,需要协调"谁做了什么"。
三个 Agent 协作完成任务:
因果链:R → W → M。如果 Reviewer 看到的 W 不是基于 R 的版本,就会基于错误的信息给出修改意见。
向量时钟保证:Reviewer 永远不会看到 W 但看不到 R——因果可见性确保了这一点。
两个 Agent 实例 A 和 B 同时修改用户档案:
user.name = "Alice",VC = [1, 0]user.email = "bob@x.com",VC = [0, 1]这两个修改是并发的(VC 不可比较),但它们修改的是不同字段——可以自动合并:
{name: "Alice", email: "bob@x.com"} # CRDT merge
如果它们修改了同一字段,就是真正的冲突,需要应用层决定保留哪个。
Agent 执行一个多步工具调用:搜索 → 分析 → 生成报告。如果"分析"步骤失败,"生成报告"不应该基于旧的搜索结果。
向量时钟可以追踪每一步的因果依赖:报告的 VC 必须严格大于分析的 VC,分析的 VC 必须严格大于搜索的 VC。
class AgentMemoryWithVC:
"""带向量时钟的 Agent 记忆系统"""
def __init__(self, agent_id: str, cluster_size: int):
self.agent_id = agent_id
self.agent_idx = self._resolve_index(agent_id)
self.vc = [0] * cluster_size
self.memories = {} # key -> (value, vc)
def write(self, key: str, value: str):
"""写入记忆,附带向量时钟"""
self.vc[self.agent_idx] += 1
self.memories[key] = (value, self.vc.copy())
def read(self, key: str):
"""读取记忆,返回 (value, vc)"""
return self.memories.get(key, (None, None))
def sync(self, other_memories: dict):
"""从其他 Agent 同步记忆"""
for key, (value, other_vc) in other_memories.items():
if key not in self.memories:
# 新 key,直接采纳
self.memories[key] = (value, other_vc)
self._merge_vc(other_vc)
else:
local_value, local_vc = self.memories[key]
cmp = self._compare_vc(local_vc, other_vc)
if cmp == 'less':
# 远端更新,采纳
self.memories[key] = (value, other_vc)
self._merge_vc(other_vc)
elif cmp == 'concurrent':
# 并发冲突!保留两个版本
self.memories[key] = self._merge_values(
local_value, local_vc, value, other_vc
)
self._merge_vc(other_vc)
# else: local 更新,忽略
def _compare_vc(self, a, b):
a_less = any(a[i] < b[i] for i in range(len(a)))
b_less = any(a[i] > b[i] for i in range(len(a)))
if a_less and b_less: return 'concurrent'
if a_less: return 'less'
if b_less: return 'greater'
return 'equal'
def _merge_vc(self, other_vc):
for i in range(len(self.vc)):
self.vc[i] = max(self.vc[i], other_vc[i])
def _merge_values(self, val1, vc1, val2, vc2):
"""应用层合并策略——默认保留两个版本"""
# 实际中可以用 CRDT 或让 Agent 自己决定
return (f"<> v1={val1} v2={val2}", vc1)
在 Agent 系统中使用向量时钟时,考虑以下几点:
"""
向量时钟完整实现:包括冲突检测、版本合并、垃圾回收
"""
from typing import Dict, List, Optional, Tuple
from dataclasses import dataclass, field
from enum import Enum
import time
class CausalRelation(Enum):
BEFORE = "before" # a → b
AFTER = "after" # b → a
CONCURRENT = "concurrent" # a || b
EQUAL = "equal" # a 和 b 相同
@dataclass
class VersionedValue:
"""带版本号的值"""
value: any
vc: List[int]
timestamp: float = field(default_factory=time.time)
class VectorClockStore:
"""带向量时钟的 KV 存储"""
def __init__(self, node_id: int, cluster_size: int):
self.node_id = node_id
self.cluster_size = cluster_size
self.vc = [0] * cluster_size # 当前节点的向量时钟
self.data: Dict[str, List[VersionedValue]] = {} # key -> [siblings]
def _increment(self):
"""递增自己的分量"""
self.vc[self.node_id] += 1
def _merge_vc(self, other_vc: List[int]):
"""合并另一个 VC 到自己的 VC"""
for i in range(self.cluster_size):
self.vc[i] = max(self.vc[i], other_vc[i])
@staticmethod
def compare_vc(a: List[int], b: List[int]) -> CausalRelation:
"""比较两个向量时钟的因果关系"""
a_less = any(a[i] < b[i] for i in range(len(a)))
b_less = any(a[i] > b[i] for i in range(len(a)))
if not a_less and not b_less:
return CausalRelation.EQUAL
if a_less and not b_less:
return CausalRelation.BEFORE
if b_less and not a_less:
return CausalRelation.AFTER
return CausalRelation.CONCURRENT
def put(self, key: str, value: any):
"""写入值"""
self._increment()
new_vc = self.vc.copy()
new_val = VersionedValue(value=value, vc=new_vc)
if key not in self.data:
self.data[key] = [new_val]
return
# 检查与现有 siblings 的关系
new_siblings = []
dominated = False
for existing in self.data[key]:
rel = self.compare_vc(new_vc, existing.vc)
if rel == CausalRelation.BEFORE:
# 新值因果先于已有值 → 新值被支配,丢弃
dominated = True
new_siblings.append(existing)
elif rel == CausalRelation.AFTER:
# 新值因果后于已有值 → 已有值被支配,丢弃
pass # 不保留 existing
else:
# 并发或相等 → 保留为 sibling
new_siblings.append(existing)
if not dominated:
new_siblings.append(new_val)
self.data[key] = new_siblings
def get(self, key: str) -> List[VersionedValue]:
"""读取值(可能返回多个 siblings)"""
return self.data.get(key, [])
def merge(self, key: str, remote_values: List[VersionedValue],
remote_vc: List[int]):
"""从远程节点合并数据"""
self._merge_vc(remote_vc)
if key not in self.data:
self.data[key] = remote_values.copy()
return
# 合并 siblings
all_values = self.data[key] + remote_values
merged = self._deduplicate_siblings(all_values)
self.data[key] = merged
def _deduplicate(self, values: List[VersionedValue]) -> List[VersionedValue]:
"""去除被支配的 siblings"""
result = []
for v in values:
dominated = False
for other in result:
rel = self.compare_vc(v.vc, other.vc)
if rel == CausalRelation.BEFORE:
dominated = True
break
elif rel == CausalRelation.AFTER:
result.remove(other)
break
if not dominated:
result.append(v)
return result
def _deduplicate_siblings(self, values):
return self._deduplicate(values)
@property
def clock(self) -> List[int]:
return self.vc.copy()
def siblings_count(self, key: str) -> int:
"""返回某个 key 的 sibling 数量"""
return len(self.data.get(key, []))
# === 使用示例 ===
if __name__ == "__main__":
# 3 节点集群
store_a = VectorClockStore(node_id=0, cluster_size=3)
store_b = VectorClockStore(node_id=1, cluster_size=3)
# A 写入 "color" = "red"
store_a.put("color", "red")
print(f"A after put 'red': VC={store_a.clock}")
# B 并发写入 "color" = "blue"(不知道 A 的写入)
store_b.put("color", "blue")
print(f"B after put 'blue': VC={store_b.clock}")
# A 再写入一次(基于 "red")
store_a.put("color", "green")
print(f"A after put 'green': VC={store_a.clock}")
# 模拟 A 收到 B 的数据并合并
b_values = store_b.get("color")
store_a.merge("color", b_values, store_b.clock)
print(f"A after merge: VC={store_a.clock}")
print(f"Siblings: {[(v.value, v.vc) for v in store_a.get('color')]}")
# 输出:两个 sibling ("green", [2,0,0]) 和 ("blue", [0,1,0])
# 因为它们是并发的!
向量时钟不会自动缩小。长时间运行的系统中,需要清理不再需要的旧版本。
def gc_vector_clocks(store: VectorClockStore, max_siblings: int = 10):
"""向量时钟垃圾回收"""
for key in list(store.data.keys()):
siblings = store.data[key]
if len(siblings) <= 1:
continue
# 策略 1:去除被支配的旧版本
store.data[key] = store._deduplicate_siblings(siblings)
# 策略 2:如果 sibling 仍然太多,按时间戳排序保留最近的
if len(store.data[key]) > max_siblings:
store.data[key].sort(key=lambda v: v.timestamp, reverse=True)
# 保留最新的 max_siblings 个
store.data[key] = store.data[key][:max_siblings]
标准向量时钟的大小 = 节点数。如果节点动态加入/离开,向量大小必须动态调整。
解法:使用 (node_id, counter) 对的稀疏表示(如 Dynamo),而非固定大小数组。
在长期运行的系统中,向量时钟的计数器只增不减,向量大小也会增长(新节点加入)。
解法:定期垃圾回收——删除已被所有活跃节点"知道"的旧版本(即 VC 的所有分量都 ≥ 该版本)。
高并发场景下,每个并发写入都产生 sibling,数量可能指数级增长。
解法:DVV 替代标准 VC;设置 sibling 上限;应用层及时合并。
在需要检测并发的场景用了 Lamport 时钟,结果丢失了并发信息。
解法:只要需要区分"有序"和"并发",就必须用向量时钟或其变体。
NTP 不同步导致 LWW 判断错误。NTP 通常精度 ±1ms,但分布式系统的事件间隔可以远小于 1ms。
解法:对于因果关系,不要依赖物理时钟。用逻辑时钟(Lamport/向量时钟)替代。
"时间戳晚" ≠ "因果后于"。10:01 的写入可能和 10:00 的写入完全无关。
解法:在分布式系统文档中明确区分"物理时间"和"因果顺序"。
Cassandra 的设计者(Avram/ Hewitt 等人)选择了基于时间戳的 LWW 而非向量时钟。理由:
⚠️ 代价:Cassandra 无法检测并发写入——并发的更新会被 LWW 无声丢弃。如果你的应用不能接受这个代价,应该用 Riak(向量时钟/DVV)或基于 CRDT 的系统。
只需要给事件排序?
├─ YES → Lamport 时钟(O(1) 空间,实现简单)
└─ NO → 需要检测并发冲突?
├─ YES → 并发频率低(<1%)?
│ ├─ YES → 标准向量时钟(如 Dynamo/Riak)
│ └─ NO → Dotted Version Vectors (DVV)
└─ NO → 需要保证因果一致性?
├─ YES → 向量时钟 + 因果广播协议
└─ NO → 最终一致性 + LWW 足矣
(如 Cassandra 场景)
更具体地:
你的数据能接受偶尔丢数据吗?
├─ YES → LWW(Cassandra 模式)
└─ NO → 你能接受多个 sibling 需要合并吗?
├─ YES → 向量时钟 / DVV(Riak 模式)
└─ NO → CRDT(自动合并,参见 crdt.html)
或 线性一致性(强一致,牺牲可用性)
| 需求 | 推荐方案 | 代表系统 | 复杂度 |
|---|---|---|---|
| 简单事件排序 | Lamport 时钟 | 分布式追踪 | ⭐ |
| 检测并发冲突 | 向量时钟 | Dynamo, Riak 1.x | ⭐⭐ |
| 检测并发+减少 sibling | DVV | Riak 2.0+ | ⭐⭐⭐ |
| 自动合并并发更新 | CRDT | Automerge, Yjs | ⭐⭐⭐⭐ |
| 强一致+无并发 | 共识算法 (Raft) | etcd, Consul | ⭐⭐⭐⭐⭐ |
| 不需要因果保证 | LWW | Cassandra | ⭐ |