分布式写入冲突如何发现?
简化版
写冲突检测要让请求声明“基于哪个版本修改”,系统判断当前版本是否仍匹配。 单主常用 version CAS/ETag;多主用版本向量区分因果与并发,HLC 可辅助排序但不能单独识别并发;唯一约束和状态机发现业务冲突。 只看更新时间或失败日志会漏掉静默覆盖。
详细版
客户端读取 v12 后提交 expectedVersion=12,存储用主键和版本做条件更新。 影响行数为 0 表示已有并发写或对象不存在,返回冲突并让客户端重读。 多副本各自 version+1 会生成同号不同内容,需要因果版本、中心序列或共识。 冲突还包括唯一键、库存不为负、状态非法等不变量。
完整版教学
一、丢失更新
A、B 都读取版本 5。 A 写入 X,B 随后写入 Y。 没有条件时 Y 静默覆盖 X。
冲突检测要把读取时的前置版本带回写入。
二、乐观锁
UPDATE item
SET value = :newValue, version = version + 1
WHERE id = :id AND version = :expectedVersion;
一行受影响表示成功,零行表示冲突或不存在。 再次查询可区分原因。
三、HTTP ETag
GET 返回 ETag v12。 更新使用 If-Match v12。 不匹配返回 412 Precondition Failed。 ETag 要绑定实体语义版本,不用不稳定序列化结果。
四、多主并发
两个副本都从 v5 写出 v6,标量无法判断。 版本向量可识别互不支配。 HLC 辅助排序,但业务仍需解决冲突。 需要唯一顺序时使用 Leader 或共识。
五、业务不变量
唯一索引发现重复用户名。 库存使用 quantity>=amount 的条件更新。 订单使用 expectedState 防非法迁移。 行版本不能替代这些业务约束。
六、冲突处理
| 冲突类型 | 可选处理 | 不能忽略的边界 |
|---|---|---|
| 单记录覆盖 | CAS 失败后重读、重算、重试 | 重试函数必须基于新状态 |
| 可交换增量 | 原子累加或合适的 CRDT | 操作必须满足对应代数性质 |
| 字段归属不同 | 按字段权威来源合并 | 不能用整行时间戳覆盖 |
| 资金或库存不变量冲突 | 拒绝并进入人工/业务流程 | 不能靠 LWW 静默吞掉 |
返回 409/412 让客户端重读和合并。 可交换操作可基于新状态重算。 不可合并版本进入人工队列。 重试使用相同 idempotencyKey 防重复副作用。
七、观测审计
统计按实体、接口和原因的 conflict rate。 记录 expected/current version、字段摘要和 requestId。 热点冲突高时考虑命令化、分片或串行化。 测试覆盖并发屏障、乱序与响应丢失。
八、常见误区与追问
- 误区:事务会自动发现所有丢更新。 取决于隔离级别与写法。
- 误区:更新时间可可靠检测。 精度与漂移会失败。
- 误区:CAS 失败后直接重放。 必须基于新状态重算。
- 误区:行版本表达所有冲突。 业务不变量仍需检查。
- 追问:多主为何不能各自加一? 会生成同版本不同内容。
- 追问:冲突返回什么? 当前版本和重试指引。
- 追问:热点怎么办? 改命令语义、分片或共识串行化。
九、加强记忆
- 写携带 expectedVersion。
- CAS/ETag 暴露静默覆盖。
- 多主用因果版本或共识。
- 业务约束独立检查。
- 冲突后重读再决策。