线上死锁问题定位
问题
业务上有个异步任务,这个任务包含许多http接口,多个接口是串行,此任务中的接口数是根据操作人员的选择动态生成的;每个接口同步返回后又会更新关联表。
这个时候会出现两个大任务A和B,每个任务又包含了几十个接口,碰巧这两个任务的子接口里又有维护相同关联关系表的数据,就会报:DeadlockLoserDataAccessException
问题解析
首先检测到了导致死锁的语句
-- 第一条
update by_oedu_lesson_relation SET is_deleted = 1, last_update_id = 11419, last_update_time = '2022-08-31 15:03:00.659'
WHERE ( is_deleted = 0 and tenant_id = 1 and class_id = 117 and schedule_id = 1437 and student_id = 2108171 );
-- 第二条
update by_oedu_lesson_relation SET is_deleted = 1, last_update_id = 11419, last_update_time = '2022-08-31 15:03:00.687'
WHERE ( is_deleted = 0 and tenant_id = 1 and class_id = 117 and schedule_id = 1465 and student_id = 2108171 );
通过explain分析此语句命中了 class_id 的索引,innodb引擎默认的锁是行锁,所以可以看到两条语句明明不是同一条记录为何会有锁的竞争,因为锁的是以class_id为维度的多行记录。
注:如果没有命中索引,锁的是整张表
最后通过阿里云的"一件诊断"中的"锁分析"得出了相同的分析结果:
解决方案
通过分析问题,得出了两个需要优化的地方
- 锁粒度太粗,不应该只命中 class_id,需要增加 schedule_id & student_id 联合索引
- 事务太大,一个任务如果包含了几十个http接口怎么说也要几十秒才能提交事务;得出的结论是把每个接口和其对应的数据维护包成一个子任务,新建一个事务,处理后直接提交(前提是整个任务错误时不需要回滚)
涉及到的相关手段
- sql
-- innodb 状态信息
SHOW ENGINE INNODB STATUS;
-- 锁等待时间
SHOW VARIABLES LIKE 'innodb_lock_wait_timeout';
-- 通过explain查看命中索引
explain update by_oedu_lesson_relation SET is_deleted = 1, last_update_id = 11419, last_update_time = '2022-08-31 15:03:00.687'
WHERE ( is_deleted = 0 and tenant_id = 1 and class_id = 117 and schedule_id = 1465 and student_id = 2108171 );
- Spring事务
// 新启事务
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
<!-- 获取当前事务 为了验证开启事务是否生效-->
<select id="getTransactionId" resultType="java.lang.String" >
SELECT TRX_ID
FROM INFORMATION_SCHEMA.INNODB_TRX
WHERE TRX_MYSQL_THREAD_ID = CONNECTION_ID();
</select>
WHERE TRX_MYSQL_THREAD_ID = CONNECTION_ID();
</select>