必须使用 gorm.io/plugin/optimisticlock 插件而非手写 SQL,模型中需定义类型为 optimisticlock.Version 的字段(如 V int64),插件自动注入版本条件;Update/Updates/Save 受保护,UpdateColumn 不受保护;冲突须用 errors.Is(err, optimisticlock.ErrOptimisticLock) 判断。

直接用 gorm.io/plugin/optimisticlock 插件,别自己拼 SQL 或手写 Where("version = ?") —— 官方插件已封装完整逻辑,且兼容 GORM v2 所有 dialect(MySQL、PostgreSQL、SQLite),手动实现容易漏掉事务一致性或版本字段自动递增。
必须加 optimisticlock.Version 字段到模型里
不是随便起个 Version int 就行。GORM 乐观锁插件只识别类型为 optimisticlock.Version 的字段,它内部做了类型断言和自动 SQL 注入。
- 字段名可以是任意的(比如
V、ver),但类型必须是optimisticlock.Version - 该类型底层是
int64,数据库对应列建议用BIGINT NOT NULL DEFAULT 0 - 不要给这个字段加
gorm:"default:0"标签 —— 插件会自动处理初始化,重复设 default 可能导致零值冲突 - 迁移时用
db.AutoMigrate(&Model{})即可生成该列,无需额外 SQL
Update 和 Updates 会自动带版本条件,但 UpdateColumn 不会
这是最容易踩的坑:插件只拦截 Update / Updates / Save 这类“全量更新语义”的方法,而 UpdateColumn 是绕过钩子的裸更新,完全不检查版本号。
- ✅ 正确:
db.Model(&u).Update("name", "new")→ 生成WHERE id = ? AND version = ? - ❌ 错误:
db.Model(&u).UpdateColumn("name", "new")→ 无版本条件,彻底失效 - ⚠️ 注意:
Save()虽然也走乐观锁,但会更新所有非零/非空字段,可能覆盖其他并发修改;推荐明确用Update
冲突时判断 errors.Is(err, optimisticlock.ErrOptimisticLock)
不能靠 result.RowsAffected == 0 判断失败 —— 因为 RowsAffected == 0 还可能是 WHERE 条件没匹配到记录(比如 ID 不存在),和乐观锁冲突是两回事。
- 必须用
errors.Is(err, optimisticlock.ErrOptimisticLock)做精准识别 - 这个 error 是插件定义的导出变量,不是字符串匹配,也不是自定义 error 类型
- 重试逻辑要重新
Take或First拉最新数据,再改再提交;别在原对象上反复Update - 如果业务允许“尽力更新”,可配合
context.WithTimeout控制最大重试次数,避免死循环
真正麻烦的不是加字段或调用方法,而是理解乐观锁只保护“单次更新原子性”——它不解决读-改-写中间状态被第三方绕过的问题(比如另一个服务直连 DB 更新)。如果业务涉及跨服务协同或强一致性要求,光靠 GORM 这层乐观锁不够,得结合应用层幂等、分布式锁或数据库唯一约束兜底。


















