SELECT FOR UPDATE没生效的根本原因是autocommit=1导致事务未开启,锁立即释放;需先SET autocommit=0或START TRANSACTION,确保WHERE走索引、避免间隙锁误用,且UPDATE本身已自动加锁无需FOR UPDATE。

SELECT FOR UPDATE 没生效,大概率是 autocommit=1
根本原因不是语法写错,而是事务根本没开启——MySQL 默认 autocommit=1,每条语句执行完立刻提交,锁也跟着释放。你看到“没锁住”,其实是锁压根没来得及起作用。
验证方法:执行 SELECT @@autocommit;,返回 1 就是问题所在。
- 临时修复:先执行
SET autocommit = 0;,再执行START TRANSACTION;,然后才是SELECT ... FOR UPDATE - 更稳妥做法:统一用
START TRANSACTION;开头,不用依赖SET autocommit—— 因为会话级设置可能被连接池重置 - 漏掉
COMMIT或ROLLBACK会导致锁长期悬挂,查INFORMATION_SCHEMA.INNODB_TRX能发现“运行中但无查询”的事务
WHERE 条件没走索引,等于锁全表
SELECT ... FOR UPDATE 的锁粒度完全由查询是否命中索引决定。没索引,InnoDB 只能全表扫描,给所有扫描行加 X 锁——看起来像“没锁住某行”,实际是锁了太多行,甚至阻塞无关更新。
检查方式:在语句前加 EXPLAIN,重点看三列:
-
type字段不能是ALL或index(全表/全索引扫描);理想值是const(主键等值)或ref(普通索引等值) -
key字段必须显示具体索引名,比如PRIMARY或idx_name -
rows应接近你要锁的行数;若显示几百上千,说明锁范围失控
常见踩坑:WHERE DATE(created_at) = '2024-01-01'(函数导致索引失效)、WHERE user_id = '123'(隐式类型转换)、WHERE status = 1 对联合索引 (user_id, status) 无效(最左前缀不匹配)
查不到记录时,间隙锁仍在生效
很多人以为“没查到数据就没锁”,但在默认 REPEATABLE READ 隔离级别下,SELECT ... FOR UPDATE 即使没命中任何行,也会加间隙锁(Gap Lock)。比如 SELECT * FROM t WHERE name = 'Alice' FOR UPDATE,若 Alice 不存在,InnoDB 会锁定 'Alice' 应该插入的位置——这会阻塞其他事务往这个间隙插数据。
- 这种锁不会出现在
INNODB_TRX的trx_query中,但能通过INNODB_LOCK_WAITS观察到阻塞现象 - 想禁用间隙锁?只能临时切隔离级别:
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED,但会失去幻读防护 -
LIMIT 1不影响锁范围——没索引时仍会全表扫描并锁所有扫描行
UPDATE 本身就会加锁,不需要 FOR UPDATE
一个常见误解是“只有 SELECT ... FOR UPDATE 才加锁”。实际上,UPDATE 和 DELETE 语句天然加排他锁(X Lock),根本不需要手动加锁。
-
UPDATE product SET stock = stock - 1 WHERE id = 1执行时,InnoDB 自动对id = 1这行加 X 锁 - 另一个事务执行相同
UPDATE会被阻塞,直到前一个事务提交或回滚 - 真正需要
SELECT ... FOR UPDATE的场景只有两个:① 业务逻辑依赖读出的旧值做判断(如“余额 > 100 才扣款”),且判断无法下推到UPDATE WHERE;② 多步操作需中间状态不被干扰(如查库存 → 查优惠券 → 统一扣减)
复杂点在于:锁的粒度、间隙锁行为、以及事务边界是否清晰——这些细节不显眼,但一旦出错,排查成本远高于写代码本身。


















