必须用db.Begin()或db.Transaction()显式开启事务,否则SELECT FOR UPDATE在GORM中不生效;锁范围取决于WHERE是否命中索引,无索引将升级为表锁;需捕获死锁错误并重试。

事务里不显式 Begin,SELECT FOR UPDATE 就是摆设
裸调 db.Clauses(...).First() 或直接拼 SQL 执行 SELECT ... FOR UPDATE,在 GORM 里根本不会加锁——因为 MySQL 只在显式事务中才识别 FOR UPDATE,而 GORM 默认的查询都是自动提交模式(autocommit=1)。此时语句看似执行成功,实则锁被数据库静默忽略,后续 UPDATE 会直接覆盖别人的数据。
必须用 db.Transaction() 或 db.Begin() 显式开启事务,再在事务对象上做带锁查询:
err := db.Transaction(func(tx *gorm.DB) error {
var user User
// ✅ 正确:锁在 tx 上生效
if err := tx.Clauses(clause.Locking{
Strength: clause.LockingStrengthUpdate,
}).Where("id = ?", 123).First(&user).Error; err != nil {
return err
}
user.Status = "processing"
return tx.Save(&user).Error
})
- 不用
Transaction而用Begin也行,但得手动Commit/Rollback,容易漏 -
Clause.Locking是 GORM v2+ 的写法;v1 里得手拼"SELECT * FROM users WHERE id = ? FOR UPDATE",且必须确保在事务内执行 - MySQL 在
autocommit=0下才认FOR UPDATE,GORM 的Transaction内部已帮你设好,别自己去改全局autocommit
WHERE 条件没走索引,SELECT FOR UPDATE 可能锁全表
加锁范围不是由语句写的“FOR UPDATE”决定的,而是由 MySQL 执行计划决定的。如果 WHERE 条件字段没索引,InnoDB 无法精确定位行,就会升级为表级锁(尤其在 REPEATABLE READ 隔离级别下)。
比如这张表:
CREATE TABLE `users` ( `id` int PRIMARY KEY, `name` varchar(255), `email` varchar(255) );
-
tx.Where("id = ?", 123).Clauses(...).First(&u)→ 锁单行(id是主键,肯定走索引) -
tx.Where("email = ?", "a@b.c").Clauses(...).First(&u)→ 若email无索引,MySQL 可能扫全表并锁所有扫描过的记录,甚至锁整个聚簇索引 -
tx.Where("name LIKE 'john%'", ...).First(&u)→ 即使name有索引,范围查询也可能触发间隙锁(gap lock),扩大锁定范围
查执行计划:EXPLAIN SELECT * FROM users WHERE email = 'a@b.c' FOR UPDATE;,确认 type 是 const 或 ref,而不是 ALL。
GORM 的 LockingStrength 不是所有数据库都支持
clause.LockingStrengthUpdate 在 MySQL 和 PostgreSQL 都可用,但语义不同:
- MySQL:等价于
SELECT ... FOR UPDATE,默认加记录锁 + 间隙锁 - PostgreSQL:只锁匹配行,不加间隙锁;若想避免锁升级,可换用
clause.LockingStrengthNoKeyUpdate(对应FOR NO KEY UPDATE) - SQLite、SQL Server 等不支持该 Clause,会忽略或报错
跨数据库项目慎用,或者封装前先判断方言:
if db.Dialector.Name() == "mysql" {
tx = tx.Clauses(clause.Locking{Strength: clause.LockingStrengthUpdate})
}
另外,LockingStrengthShared(共享锁)在 MySQL 中实际是 SELECT ... LOCK IN SHARE MODE,它允许其他事务读,但阻塞写和更新锁,适合“只读校验后交由下游处理”的场景。
死锁不是异常,是并发常态,必须重试
两个事务交叉加锁(比如 A 先锁 ID=1 再锁 ID=2,B 反过来),MySQL 会主动检测并回滚其中一个事务,抛出错误:ERROR 1213 (40001): Deadlock found when trying to get lock。GORM 不会自动重试,你得自己兜底。
- 不要捕获泛型
error后直接返回,要检查错误是否含"Deadlock"关键字 - 重试次数建议 ≤ 3 次,每次加随机小延迟(如
time.Sleep(time.Millisecond * time.Duration(rand.Intn(50)))),避免雪崩 - 固定加锁顺序能大幅降低死锁概率:比如始终按
user_id升序加锁,而不是按参数传入顺序
最简重试模板:
FOR UPDATE











