GORM的Update默认不触发乐观锁,因其仅在Save或显式配合Where("version = ?")时校验版本;Update/Updates本身不自动检查Version字段,必须手动添加版本条件并验证RowsAffected()是否为0。

为什么 GORM 的 Update 默认不触发乐观锁
GORM 不会自动基于版本字段做并发校验,哪怕你定义了 Version 字段。它只在调用 Save 或显式启用时才检查版本——这是最容易被忽略的前提。很多人加了 gorm.Model 里的 Version uint,一并发更新就覆盖成功,根本没拦住冲突。
真正起作用的是 Update 方法配合 Where 中的版本条件,或者更推荐的方式:用 Updates + 原子条件更新。
-
Save会自动读取当前记录、递增Version并写回,但前提是先First查出来——这中间有时间窗口,不是原子操作 -
Update(单字段)和Updates(多字段)本身不碰Version,必须手动拼Where("version = ?", oldVersion) - 若想让 GORM 自动管理整个流程,得用
Session(&gorm.Session{SkipHooks: true})避免重复触发钩子,再配合RowsAffected判断是否更新成功
怎么写一个安全的乐观锁更新函数
核心是「读-改-写」变成「原子条件更新」:只在数据库层面校验版本未变时才更新,并返回影响行数。下面是一个典型实现:
func UpdateWithOptimisticLock(db *gorm.DB, id uint, updates map[string]interface{}, expectedVersion uint) error {
result := db.Model(&User{}).
Where("id = ? AND version = ?", id, expectedVersion).
Updates(updates)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return errors.New("optimistic lock failed: version mismatch or record not found")
}
return nil
}
注意几个关键点:
-
expectedVersion必须来自上一次查询(比如SELECT id, name, version FROM users WHERE id = 123),不能靠客户端生成或缓存 - 不要用
Save替代Updates,否则 GORM 会先 SELECT 再 UPDATE,失去原子性 - 如果业务需要更新后拿到新版本号,得额外查一次,或用 PostgreSQL 的
RETURNING(需开启EnablePostgreSQLReturning)
MySQL 和 PostgreSQL 在乐观锁上的行为差异
底层 SQL 行为一致,但错误处理和扩展能力不同:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- MySQL:不支持
UPDATE ... RETURNING,所以更新后无法直接拿到新version,必须再SELECT;RowsAffected对于无变化的更新(如 set name='a' where name='a')可能返回 0,需确保更新字段值确实不同 - PostgreSQL:支持
RETURNING version,GORM 可通过Returning("version")拿到新值;同时对WHERE条件不匹配的情况,RowsAffected更可靠 - SQLite:行为接近 MySQL,但某些旧版本对
UPDATE ... WHERE返回影响行数不严格,建议升级到 3.24+
如果你依赖 Returning,记得初始化 DB 时开启:
db, _ := gorm.Open(postgres.Open(dsn), &gorm.Config{
NowFunc: func() time.Time { return time.Now().UTC() },
})
db.Callback().Create().After("gorm:update").Register("update_version", func(tx *gorm.DB) {
// 手动递增 version 的钩子(仅用于 Save 场景)
})
// 注意:Returning 需要数据库驱动支持,且 GORM v1.25+ 才稳定
什么时候不该用 GORM 内置 Version 字段做乐观锁
当你的并发粒度不是「整行更新」,而是「字段级加锁」或「状态机流转」时,Version 就不够用了。比如:
- 用户余额扣减:不能只靠 version,得用
UPDATE accounts SET balance = balance - 100 WHERE id = 123 AND balance >= 100 - 订单状态只能从
pending→confirmed:要用WHERE status = 'pending',而不是比对 version - 多个服务共用一张表,有些服务不走 GORM:version 字段可能被绕过,导致校验失效
这种情况下,应放弃 gorm.Model 的自动 version 管理,转为手写带业务条件的 Where 更新,并把 version 字段当作普通字段参与条件判断。
真正的难点不在语法,而在于「谁负责读取旧 version」「失败后重试策略怎么设计」「前端要不要暴露冲突提示」——这些没法靠框架自动解决。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










