先交代下背景:我们做的是个电商促销系统,高峰期每秒得有上百个下单请求。数据库用的是MySQL 5.7,InnoDB引擎,事务隔离级别是默认的REPEATABLE-READ。订单表结构大概长这样:
问题出在用户抢购时,系统会同时执行两个操作:一是更新订单状态(比如从待支付改为已完成),二是减少商品库存。这两步放在一个事务里,用到了行锁。但诡异的是,监控日志里总出现"Deadlock found when trying to get lock"的报错,平均每分钟能触发两三次。
排查过程挺折腾的。首先用抓取了死锁详情,发现冲突集中在这个普通索引上。事务A先锁住了product_id=100的记录准备更新状态,同时事务B锁住了product_id=101的记录要减库存,但接着事务A试图去获取product_id=101的锁,事务B又反过来抢product_id=100的锁——典型的循环等待。根因在于更新语句走了非唯一索引,导致间隙锁(gap lock)和临键锁(next-key lock)互相阻塞。
具体来说,问题语句是:
虽然看着是操作同一条产品记录,但MySQL在REPEATABLE-READ级别下,对非唯一索引的更新会锁住索引区间。比如product_id=100附近有间隙锁保护,其他事务要更新相邻product_id时就会被阻塞。更麻烦的是,库存表更新时也会对产品ID加锁,两个事务交叉等待就死锁了。
解决方案从三方面入手:第一是优化索引,把改成覆盖索引,让更新操作直接走索引覆盖,避免回表锁冲突;第二是调整事务顺序,统一先更新库存再处理订单状态,减少锁持有时间;第三是加入重试机制,用Spring的注解在死锁时自动重试3次。改完后的核心代码类似:
另外还把事务隔离级别降到了READ-COMMITTED,虽然可能引发幻读,但对我们这个业务场景影响不大,毕竟促销活动都是短时爆发,数据一致性要求没那么严苛。
上线后观察了一周,死锁频率从每天几十次降到零。这个案例给我的教训是:MySQL的锁机制远比想象中复杂,特别是在非主键索引上操作时,间隙锁容易成为性能杀手。开发时不能光盯着业务逻辑,还得结合数据访问模式来设计索引和事务。建议大家在设计阶段就用分析执行计划,必要时用视图监控锁竞争。毕竟数据库调优就是个不断填坑的过程,多积累点实战经验总没坏处。
2516

被折叠的 条评论
为什么被折叠?



