SELECT FOR UPDATE行级锁必须在事务中使用Clauses声明,且WHERE条件须命中索引;否则会升级为表锁或失效。

事务里加 FOR UPDATE 行级锁必须用 Clauses,不能拼 SQL
直接在 Where 后面写 "FOR UPDATE" 字符串,或者用 Raw 执行自定义 SQL,GORM 不会识别为锁语义,MySQL 也收不到锁指令。正确方式是通过 Clauses(clause.Locking{Strength: clause.LockingStrengthUpdate}) 声明。
常见错误现象:SELECT * FROM users WHERE id = 1 FOR UPDATE 在裸查或非事务中执行不报错,但锁立刻释放;在事务中用字符串拼接,生成的 SQL 缺少锁关键词,等于白加。
-
Strength: "UPDATE"是唯一有效值,"X"、"EXCLUSIVE"等都会静默失效 - 必须搭配
First、Find、Take等查询方法,Count或Pluck不触发锁 - 锁只作用于 WHERE 条件命中的行,没
WHERE会锁全表(危险!)
WHERE 条件必须走索引,否则可能升级成表锁
MySQL 的 InnoDB 在无法用索引精确定位时,会扫描聚簇索引并给扫描范围加锁,极端情况锁住整个主键区间甚至整张表。这不是 GORM 的问题,而是 MySQL 底层行为。
使用场景:比如核销优惠券前查 status = 'unused' AND code = ?,如果 code 没建索引,哪怕只查一条,也可能锁住所有 status = 'unused' 的行。
- 用
EXPLAIN确认执行计划是否命中索引(type字段至少为ref或更优) - 避免在 WHERE 中对字段用函数,如
WHERE DATE(created_at) = '2026-10-19' - 复合索引要注意最左匹配原则,
WHERE status = ?单独查,即使有(status, code)索引也无效
共享锁 LOCK IN SHARE MODE 要用 Strength: "SHARE"
共享锁允许并发读,但阻止其他事务写。GORM 中唯一合法写法是 Clauses(clause.Locking{Strength: clause.LockingStrengthShared}),不是 "SHARED" 也不是 "READ" —— 这两个值会被忽略,生成的 SQL 没锁。
典型场景:订单支付前校验库存是否充足,只读不改,但要防别人同时扣减同一库存。
- 必须在事务中使用,否则锁无意义(MySQL 自动提交模式下锁立即释放)
- 和
FOR UPDATE一样,依赖索引;不走索引时同样可能锁升级 - 注意与
FOR UPDATE的兼容性:SHARE锁和UPDATE锁互斥,但多个SHARE锁可共存
死锁风险高发点:加锁顺序不一致 + 长事务
两个 goroutine 分别按不同顺序锁定多行,比如 A 先锁 ID=1 再锁 ID=2,B 反过来先锁 ID=2 再锁 ID=1,就容易触发 MySQL 死锁检测并回滚其中一个事务。
性能影响明显:锁等待超时默认是 50 秒(innodb_lock_wait_timeout),线上业务等这么久基本等于失败。
- 固定加锁顺序,例如总是按 ID 升序查:
WHERE id IN (1, 2)改成WHERE id IN (1, 2) ORDER BY id - 把锁粒度控制在最小必要范围,不要在事务开头就查一堆数据再慢慢处理
- 捕获
ErrDeadlock并重试(GORM 不自动重试,需手动实现)
实际中最容易被忽略的是锁的“生效前提”:它既依赖 GORM 的正确调用方式,也强依赖 MySQL 的索引设计和事务隔离级别。写对了 Clauses,但没建好索引,等于锁了个寂寞。











