mysql行锁由执行计划和索引使用决定,update/delete带where且命中索引才加行锁;select for update显式加锁;insert因唯一性检查也触发行锁;索引失效会导致锁升级为表锁。

UPDATE 或 DELETE 语句带 WHERE 条件且命中索引时会加行锁
MySQL 的行锁(Record Lock)不是由语法决定的,而是由执行计划和索引使用情况共同触发的。只有当 UPDATE 或 DELETE 语句能通过索引精确定位到某几行(即走索引扫描,而非全表扫描),InnoDB 才会对这些实际访问的记录加行锁。
常见错误现象:明明写了 WHERE id = 100,却锁了整张表——大概率是 id 列没建索引,或索引失效(比如对 id 做了函数操作:WHERE ABS(id) = 100)。
- 必须使用支持行锁的存储引擎(仅
InnoDB,MyISAM只有表锁) - WHERE 条件字段需有有效索引,且优化器实际选用该索引(可用
EXPLAIN验证) - 主键或唯一索引上的等值查询,只锁匹配行;普通索引等值查询,还会额外加
Gap Lock或Next-Key Lock
SELECT ... FOR UPDATE / LOCK IN SHARE MODE 显式加行锁的典型用法
这是应用层主动控制并发的核心手段,常用于“读-改-写”场景,避免脏读或更新丢失。例如秒杀扣库存、账户余额转账前校验。
注意:即使 SELECT 本身不修改数据,加上 FOR UPDATE 后,只要事务未提交,其他事务对同一行执行 UPDATE 或 SELECT ... FOR UPDATE 就会被阻塞。
-
SELECT * FROM order WHERE order_no = 'ORD123' FOR UPDATE;—— 锁住匹配的订单行 - 若
order_no是普通索引(非唯一),该语句可能锁住间隙,防止幻读 - 在可重复读(
REPEATABLE READ)隔离级别下,FOR UPDATE默认使用Next-Key Lock(行锁 + 间隙锁) - 如果只想锁记录不锁间隙,可改用
SELECT ... FOR UPDATE NOWAIT(MySQL 8.0+)或搭配READ COMMITTED隔离级别
INSERT 语句也会触发行锁:不只是“插入”,还有“检查唯一性”
很多人忽略 INSERT 的锁行为——它不仅锁自己要插入的记录,还会在唯一索引上加 Insert Intention Lock,并在插入位置的间隙上加锁,防止其他事务同时插入相同值或冲突值。
典型问题:两个事务几乎同时执行 INSERT INTO user (email) VALUES ('a@b.com');,而 email 是唯一索引。其中一个会成功,另一个报错 Deadlock found when trying to get lock 或 Duplicate entry,但背后其实是行锁等待或死锁检测介入。
- 唯一约束冲突检测过程需要扫描索引定位是否存在相同值,这个扫描过程会加锁
- 即使插入失败(如唯一键冲突),InnoDB 仍可能已持有部分锁,影响其他并发操作
- 批量
INSERT ... ON DUPLICATE KEY UPDATE中,所有涉及的唯一键路径都会被扫描并加锁,范围可能比预期大
行锁不是万能的:容易被绕过或升级成表锁的几种情况
行锁生效的前提是“能精准定位到行”。一旦优化器放弃索引、或者锁粒度扩大,就可能退化为表级锁或锁住远超预期的范围。
最隐蔽的问题是“隐式类型转换”导致索引失效:比如 user_id 是 BIGINT,但 SQL 写成 WHERE user_id = '123'(字符串),MySQL 会转成 WHERE CAST(user_id AS CHAR) = '123',无法使用索引,最终锁全表。
- WHERE 条件中对索引列使用函数、表达式、不同数据类型比较 → 索引失效 → 行锁失效
- 事务中先
SELECT ... FOR UPDATE无索引条件 → 锁全表(或所有索引树叶子节点) - 大范围扫描(如
WHERE create_time > '2020-01-01'且该字段无索引)→ 锁所有扫描过的行,甚至演变为锁整个聚簇索引 - 大量行锁占用内存后,InnoDB 可能触发锁升级(虽然官方说不支持锁升级,但高并发下锁管理开销剧增,表现类似)
行锁的边界很依赖执行计划,光看 SQL 写法远远不够;真正上线前,务必用 EXPLAIN 和 INFORMATION_SCHEMA.INNODB_TRX + INNODB_LOCKS(MySQL 5.7)或 performance_schema.data_locks(8.0+)验证锁的实际行为。










