SELECT ... FOR UPDATE只锁当前事务实际访问并满足WHERE条件的索引记录及间隙;若WHERE无索引则退化为表级锁;RR级别下无法关闭间隙锁,但可改用RC级别规避,需配合ROW格式binlog。

MySQL的SELECT ... FOR UPDATE到底锁什么
它不锁整张表,也不锁所有索引,只锁**当前事务实际访问并满足WHERE条件的索引记录(包括间隙)**。如果你的WHERE没走索引,InnoDB会退化为表级锁(更准确说是聚簇索引全扫描+行锁),并发性能断崖下跌。
实操建议:
- 务必确认
EXPLAIN显示type是range、ref或const,避免ALL或index - 复合索引要注意最左前缀匹配——
WHERE status = 1 AND user_id = 100,若索引是(user_id, status),那status字段无法走索引 - 主键查询最安全:
SELECT * FROM orders WHERE id = 123 FOR UPDATE,只锁单行,无间隙问题
为什么UPDATE ... WHERE有时不阻塞,但SELECT ... FOR UPDATE会阻塞
根本区别在于:普通UPDATE自带隐式加锁,但它的锁行为取决于执行计划;而SELECT ... FOR UPDATE是显式请求锁,且InnoDB会严格按当前读(current read)语义加锁,包括记录锁+间隙锁(gap lock)。
常见错误现象:
- 事务A执行
UPDATE t SET x=1 WHERE a=5(a无索引)→ 锁全表 → 事务B的SELECT ... FOR UPDATE WHERE a=10被阻塞 - 事务A执行
SELECT ... FOR UPDATE WHERE a=5(a有索引)→ 只锁a=5对应行及前后间隙 → 事务B查a=10完全不受影响 - 但若事务B执行
INSERT INTO t (a) VALUES (6),且a=5和a=10之间存在间隙(比如当前只有a=5和a=10两行),则可能被间隙锁阻塞
RR隔离级别下,SELECT ... FOR UPDATE的间隙锁怎么关
不能“关”,但可以规避。间隙锁是RR级别防止幻读的核心机制,禁用会导致一致性破坏。真正可控的是是否启用innodb_locks_unsafe_for_binlog(已废弃)或改用READ COMMITTED(RC)级别。
使用场景与权衡:
- RC级别下,
SELECT ... FOR UPDATE只加记录锁,不加间隙锁 → 幻读可能发生,但写冲突减少 - RC下
UPDATE语句只锁匹配到的行,不锁间隙 → 更适合高并发写入、业务能容忍短时间幻读的场景(如秒杀库存扣减) - 切RC前必须确认binlog格式为
ROW,否则主从不一致风险极高 - 别试图用
SELECT ... LOCK IN SHARE MODE替代——它同样有间隙锁,且不能升级为写锁
分布式事务里,MySQL锁对强一致性的实际约束边界
MySQL单实例的锁只能保证本地事务ACID,跨库、跨服务时,它不提供分布式协调能力。所谓“强一致性”在分布式系统中本质是妥协后的最终一致性,MySQL锁只是其中一环。
容易踩的坑:
- 在微服务中,仅靠
SELECT ... FOR UPDATE锁住订单表,不代表支付服务、库存服务也同步锁住了——它们各自数据库互不可见 - 两阶段提交(XA)理论上可行,但MySQL XA在崩溃恢复、超时处理上极难稳定,生产环境基本不用
- 真正落地的方案通常是:MySQL锁保本地数据正确性 + 分布式锁(如Redis RedLock)协调关键路径 + 最终补偿(定时对账)
- 注意
FOR UPDATE默认不等待,要加WAIT N(如FOR UPDATE WAIT 5),否则直接报Lock wait timeout exceeded
锁不是银弹,它只在你清楚知道数据边界、访问路径和失败模式的前提下才可靠。跨节点时,别指望一个FOR UPDATE能兜住全局。


















