select for update 必须在事务中执行,否则锁立即失效;gorm 的 forupdate() 仅拼sql,需配合事务使用,且依赖索引避免表锁,mysql 8.0+ 支持 nowait/wait 但需手动注入。

SELECT FOR UPDATE 必须在事务中执行
直接调用 db.First(&user).Select("id, balance").Where("id = ?", 1).ForUpdate().Error 不会生效——GORM 的 ForUpdate() 只是拼 SQL,真正加锁依赖事务上下文。一旦不在事务里,语句执行完连接就释放,锁立刻失效。
正确做法是显式开启事务,并在事务内完成查询+更新:
- 用
db.Transaction(func(tx *gorm.DB) error { ... })包裹逻辑 - 或手动
tx := db.Begin()→ 操作 →tx.Commit()/tx.Rollback() - 注意:事务未提交前,其他事务对同一行执行
SELECT ... FOR UPDATE会阻塞,直到超时(默认由innodb_lock_wait_timeout控制,通常 50 秒)
GORM 的 ForUpdate() 默认不加 WAIT / NOWAIT
MySQL 8.0+ 支持 FOR UPDATE WAIT n 和 FOR UPDATE NOWAIT,但 GORM 原生 ForUpdate() 不带参数,生成的 SQL 是裸的 SELECT ... FOR UPDATE,行为等同于 WAIT 0(即无限等待),容易导致请求堆积。
如需控制等待行为,得手写 SQL 或用 Session() 注入:
db.Session(&gorm.Session{PrepareStmt: true}).Raw("SELECT * FROM users WHERE id = ? FOR UPDATE NOWAIT", 1).Scan(&user)- 或用
db.Clauses(clause.Locking{Strength: "UPDATE", Options: clause.LockingOptions{Nowait: true}})(GORM v1.24+) - 避免在高并发接口里无限制等待,建议搭配重试或降级逻辑
ForUpdate() 对索引和 WHERE 条件敏感
没走索引的 WHERE 条件,InnoDB 可能升级为表锁或锁住更多行,极大降低并发能力。比如 SELECT * FROM orders WHERE status = 'pending' FOR UPDATE,若 status 无索引,整张表都可能被锁住。
确保锁定语句满足以下条件:
- WHERE 中的字段必须有索引(最好是联合索引覆盖查询条件)
- 避免
LIKE '%xxx'、函数包裹字段(如DATE(created_at))等导致索引失效的操作 - 用
EXPLAIN验证执行计划是否命中索引和只锁目标行
别混淆 ForUpdate 和 Locking Clauses 的语义
GORM 提供了多个锁相关方法,但含义不同:ForUpdate() 是排他锁(X 锁),而 ForShare() 是共享锁(S 锁),Clones() 或 Joins() 中误用会导致锁冲突或死锁。
常见误区:
- 在读多写少场景下错用
ForUpdate(),造成不必要的写阻塞 - 在同一个事务里对同一行先后调用
ForUpdate()和ForShare(),触发死锁(X 锁与 S 锁互斥) - 跨表 JOIN 查询时未明确指定锁表,InnoDB 可能对驱动表或被驱动表加锁策略不一致
真正需要强一致性写操作时才用 ForUpdate();读操作优先考虑快照读(MVCC)或 ForShare(),锁粒度和时机比“有没有锁”更关键。











