InnoDB行锁实际锁的是索引记录而非数据行;主键/唯一索引等值查询加记录锁,普通索引等值查询同时锁二级索引和主键,范围查询加间隙锁+记录锁,无索引时退化为近似表锁。

InnoDB 的行锁到底锁的是什么
InnoDB 实现行锁,**不是直接锁住某一行数据本身**,而是通过在索引记录上加锁来实现的。这意味着:如果没有合适的索引,UPDATE 或 DELETE 语句可能退化为表锁(或间隙锁组合),而不是你期望的“只锁一行”。
常见错误现象:SELECT ... FOR UPDATE 在无索引字段上执行后,其他事务更新同表任意行都被阻塞——这说明实际加了表级意向锁+大量记录锁,而非行锁。
- 主键或唯一索引等值查询(如
WHERE id = 100)→ 加 **记录锁(Record Lock)** - 普通索引等值查询 → 同样加记录锁,但会同时在主键上加锁(聚簇索引锁定)
- 范围查询(如
WHERE age > 25)→ 加 **间隙锁(Gap Lock) + 记录锁**,防止幻读 - 无索引字段查询 → 优化器可能走全表扫描,对所有扫描到的记录加锁,效果接近表锁
为什么 SELECT FOR UPDATE 有时不生效
行锁只在事务中、且隔离级别 ≥ READ COMMITTED 时才起作用;但如果语句没走索引,或者被 MySQL 优化器判定为不可加锁(比如查询条件含函数、隐式类型转换),SELECT ... FOR UPDATE 可能不加任何行级锁,仅加意向锁。
典型场景:执行 SELECT * FROM user WHERE name = '张三' FOR UPDATE,但 name 字段没建索引 → InnoDB 无法精确定位记录,只能对聚集索引的每条记录尝试加锁,最终可能因锁冲突或死锁检测提前释放,看起来“没锁住”。
- 检查执行计划:
EXPLAIN确认是否用到索引,type字段不能是ALL - 避免隐式转换:比如
WHERE mobile = 13800138000(mobile 是 VARCHAR),会导致索引失效 - 注意隔离级别:
READ UNCOMMITTED和READ COMMITTED下不使用间隙锁,但幻读风险上升
REPEATABLE READ 下的间隙锁与死锁风险
MySQL 默认隔离级别 REPEATABLE READ 会启用间隙锁(Gap Lock),它锁住的是索引记录之间的“空隙”,用于阻止其他事务插入新记录,从而解决幻读。但这也带来更高死锁概率。
例如两个事务并发执行:INSERT INTO t VALUES (5) 和 SELECT * FROM t WHERE id BETWEEN 3 AND 7 FOR UPDATE,后者会锁住 (3,7) 这个间隙,前者尝试插入 5 就会被阻塞;若另一个事务也类似操作,极易触发死锁。
- 关闭间隙锁?不行 ——
innodb_locks_unsafe_for_binlog已废弃,且关闭会导致主从不一致 - 降低风险:尽量用等值查询代替范围;控制事务粒度,避免长事务持有间隙锁
- 监控死锁:
SHOW ENGINE INNODB STATUS查看最近死锁详情,重点关注TRANSACTION和LOCK WAIT部分
MyISAM 完全不支持行锁,别误配
如果建表时没显式指定引擎,或配置中默认引擎是 MyISAM,那所有 DML 操作都只有表锁。哪怕写 SELECT ... FOR UPDATE,MySQL 也不会报错,但实际不会加任何行级锁,只会加表级读锁(LOCK TABLES ... READ)。
验证方式很简单:SHOW CREATE TABLE tbl_name 看 ENGINE=InnoDB 是否存在;或者查 information_schema.TABLES 表的 ENGINE 列。
- 线上环境务必确认存储引擎:DDL 脚本里显式写
ENGINE=InnoDB - 迁移老表时注意:
ALTER TABLE t ENGINE=InnoDB会重建表,期间锁表,需评估影响 - 不要依赖客户端提示或文档描述,默认不是 InnoDB 就没有行锁能力
行锁的“行”字很误导人——它本质是索引项锁,不是数据行锁;真正难调的从来不是语法怎么写,而是索引设计是否匹配锁需求,以及事务边界是否清晰。一个没走索引的 FOR UPDATE,和一条 SELECT 1 没区别。










