一、背景描述
背景描述
一个订单系统和库存系统的死锁问题。
比如用户下单时,需要同时更新订单表和库存表,而不同的操作顺序可能导致死锁。这种情况下,事务A先更新订单表再更新库存表,而事务B先更新库存表再更新订单表,这样可能会形成循环等待,导致死锁。
背景知识
对MySQL锁机制的理解,包括共享锁、排他锁、行锁、间隙锁、Next-Key锁,以及不同隔离级别对锁的影响。
解决问题的系统方法:复现问题、分析日志、定位原因、实施解决方案、验证效果。
通用步骤
分析死锁日志、理解事务执行顺序和锁获取顺序、调整事务逻辑或数据库设计、测试验证,以及监控预防。
同时强调预防死锁的最佳实践,如保持事务简短、按顺序访问资源、合理设计索引等。
二、解决MySQL死锁的案例及全面分析
在电商系统中,用户下单时需同时操作订单表(orders
)和库存表(inventory
)。两个事务因操作顺序不同导致死锁。以下是具体场景:
订单表结构orders
CREATE TABLE orders (
order_id INT PRIMARY KEY,
user_id INT,
item_id INT,
quantity INT,
status VARCHAR(20)
);
库存表结构orders
CREATE TABLE inventory (
item_id INT PRIMARY KEY,
stock INT
);
死锁发生过程
事务A:用户下单操作
BEGIN;
-- 1. 插入新订单
INSERT INTO orders VALUES (1001, 1, 101, 2, 'pending');
-- 2. 扣减库存
UPDATE inventory SET stock = stock - 2 WHERE item_id = 101;
COMMIT;
事务B:后台库存调拨操作
BEGIN;
-- 1. 增加调拨库存
UPDATE inventory SET stock = stock + 5 WHERE item_id = 101;
-- 2. 标记调拨订单完成
UPDATE orders SET status = 'completed' WHERE order_id = 1000;
COMMIT;
死锁原因分析
1.锁竞争顺序不同
事务A先锁orders,再锁inventory。
事务B先锁inventory,再锁orders。
形成循环等待:A等B释放inventory,B等A释放orders。
2.间隙锁(Gap Lock)的隐形作用
在REPEATABLE READ隔离级别下,事务A的INSERT操作会隐式加间隙锁,事务B的UPDATE可能被阻塞
排查与诊断
查看死锁日志
执行 SHOW ENGINE INNODB STATUS;,关注 LATEST DETECTED DEADLOCK 部分:
*** (1) TRANSACTION: TRANSACTION 12345, ACTIVE 2 sec updating
LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s)
MySQL thread id 100, OS thread handle 0x7f8a1c0b6700, query id 2000 updating
UPDATE inventory SET stock = stock + 5 WHERE item_id = 101;
*** (1) HOLDS THE LOCK(S):
-- 事务B持有 inventory.item_id=101 的排他锁(X锁)
*** (2) TRANSACTION: TRANSACTION 12346, ACTIVE 3 sec inserting
LOCK WAIT 3 lock struct(s), heap size 1136, 2 row lock(s)
MySQL thread id 101, OS thread handle 0x7f8a1c0b6800, query id 2001 inserting
INSERT INTO orders VALUES (1001, 1, 101, 2, 'pending');
*** (2) HOLDS THE LOCK(S):
-- 事务A持有 orders 表的间隙锁(Gap Lock)
关键结论
事务A和B以相反顺序竞争锁,形成循环依赖。
间隙锁在插入新订单时隐式加锁,加剧了阻塞。
解决方案
1.统一资源访问顺序
强制所有事务按固定顺序操作:先操作inventory,再操作orders。
调整事务A:
BEGIN;
-- 先操作库存表
UPDATE inventory SET stock = stock - 2 WHERE item_id = 101;
-- 再操作订单表
INSERT INTO orders VALUES (1001, 1, 101, 2, 'pending');
COMMIT;
2.降低隔离级别
从REPEATABLE READ改为READ COMMITTED,减少间隙锁的使用:
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
3.优化索引设计
为orders
表的item_id
添加索引,避免全表扫描:
CREATE INDEX idx_orders_item_id ON orders (item_id);
4.重试机制
在应用层捕获死锁错误(MySQL错误码1213),自动重试事务:
# Python伪代码示例
max_retries = 3
for _ in range(max_retries):
try:
execute_transaction()
break
except MySQLdb.OperationalError as e:
if 'Deadlock found' in str(e):
continue
else:
raise
覆盖的其他常见死锁场景
批量更新中的锁升级
场景:两个事务批量更新同一组数据的不同子集。
解决:将批量操作拆分为单条处理,或按固定顺序排序。
不同索引导致的锁冲突
场景:事务A通过索引A锁定行,事务B通过索引B锁定同一行。
解决:统一使用主键或唯一索引访问数据。
长事务与锁堆积
场景:长事务持有锁时间过长,导致后续事务堆积。
解决:缩短事务执行时间,避免在事务中处理业务逻辑。