GORM 乐观锁需显式注册插件且字段名必须为 Version、类型为 optimisticlock.Version;仅 Model().Update() 等触发校验,冲突时 RowsAffected()==0 而非报错,须重试前重新 SELECT 并限制次数。

gorm.optimisticlock 插件必须显式注册才生效
只在模型里加 Version 字段,GORM 完全不会触发乐观锁逻辑——这是最常被忽略的前提。插件不注册,Update 语句里压根不带 WHERE version = ? 条件,等同于裸写。
必须手动初始化插件:
import "gorm.io/plugin/optimisticlock"
db.Use(optimisticlock.New(optimisticlock.Config{}))
- 字段名必须是
Version(首字母大写),类型必须是optimisticlock.Version,不能用int或uint64替代 - 该字段必须是结构体一级字段,不能嵌套在匿名结构体或基类中
- 数据库迁移时,列类型需匹配:MySQL 用
INT,PostgreSQL 用INTEGER;BIGINT在 GORM v1.25+ 中可能不自动递增
Model().Update() 和 Save() 的行为天差地别
db.Model(&u).Update("name", "xxx") 会生成带版本校验的 SQL;而 db.Save(&u) 永远不会检查版本——它走的是全量覆盖路径,哪怕你字段里有 Version 值也无济于事。
- 仅
Model().Update()、Model().Updates()触发乐观锁拦截器 -
Create()、FirstOrCreate()、UpdateColumns()、Exec()全部绕过版本校验 - 批量更新多个字段,必须用
Updates(map[string]interface{}),别用结构体赋值后Save()
冲突检测只看 RowsAffected(),不是看 err
MySQL 和 PostgreSQL 在 WHERE version = ? 不匹配时,err 仍是 nil,只返回 RowsAffected() == 0。靠 if err != nil 判断,等于默认放弃所有并发冲突检测。
立即学习“go语言免费学习笔记(深入)”;
- 正确流程:执行
Update→ 检查result.RowsAffected()→ 若为 0,则说明版本已失效 - GORM 乐观锁冲突时只返回通用错误
gorm.ErrRecordNotFound,无法单靠错误类型区分是“记录不存在”还是“版本冲突” - 建议封装自定义错误,如
ErrOptimisticLock,并在业务层统一处理重试逻辑
重试逻辑不能塞进事务,且必须重新 SELECT
乐观锁本意是轻量、无锁、短交互。把重试包在 db.Transaction() 里,会让事务长时间持锁,违背设计初衷,还容易引发死锁和雪崩。
- 每次重试前,必须重新
SELECT当前行(含最新Version),绝不能复用旧值 - 重试次数必须硬限制,推荐 ≤3 次;间隔建议指数退避(如 10ms → 30ms → 100ms)
- 高频小字段更新(如点赞数、阅读数)应直接用
UPDATE SET count = count + 1,没必要引入Version字段


















