mysql存储引擎如何实现行锁_mysql锁机制说明

3次阅读

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

mysql存储引擎如何实现行锁_mysql锁机制说明

InnoDB 的行锁到底锁的是什么

InnoDB 实现行锁,**不是直接锁住某一行数据本身**,而是通过在索引记录上加锁来实现的。这意味着:如果没有合适的索引,UPDATEdelete 语句可能退化为表锁(或间隙锁组合),而不是你期望的“只锁一行”。

常见错误现象: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 UNCOMMITTEDREAD 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 查看最近死锁详情,重点关注 TRANSACTIONLOCK WAIT 部分

MyISAM 完全不支持行锁,别误配

如果建表时没显式指定引擎,或配置中默认引擎是 MyISAM,那所有 DML 操作都只有表锁。哪怕写 SELECT ... FOR UPDATE,MySQL 也不会报错,但实际不会加任何行级锁,只会加表级读锁(LOCK TABLES ... READ)。

验证方式很简单:SHOW CREATE TABLE tbl_nameENGINE=InnoDB 是否存在;或者查 information_schema.TABLES 表的 ENGINE 列。

  • 线上环境务必确认存储引擎:DDL 脚本里显式写 ENGINE=InnoDB
  • 迁移老表时注意:ALTER TABLE t ENGINE=InnoDB 会重建表,期间锁表,需评估影响
  • 不要依赖客户端提示或文档描述,默认不是 InnoDB 就没有行锁能力

行锁的“行”字很误导人——它本质是索引项锁,不是数据行锁;真正难调的从来不是语法怎么写,而是索引设计是否匹配锁需求,以及事务边界是否清晰。一个没走索引的 FOR UPDATE,和一条 SELECT 1区别

text=ZqhQzanResources