select for update 仅在 repeatable read 和 read committed 隔离级别下生效:前者加 next-key lock(行锁+间隙锁)防幻读,后者仅锁命中行且语句结束即释放。

SELECT FOR UPDATE 在什么事务隔离级别下才真正起作用
它只在 REPEATABLE READ 和 READ COMMITTED 隔离级别下生效,但行为差异极大。在 READ COMMITTED 下,SELECT FOR UPDATE 只锁住实际命中的行(且是当前读的快照版本),语句执行完就释放;而在 REPEATABLE READ 下,它会加 next-key lock(行锁 + 间隙锁),可能锁住不存在的“间隙”,防止幻读——这也是线上死锁最常发生的根源。
常见错误现象:Deadlock found when trying to get lock,尤其在高并发插入或范围查询时突然爆发。
- 生产环境默认用
REPEATABLE READ,别假设“我只是查一行就不会锁别的” - 如果业务允许,把隔离级别临时降到
READ COMMITTED(需确认无幻读风险),能显著减少锁范围 - 用
SELECT ... FOR UPDATE NOWAIT(MySQL 8.0.1+)或SELECT ... FOR UPDATE SKIP LOCKED避免阻塞,但要注意业务逻辑是否容错
哪些 SELECT FOR UPDATE 语句根本没加上行锁
不是写了 SELECT ... FOR UPDATE 就一定锁行。MySQL 优化器会跳过加锁:比如没有走索引、用了函数包装字段、或者 WHERE 条件里出现隐式类型转换。
典型场景:对 user_id(INT)字段执行 SELECT * FROM users WHERE user_id = '123' FOR UPDATE,字符串和整数比较触发隐式转换,导致索引失效,最终全表扫描 + 表级锁(或大量行锁),性能雪崩。
- 务必确保 WHERE 条件中的列使用了有效索引,且类型完全匹配
- 用
EXPLAIN看type是否为const/ref,key显示实际使用的索引名 -
SELECT ... FOR UPDATE对NULL值列加锁效果不确定,避免在 WHERE 中用IS NULL或IS NOT NULL作为主要条件
UPDATE 之前先 SELECT FOR UPDATE,真的必要吗
多数情况下不必要,甚至有害。MySQL 的 UPDATE 语句本身就会在更新前对目标行加 X 锁(排他锁),且是原子的。多一次 SELECT FOR UPDATE 不仅增加一次网络往返,还延长了锁持有时间,提高冲突概率。
只有两种情况值得额外加锁:
– 需要基于当前值做复杂判断(比如“余额 > 100 才扣款”),且该判断不能直接写进 UPDATE 的 WHERE 子句中;
– 多条语句组成逻辑事务,中间需要确保数据不被其他事务修改(例如先查库存,再查优惠券状态,最后统一扣减)。
- 能写成单条
UPDATE ... WHERE balance > 100的,就别拆成两步 - 如果必须分步,记得开启显式事务(
BEGIN),并在最后COMMIT前完成所有操作 - 不要在应用层缓存
SELECT FOR UPDATE结果后延迟执行 UPDATE——锁早释放了,缓存的数据可能已过期
如何验证某行到底被谁锁住了
靠猜没用,得看 information_schema 里的实时锁视图。MySQL 5.7+ 提供 INNODB_TRX、INNODB_LOCKS(8.0 已移除)、INNODB_LOCK_WAITS,但更直接的是查 performance_schema.data_locks(8.0.1+)。
常用诊断组合:
SELECT ENGINE_TRANSACTION_ID, INDEX_NAME, LOCK_TYPE, LOCK_MODE, LOCK_DATA FROM performance_schema.data_locks WHERE OBJECT_SCHEMA = 'your_db' AND OBJECT_NAME = 'your_table';
配合 SELECT * FROM information_schema.INNODB_TRX 查正在运行的事务,就能定位到哪个 TRX_ID 持有哪行锁。
- 注意
LOCK_DATA显示的是被锁记录的索引字段值(不是主键值!),如果是二级索引,得结合INDEX_NAME理解 - 如果看到大量
LOCK_MODE是RECORD+LOCK_TYPE是RECORD,说明是行锁;如果是LOCK_MODE含GAP,就是间隙锁——这时候即使 WHERE 条件查不到数据,也可能阻塞插入 - 开发环境可以故意卡住一个事务不提交,用上述 SQL 实时观察,比看文档直观得多
锁的边界从来不在语法表面,而在索引结构、事务生命周期和隔离级别的交界处。没查执行计划就写 FOR UPDATE,和没关保险就按扳机差不多。










