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');,而 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+)验证锁的实际行为。
