
在一个正常的业务中,MySQL报错了阻塞。第一反应是死锁,但是发现是一条insert语句,语句如下:
1 | INSERT INTO make_card_info ( |
此时有点懵圈,手动执行了下这条SQL,发现确实被阻塞了。思路步骤如下
- 分析表结构
- 查看MySQL状态
- 查看代码,分析调用链
现在咱们挨个分析一下
分析表结构
一般出现这种问题,要么就是ID重复,要么就是索引里面存在唯一索引。ID重复倒是可以排除了,这个表的id是自增的,那么就是唯一索引的问题
看完这个表结构,猜想大概率是唯一索引问题,造成阻塞的想必也一定是这个唯一索引。
我的思路是mk_order_no
字段难道重复了吗?走了一个这个sql,发现并没有查询出来
1 | select * from make_card_info where mk_order_no=1870999825732784129 |
问题已经出来了,应该是并发插了。这时候去看看MySQL的状态就行了,看看可不可以kill掉它
查看MySQL状态
1 | SHOW PROCESSLIST; |
查看代码,分析调用链
1 |
|
大概能猜出来了,该方法问题点如下:
- 该方法开启了事务,未手动提交,交给spring进行管理
- 该方法的执行流程为,先insert拿到id,后http请求一个接口,得到结果后再进行修改
- 注意: 该步骤也是在事务下进行的,会有可能出现A事务没提交,B事务又开启的情况
- 该接口未做任何幂等性的限制
综上所述,做个复盘,从请求侧确实能看出来两次请求(MQ重试)
也就是A线程在执行未提交事务前的逻辑,已经insert。B线程又进来请求insert,两个线程虽主键不冲突,但有唯一索引限制,A事务无法提交update SQL,B事务则被insert SQL阻塞,故出现死锁。
解决方案
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
19739684RUNNING
2025-01-07 14:41:15
(NULL) (NULL)
3269085273
(NULL) (NULL)
01
2
1 | KILL 269085273; |
改造代码
改造代码的落地方式分为多种,简单说下两种比较实用的思路
- 给订单号做分布式锁,防止单订单号重复提交,也就是单订单号串行化处理
- 这段代码核心就是需要得到insert的id,后修改造成的。 可以改造为不需要数据库返回id,使用雪花等id的分配方式,这样还可以减少一次修改