乐观锁的完整闭环:版本校验、冲突提示与重试

假设两个人同时编辑一条便签,打开时看到的都是第 4 版。甲先保存,便签变成第 5 版;乙随后提交自己基于第 4 版的修改。如果接口直接覆盖,甲的内容就会悄悄消失。

本篇目标: 用版本条件识别旧编辑,并把冲突处理设计到用户能继续操作的程度。

两个编辑者,如何避免覆盖
两个编辑者,如何避免覆盖放大图解 ↗

更新和版本比较必须一起发生

不要先读版本、在应用层比较,再执行没有版本条件的更新。比较与写入之间仍可能出现另一个提交。把条件放进同一条更新语句,才能让持久层决定本次修改是否仍然有效。

UPDATE notes
SET body = $1, version = version + 1
WHERE id = $2 AND version = $3
RETURNING version;

这是 PostgreSQL 风格的独立示例。返回一行说明提交成功;没有返回行时,需要进一步区分记录不存在、不可访问或版本冲突。对用户没有权限的对象,不应借冲突响应泄露最新内容。

客户端保留三个版本

做一个更容易理解的编辑界面,可以同时保留“打开时的原值”“尚未保存的输入”“服务器最新值”。这样才能判断差异来自哪里,也能避免简单地把新数据覆盖到输入框。

对于不同字段上的修改,可以展示合并建议;对同一字段的冲突,应让用户明确选择。自动合并需要领域规则,不能假定所有文本都能无损拼接。

冲突不是网络重试

如果使用旧版本号重新发送原请求,结果应继续冲突。正确的重试来自一次新的核对:读取最新状态、确认要保存的内容、携带新版本提交。

界面在等待期间也要防止重复点击。但禁用按钮只是交互措施,不能替代服务端的并发保护。多个窗口、手机与电脑同时操作,都可能绕过单个页面的按钮状态。

一个完整的演练顺序

  1. A 和 B 打开同一条便签。
  2. B 保存,版本递增。
  3. A 保存被拒,A 的输入仍完整保留。
  4. A 能看到哪些字段发生变化。
  5. A 明确确认后,使用新版本保存成功。

还应测试记录被删除、权限被收回、保存响应丢失等情况。好的乐观锁既不会悄悄覆盖别人,也不会让用户因为一次冲突重新输入整段内容。

见字如晤

图解