select for update不生效的主因是autocommit=1导致锁瞬时释放;必须显式开启事务(start transaction)、确保where走索引、注意rr级间隙锁影响,且仅在需依赖查询结果做条件更新时才前置使用。

SELECT FOR UPDATE 不生效,大概率是 autocommit=1
MySQL 默认 autocommit=1,此时 SELECT ... FOR UPDATE 执行完立刻提交,锁瞬间释放——根本锁不住行。你看到“没加锁”,其实是锁早没了。
必须先确认并关闭自动提交:
- 查当前状态:
SELECT @@autocommit;,返回0才安全 - 若为
1,执行:SET autocommit = 0;或更推荐的START TRANSACTION; - 后续所有操作(包括
SELECT ... FOR UPDATE和UPDATE)必须在同一个事务内完成 - 最后别忘了
COMMIT;或ROLLBACK;,漏掉会导致长事务和锁堆积
WHERE 条件不走索引,SELECT FOR UPDATE 就等于锁全表
InnoDB 行锁依赖索引,没索引就只能扫表,然后给每行都加 X 锁——效果和表锁无异,并发直接崩。
检查是否真走索引:
- 用
EXPLAIN SELECT ... FOR UPDATE看type字段:要是ALL,就是全表扫描 -
key字段必须显示实际使用的索引名,比如PRIMARY或idx_user_name - 主键、唯一索引、普通索引都行;但联合索引要满足最左前缀,
WHERE status = ?对索引(user_id, status)是有效的,对(status, user_id)就不行 - 避免在 WHERE 中对字段做函数操作,比如
WHERE YEAR(created_at) = 2024会失效索引
RR 隔离级别下,SELECT FOR UPDATE 会悄悄锁住间隙
MySQL 默认隔离级别是 REPEATABLE READ,这时 SELECT ... FOR UPDATE 不仅锁匹配的行,还锁住索引间隙(Gap Lock),防止幻读——但这也意味着看似无关的插入会被阻塞。
典型表现:
- 表里有
id = 1, 5, 10,执行SELECT * FROM t WHERE id BETWEEN 3 AND 7 FOR UPDATE - 会锁住间隙
(1,5)和(5,10),INSERT INTO t (id) VALUES (4)或(6)就会被卡住 - 如果业务不需要防幻读(比如纯更新已存在记录),可临时切到
READ COMMITTED:SET TRANSACTION ISOLATION LEVEL READ COMMITTED;,此时只锁行,不锁间隙
UPDATE 前到底要不要先 SELECT FOR UPDATE?
不是所有 UPDATE 都需要前置 SELECT。InnoDB 在执行 UPDATE 时,只要 WHERE 条件命中索引,就会自动加行锁——这时候多一次 SELECT ... FOR UPDATE 只是白耗一次查询和锁获取。
真正需要先查后锁的场景只有两个:
- 业务逻辑依赖读出的旧值做判断,比如库存扣减前要检查
stock >= 1,这个判断必须和锁绑定在同一事务中 - 需要读取多个字段参与计算(如
balance和version),且后续 UPDATE 要基于这些值做原子更新 - 否则,直接写带条件的 UPDATE 更高效:
UPDATE products SET stock = stock - 1 WHERE id = 123 AND stock >= 1
间隙锁、锁升级、事务生命周期——这些细节一旦漏掉一个,SELECT FOR UPDATE 就从保护伞变成定时炸弹。











