mysql事务中的锁定机制与死锁优化

来源:这里教程网 时间:2026-02-28 20:40:03 作者:

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 也能靠等待队列自然排队,而不是陷入循环等待。

相关推荐