在一个正常的业务中,MySQL报错了阻塞。第一反应是死锁,但是发现是一条insert语句,语句如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| INSERT INTO make_card_info ( id, open_id, md_order_no, apollo_user_id, project_name, task_type, task_pass_time, amount, app_mark ) VALUES ( 1876546101549854799, '1851452288917311489', 1870999825732784129, '1859075883175706625', '模糊处理', '模糊处理', '2025-01-07 16:27:09.737', 10, 'mark-cloud' );
|
此时有点懵圈,手动执行了下这条SQL,发现确实被阻塞了。思路步骤如下
- 分析表结构
- 查看MySQL状态
- 查看代码,分析调用链
现在咱们挨个分析一下
分析表结构
一般出现这种问题,要么就是ID重复,要么就是索引里面存在唯一索引。ID重复倒是可以排除了,这个表的id是自增的,那么就是唯一索引的问题
![image]()
看完这个表结构,猜想大概率是唯一索引问题,造成阻塞的想必也一定是这个唯一索引。
我的思路是mk_order_no字段难道重复了吗?走了一个这个sql,发现并没有查询出来
1
| select * from make_card_info where mk_order_no=1870999825732784129
|
问题已经出来了,应该是并发插了。这时候去看看MySQL的状态就行了,看看可不可以kill掉它
查看MySQL状态
1 2 3 4
| SHOW PROCESSLIST; #这将显示当前所有连接的状态,包括正在运行的 SQL 语句。 SELECT * FROM information_schema.innodb_trx; #这个查询将返回当前所有活动的 InnoDB 事务,包括事务 ID、开始时间、状态等信息。
|
查看代码,分析调用链
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| @Override @Transactional public String uploadChain(MakeCardInfoDTO dto) { if(dto.getMdOrderNo() == null){ throw new RuntimeException("订单id不能为空"); } UserWalletDTO userWalletDTO = ConvertUtils.sourceToTarget(dto, UserWalletDTO.class); dto.setApolloUserId(createUserWallet(userWalletDTO).getUserId());
MakeCardInfoEntity makeCardInfoEntity = makeCardInfoDao.selectOne(new QueryWrapper<MakeCardInfoEntity>() .lambda() .eq(MakeCardInfoEntity::getMdOrderNo, dto.getMdOrderNo())); if(makeCardInfoEntity != null){ if(makeCardInfoEntity.getTradeStatus().compareTo(WalletStatusEnum.SUCCESS.getStatus()) == 0){ return makeCardInfoEntity.getTaskId(); }else{ return StringUtils.EMPTY; } }
String uploadChainBody = getUploadChainBody(dto); MakeCardInfoEntity entity = ConvertUtils.sourceToTarget(dto, MakeCardInfoEntity.class); makeCardInfoDao.insert(entity);
JSONObject userWallet = walletRequestInfo.uploadChain(uploadChainBody); String taskId = userWallet.getStr("taskId"); entity.setTaskId(taskId); entity.setReqBody(uploadChainBody); makeCardInfoDao.updateById(entity); return taskId; }
|
大概能猜出来了,该方法问题点如下:
- 该方法开启了事务,未手动提交,交给spring进行管理
- 该方法的执行流程为,先insert拿到id,后http请求一个接口,得到结果后再进行修改
- 注意: 该步骤也是在事务下进行的,会有可能出现A事务没提交,B事务又开启的情况
- 该接口未做任何幂等性的限制
综上所述,做个复盘,从请求侧确实能看出来两次请求(MQ重试)
也就是A线程在执行未提交事务前的逻辑,已经insert。B线程又进来请求insert,两个线程虽主键不冲突,但有唯一索引限制,A事务无法提交update SQL,B事务则被insert SQL阻塞,故出现死锁。
![image]()
解决方案
kill掉,让业务正常运转
找到阻塞的SQL,kill掉就ok了。kill掉后通知业务方进行重试 and 人工介入处理
trx_id trx_state trx_started trx_requested_lock_id trx_wait_started trx_weight trx_mysql_thread_id trx_query trx_operation_state trx_tables_in_use trx_tables_locked trx_lock_structs trx_lock_memory_bytes trx_rows_locked trx_rows_modified trx_concurrency_tickets trx_isolation_level trx_unique_checks trx_foreign_key_checks trx_last_foreign_key_error trx_adaptive_hash_latched trx_adaptive_hash_timeout trx_is_read_only trx_autocommit_non_locking
19739684
RUNNING
2025-01-07 14:41:15
(NULL) (NULL)
3
269085273
(NULL) (NULL)
0
1
2
改造代码
改造代码的落地方式分为多种,简单说下两种比较实用的思路
- 给订单号做分布式锁,防止单订单号重复提交,也就是单订单号串行化处理
- 这段代码核心就是需要得到insert的id,后修改造成的。 可以改造为不需要数据库返回id,使用雪花等id的分配方式,这样还可以减少一次修改