mysql锁机制本质是索引上的行锁,rr级别下默认使用临键锁防幻读,rc级别禁用间隙锁仅用记录锁;无索引查询会退化为全表行锁,mdl锁隐式阻塞ddl,死锁是正常机制需按序访问数据规避。

MySQL 在高并发场景下,锁机制不是“要不要加”的选择题,而是“加什么锁、加在哪、什么时候释放”的系统性工程。核心目标只有一个:在保障数据一致性的前提下,尽可能提升并发吞吐。InnoDB 引擎的锁设计围绕索引展开,脱离索引谈行锁就是空中楼阁;锁行为又深度绑定事务隔离级别,RR 和 RC 下的锁表现截然不同——理解这两点,才能真正用好锁。
行锁不是锁“数据行”,而是锁“索引记录”
InnoDB 的行级锁实际加在聚簇索引或二级索引的叶子节点上。没有索引的 WHERE 条件?那会退化为全表扫描 + 每条匹配记录都加 X 锁,甚至触发锁升级风险。
- 主键查询(如 WHERE id = 5)→ 精确锁定对应聚簇索引记录,最高效
- 唯一索引查询(如 WHERE email = 'a@b.com')→ 锁定该唯一索引项及对应主键记录
- 普通索引查询(如 WHERE status = 1)→ 锁定所有满足条件的二级索引项,以及它们回表查到的每一条主键记录
- 无索引字段查询(如 WHERE remark LIKE '%test%')→ 全表扫描,对所有被扫描的行加锁,极易引发锁争用
间隙锁与临键锁:RR 级别防幻读的双刃剑
可重复读(RR)下,InnoDB 不仅锁记录,还锁住记录之间的“空隙”。比如 age 字段有值 21、23、26、30,执行 SELECT * FROM t_user WHERE age BETWEEN 22 AND 25 FOR UPDATE,实际会锁住 (21,23)、(23,26) 这两个间隙,防止其他事务插入 age=22 或 24 的新行。
- 间隙锁(Gap Lock)本身不锁记录,只锁范围,不阻塞快照读,但会阻塞其他事务在该范围内插入新记录
- 临键锁(Next-Key Lock)= 记录锁 + 间隙锁,是 RR 下的默认行为,用于彻底解决幻读
- RC 隔离级别下,间隙锁被禁用,只保留记录锁,因此不会阻塞范围内的 INSERT,但存在幻读可能
- 唯一等值查询(带唯一索引或主键)时,InnoDB 会优化掉间隙锁,只加记录锁,这是重要性能优化点
元数据锁(MDL):常被忽视的隐性阻塞源
你没显式加锁,但 SELECT 语句一执行,MySQL 就自动给表加了 MDL 读锁;此时若有人在另一会话执行 ALTER TABLE,就会卡在等待 MDL 写锁上——而这个写锁又会阻塞后续所有新的 SELECT,形成雪崩式阻塞。
- MDL 锁在语句开始执行时加,在语句结束(非事务提交)时释放
- 长事务中的慢查询会让 MDL 读锁长期持有,成为 DDL 操作的瓶颈
- 线上禁止在业务高峰期执行 DDL;必要时可用 pt-online-schema-change 等工具规避
- 可通过 show processlist 查看状态为 Waiting for table metadata lock 的线程定位问题
死锁不是异常,而是并发系统的常态
InnoDB 每次加锁前都会做死锁检测(wait-for graph 算法),一旦发现环路,立即回滚其中一个事务(通常选 undo log 小的),返回 ERROR 1213。这不是 bug,是机制设计的一部分。
- 典型死锁场景:事务 A 先更新 id=1,再更新 id=2;事务 B 反向操作。双方互相等待对方释放锁
- 避免关键:按固定顺序访问多行数据(如总是按主键升序更新)、缩短事务时间、减少锁持有范围
- 高频更新场景可考虑将多个小更新合并为单条 UPDATE ... CASE,减少锁数量
- 监控死锁:开启 innodb_print_all_deadlocks = ON,日志中会完整记录每次死锁的参与事务和 SQL
锁机制本身不复杂,但它的表现高度依赖索引设计、SQL 写法和事务边界。与其被动排查锁等待,不如在建表时规划好索引,在写 SQL 时明确意图,在启事务时控制粒度——这才是高并发下稳住数据库的底层逻辑。











