gorm中用clauses(clause.locking{strength: "update"})加for update行锁,必须在事务中使用,否则锁立即释放;strength为"share"时加共享锁,允许并发读但禁止写。

gorm.Clauses(clause.Locking) 怎么加 FOR UPDATE
直接在查询时加行级排他锁,必须配合事务使用,否则锁在语句执行完就释放,起不到保护作用。
-
db.Transaction()是安全起点,避免手动Begin()/Commit()遗漏回滚 -
Strength: "UPDATE"对应 MySQL 的SELECT ... FOR UPDATE,锁定选中行,其他事务无法修改或加FOR UPDATE - 若要允许其他事务读但禁止写,用
Strength: "SHARE"(即LOCK IN SHARE MODE) - 高并发下可能阻塞,需设超时:MySQL 层面配置
innodb_lock_wait_timeout,或 GORM 中加clause.Locking{Options: "NOWAIT"}立即报错而不是等待
示例:
db.Transaction(func(tx *gorm.DB) error {
var stock Stock
err := tx.Clauses(clause.Locking{
Strength: "UPDATE",
Options: "NOWAIT",
}).Where("goods_id = ? AND quantity > 0", goodsID).First(&stock).Error
if err != nil {
return err // 可能是 ErrRecordNotFound 或 lock wait timeout
}
return tx.Model(&stock).Update("quantity", gorm.Expr("quantity - 1")).Error
})
乐观锁必须手动加 version 字段吗
不是“必须”,但 GORM 原生不自动管理版本号;你得自己定义字段,并在更新时显式校验。官方插件 gorm.io/plugin/optimisticlock 可简化,但它仍要求你在模型里嵌入 optimisticlock.Version 类型字段。
- 手写方案更可控:用
int或uint类型的version字段,UPDATE时带WHERE version = ?条件,检查RowsAffected == 0判断是否被抢改 - 插件方案省代码但灵活性低:它会自动注入
WHERE version = ?和version = version + 1,但不支持自定义字段名、不兼容复合主键场景 - 注意字段默认值:首次插入时
version应为 0 或 1,别留空或 NULL,否则WHERE version = NULL永远不成立
手写更新逻辑片段:
result := db.Model(&product).Where("id = ? AND version = ?", id, oldVersion).
Updates(map[string]interface{}{
"stock": product.Stock - 1,
"version": oldVersion + 1,
})
if result.RowsAffected == 0 {
return errors.New("optimistic lock failed: data changed by others")
}
悲观锁失败时错误信息怎么判断
GORM 不统一包装锁失败错误,最终取决于数据库驱动和 MySQL 配置,常见两类错误需分别捕获:
-
Lock wait timeout exceeded:典型 MySQL 超时错误,err.Error()包含该字符串,对应 MySQL 错误码1205(死锁)或1206(锁表过多),但更常见的是1205或超时触发的ER_LOCK_WAIT_TIMEOUT -
context deadline exceeded:如果你用了带超时的context.WithTimeout,GORM 查询会在上下文取消时返回此错误,和锁无关,但现象类似 -
NOWAIT模式下直接报错:Error 1205: Deadlock found when trying to get lock或Error 3572: Statement aborted because lock(s) could not be acquired immediately(MySQL 8.0.19+)
建议统一处理方式:
if strings.Contains(err.Error(), "Lock wait timeout") ||
strings.Contains(err.Error(), "Deadlock") ||
strings.Contains(err.Error(), "could not be acquired immediately") {
return &LockError{Original: err}
}
乐观锁重试逻辑要不要放在 GORM 层
不要。GORM 是数据访问层,重试属于业务控制流,混进去会污染模型逻辑、难以测试、且无法适配不同重试策略(如退避、熔断、降级)。
- 把乐观锁校验失败当作一个明确的业务错误返回,由上层 handler 或 usecase 决定是否重试、重试几次、间隔多久
- 避免在
db.Model().Updates()外包一层“自动重试”,因为重试前通常需要重新查最新状态(比如库存是否已售罄),不能只靠旧 version 值循环 - 若真要封装,也应是独立函数,接收
func() (bool, error)执行体,而非侵入 GORM 调用链
真正容易被忽略的一点:乐观锁只防“覆盖写”,不防“逻辑冲突”。比如两个请求都读到 stock=1,各自减 1 后都写回 0 —— 这看似成功,实则超卖。所以 version 校验必须搭配业务约束(如 stock > 0)一起用。











