GORM中SELECT...FOR UPDATE不自动生效,因默认查询不开启事务且未显式声明锁;必须用db.Transaction包裹,并配合Clauses(clause.Locking{Strength:"UPDATE"})或Raw在事务内执行,否则锁无效。

为什么 SELECT ... FOR UPDATE 在 GORM 中不自动生效
GORM 默认执行的 Find、First 等查询不会加锁,即使你写了 SELECT ... FOR UPDATE 原生 SQL,也得显式调用 Session 或 Raw 并确保事务开启。常见错误是:只在普通查询里拼 SQL,却没进事务,结果锁根本没生效,还误以为“已加锁”。
真正起作用的锁必须满足两个条件:事务未提交 + 查询带锁提示(如 FOR UPDATE)。GORM 的链式调用本身不开启事务,Session(&gorm.Session{PrepareStmt: true}) 也不等于事务。
- 必须用
db.Transaction(func(tx *gorm.DB) error { ... })包裹锁查询逻辑 - 锁语句需通过
tx.Raw(...).Scan()或tx.Clauses(clause.Locking{Strength: "UPDATE"}).First()显式声明 - MySQL 要求表有主键或唯一索引,否则可能升级为表级锁;PostgreSQL 对索引要求宽松些
用 Clauses(clause.Locking{...}) 实现行级锁(推荐)
这是 GORM v2+ 官方支持的声明式锁方式,兼容 MySQL、PostgreSQL、SQL Server,语义清晰且能复用 GORM 模型映射。但注意它只对 First、Take、Find 等读操作有效,不能用于 Create 或 Save。
示例:对用户余额扣减前加写锁
立即学习“go语言免费学习笔记(深入)”;
err := db.Transaction(func(tx *gorm.DB) error {
var user User
// 加 FOR UPDATE 锁,跳过被锁住的行(避免阻塞)
if err := tx.Clauses(clause.Locking{
Strength: "UPDATE",
Options: "SKIP LOCKED",
}).Where("id = ? AND balance >= ?", userID, amount).First(&user).Error; err != nil {
return err
}
user.Balance -= amount
return tx.Save(&user).Error
})
-
Strength: "UPDATE"对应 MySQL 的FOR UPDATE,PostgreSQL 的FOR UPDATE -
Options: "SKIP LOCKED"很关键——防止并发时因等待锁而超时;但 MySQL 8.0+ 和 PostgreSQL 9.5+ 才支持 - 若去掉
SKIP LOCKED,遇到锁冲突会一直阻塞,直到lock_wait_timeout(MySQL 默认 50 秒)
原生 Raw 查询加锁的注意事项
当需要更精细控制(比如联合查询加锁、指定索引提示),就得用 Raw。但这里容易漏掉两件事:事务上下文丢失、Scan 目标结构体字段名不匹配。
典型错误写法:db.Raw("SELECT * FROM users WHERE id = ? FOR UPDATE", id).Scan(&u) —— 这不在事务中,锁无效。
- 必须用事务对象调用:
tx.Raw(...).Scan(&u) - 如果结构体字段名和数据库列名不一致(比如 DB 是
user_name,Go 是UserName),Scan不会自动映射,得用map[string]interface{}或显式别名:SELECT id AS ID, user_name AS UserName FROM ... - MySQL 中,
FOR UPDATE在非唯一条件上可能锁住多个间隙(gap lock),引发意外阻塞;建议 WHERE 条件尽量走主键或唯一索引
乐观锁 vs 悲观锁:什么时候不该用 FOR UPDATE
不是所有并发场景都适合悲观锁。比如高并发抢券、秒杀,若大量请求卡在 FOR UPDATE 等待,数据库连接池可能被耗尽,反而拖垮服务。
- 乐观锁更适合低冲突、高吞吐场景:给模型加
Version字段,用db.Where("id = ? AND version = ?", id, oldVer).Update(...),失败则重试 - 悲观锁适合短事务、强一致性要求、冲突概率高的场景(如库存预占、订单状态严格串行更新)
- GORM 的
Cluases锁无法跨事务复用,每次都要开新事务;若业务逻辑复杂、涉及多表多步,要考虑锁粒度是否过大
实际部署时,MySQL 的 innodb_lock_wait_timeout 和连接池最大空闲时间要对齐,否则应用层等不到锁就超时,但数据库还在持锁,容易引发雪崩。


















