GORM 乐观锁更新不能仅靠普通事务“增强可靠性”,其核心是 UPDATE 的 WHERE version = ? 条件与 RowsAffected == 0 判断;事务仅保障「读-改-写」整体原子性,不替代版本校验。

直接说结论:GORM 实现基于事务的乐观锁更新,**不能靠把乐观锁塞进一个普通事务里来“增强可靠性”**——乐观锁本身不依赖事务加锁,它的冲突检测靠的是 WHERE version = ? 条件和 RowsAffected == 0 判断;而事务在这里真正起作用的,是包裹「读-改-写」这一整套逻辑的原子性兜底(比如防止读完就崩溃、或中间被其他非乐观锁操作干扰),不是用来锁行。
为什么不能只用 db.Transaction 包一层乐观锁更新?
很多人误以为“加个事务就能让乐观锁更稳”,结果发现并发时照样丢数据。问题出在理解偏差:
- 乐观锁生效的关键是
UPDATE ... WHERE id = ? AND version = ?这条语句本身带版本校验,GORM 的optimisticlock插件正是靠自动注入这个条件实现的 -
db.Transaction只保证内部多个 SQL 原子执行(成功一起提交、失败一起回滚),但它**不会改变乐观锁 UPDATE 的语义**,也不会阻止其他事务在你First和Update之间抢先更新同一行 - 如果你手动拼 SQL 或用
UpdateColumn却忘了写WHERE version = ?,包再深的事务也没用——它锁不住逻辑冲突
optimisticlock.Version 字段必须显式声明且类型正确
GORM 官方插件 gorm.io/plugin/optimisticlock 不会自动识别叫 Version 的 int64 字段,必须用它提供的专用类型,否则乐观锁逻辑压根不触发。
错误写法(无效):
type User struct {
ID int64 `gorm:"primaryKey"`
Name string
Version int64 `gorm:"default:0"`
}
正确写法(必须):
gorm:"primaryKey"
-
optimisticlock.Version是一个带方法的自定义类型,GORM 内部通过类型断言识别它 - 字段名不强制叫
Version,但类型必须是optimisticlock.Version,否则插件静默忽略 - 迁移建表时,该字段会被自动设为
bigint(MySQL)或对应数据库的大整型,别手动改成int
事务 + 乐观锁的合理组合方式
真正需要事务包裹乐观锁的场景,是「读取后还要做其他数据库操作,且全部要原子化」。比如扣库存+生成订单记录,两个动作要么全成,要么全败,而库存更新本身还得防并发。
- 先用
db.First读出带最新Version的记录 - 在事务内调用
db.Model(...).Update—— 此时 GORM 自动拼出带WHERE ... AND version = ?的 SQL - 检查
result.RowsAffected:为 0 表示乐观锁冲突,应主动tx.Rollback()并返回错误,**不要等事务自动提交** - 如果还有其他操作(如插入订单),也放在同一事务内,确保整体一致性
示例关键片段:
err := db.Transaction(func(tx *gorm.DB) error {
var product Product
if err := tx.First(&product, "id = ?", pid).Error; err != nil {
return err
}
<pre class="brush:php;toolbar:false;">// 扣减库存(自动带 version 检查)
result := tx.Model(&product).Updates(map[string]interface{}{
"stock": gorm.Expr("stock - ? ", quantity),
"version": gorm.Expr("version + 1"),
})
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return optimisticlock.ErrOptimisticLock // 显式抛出,触发回滚
}
// 同一事务内创建订单
if err := tx.Create(&Order{ProductID: pid, Qty: quantity}).Error; err != nil {
return err
}
return nil // 成功则提交
})
容易被忽略的三个细节
实际踩坑最多的地方往往不在主流程,而在这些边缘点:
-
First和Take都能读取单条,但Take不保证按主键顺序,若表无主键或有复合主键,Take可能读到非预期行,导致后续乐观锁条件错配 - 使用
UpdateColumns会绕过 GORM 的钩子和乐观锁自动注入,哪怕字段里有optimisticlock.Version—— 它只认Update和Updates - 乐观锁冲突错误
optimisticlock.ErrOptimisticLock是一个特定错误值,不能用errors.Is(err, errors.New("xxx"))判断,必须用errors.Is(err, optimisticlock.ErrOptimisticLock)











