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更新,另一个用
SELECT id FROM orders WHERE ... ORDER BY id FOR UPDATE
死锁无法完全杜绝,但只要锁获取顺序始终一致,哪怕并发再高,InnoDB 也能靠等待队列自然排队,而不是陷入循环等待。
