MySQL事务中触发行锁的操作包括UPDATE、DELETE、SELECT ... FOR UPDATE和SELECT ... LOCK IN SHARE MODE,前提是WHERE条件命中索引;否则可能升级为表锁或间隙锁。

MySQL 事务中哪些操作会触发行锁?
InnoDB 默认在 UPDATE、DELETE、SELECT ... FOR UPDATE 和 SELECT ... LOCK IN SHARE MODE 语句执行时加行锁,但前提是 WHERE 条件能命中索引。如果走全表扫描,就会升级为表锁或大量间隙锁,大幅增加死锁概率。
常见误区是认为“只要加了主键条件就一定只锁一行”——实际还要看执行计划:EXPLAIN 显示 type 是 const 或 ref 才相对安全;若出现 range 或 index,可能锁住多行甚至间隙。
- 唯一索引等值查询(如
WHERE id = 100)→ 只锁匹配的那行记录 - 非唯一索引等值查询(如
WHERE status = 'pending')→ 锁所有满足条件的行 + 对应间隙 - 范围查询(如
WHERE created_at > '2024-01-01')→ 锁住范围内所有记录 + 两端间隙,可能覆盖未存在的数据区间
如何快速定位正在发生的死锁?
MySQL 不会自动暴露死锁现场,必须主动查。最直接的方式是开启死锁日志并实时观察:
SHOW ENGINE INNODB STATUS\G
输出中 LATEST DETECTED DEADLOCK 段落会完整列出最近一次死锁的两个事务、各自持有的锁、等待的锁、SQL 语句和事务 ID。注意这个信息只保留最后一次,不是历史记录。
更可持续的做法是启用 InnoDB 死锁日志:
- 设置
innodb_print_all_deadlocks = ON(写入 error log) - 配合监控脚本定期抓取
SHOW ENGINE INNODB STATUS输出并提取死锁块 - 应用层捕获
Deadlock found when trying to get lock错误码(1213),记录上下文 SQL 和参数
INSERT … ON DUPLICATE KEY UPDATE 为什么也会死锁?
很多人以为这只是“插入或更新”,不涉及复杂锁逻辑,但实际上它会在唯一键冲突路径上加 S(共享)锁,再升级为 X(排他)锁,过程中与其它事务的锁顺序稍有错位就触发死锁。
典型场景:两个事务几乎同时执行:
INSERT INTO orders (order_no, user_id, status) VALUES ('ORD-001', 123, 'new')
ON DUPLICATE KEY UPDATE status = 'updated';
若 order_no 是唯一索引,且两事务都试图插入相同 order_no,它们会先尝试获取该索引记录的 S 锁,然后争抢升级为 X 锁——此时极易形成循环等待。
- 避免方式:确保业务侧对同一业务键(如订单号)的并发写入尽量串行化(例如用 Redis 分布式锁控制)
- 替代方案:改用
SELECT ... FOR UPDATE先查再判再插/更,但需严格保证 SELECT 和后续操作在同一事务中 - 注意:即使没有显式事务,
INSERT ... ON DUPLICATE KEY UPDATE本身也构成一个隐式事务,锁行为不受 AUTOCOMMIT 关闭影响
减少死锁的核心策略不是“加锁更少”,而是“锁顺序一致”
绝大多数死锁源于多个事务以不同顺序访问相同资源。比如事务 A 先锁行 X 再锁行 Y,事务 B 却先锁 Y 再锁 X —— 这种交叉就是死锁温床。
真正有效的优化点很朴素:
- 所有涉及多行更新的逻辑,强制按主键(或某个唯一、稳定、可排序字段)升序处理,例如:先
ORDER BY id ASC再批量更新 - 避免在事务中混合使用不同索引条件更新同一张表(如一个事务用
user_id更新,另一个用email更新),容易导致锁粒度和顺序不可控 - 缩短事务生命周期:把非数据库操作(如 HTTP 调用、文件读写)移出事务块;避免在事务中做 sleep 或用户交互
- 必要时主动加锁统一顺序:比如对一批订单做状态变更前,先
SELECT id FROM orders WHERE ... ORDER BY id FOR UPDATE
死锁无法完全杜绝,但只要锁获取顺序始终一致,哪怕并发再高,InnoDB 也能靠等待队列自然排队,而不是陷入循环等待。










