最近例行巡检时候发现一个死锁,阿里云RDS FOR MYSQL 8.0.X!
虽然阿里云的死锁页面看起来比较友好,不过跟社区版一样只是显示事务最后一条死锁SQL和相关的信息.一不小心对初级MYSQL DBA来说,深深地误导,浪费大量时间研究这两个SQL怎么发生了死锁!
阿里云RDS默认情况下审计没有开线程ID或者是事务ID.后期开启后才能根据SQL抓到线程ID,然后根据线程ID抓出相关的SQL.
业务结算:我们商户进行结算的时候,用户下单购买多个商品,每次给我们平台给商户结算是按用户订单来的,每个商品的价格要累加到订单上的.
为了快速响应,应用架构采用异步方式,下订单,付款,给商家结算都是异步模式,
每一步工作完成后会向LTS记录一笔,然后LTS定时启动该笔对应的JAVA接口去完成剩余工作.
有时候虽然前台用户下单少,LTS也会启动剩余工作时候造成短时间的并发量.
从某种程度来说LTS就是个高并发源!
事务 1(巳回滚) | 事务 2 | |
Session ID | 37095516 | 37095518 |
Thread id | 364126 | 366610 |
请求类型 | updating | updating |
事务ID | 64136665 | 164136664 |
涉及表 | DB`.`trans_daily_collect DB.`trans_order`/* Partition`request date time 20240403` | db`,`trans_daily collect db`.`trans_order’/* Partitionreguest date time 20240403` |
等待锁 | index PRIMARY of table db`.`trans_daily_collect xid 164136665 lock mode X locks rec but not gap waiting | index iux trans no of table `db`,'trans_order” /* Partition `reguest_date_time 20248403`*/ trx id 164136664 1ock mode X 1ocks rec but not gap waiting |
等待锁索引名 | PRIMARY | iux trans_no |
等待锁类型 | X locks rec but not gap waiting | X locks rec but not gap waiting |
持有锁 | index iux trans no of table 'db'.'trans_order’ /* Partition`request date time 20248483`*/ trx id 164136665 lock mode X 1ocks rec but not gap | index PRIMARY of table "db'.'trans_daily_collect' trx id 164136664 lock mode X locks rec but not gap |
持有锁索引名 | iux_ trans_no | RIMARY |
持有锁类型 | X locks rec but not gap | X locks rec but not gap |
事务SQL | UPDATE SK TRANS DAILY COLLECT SET TOTAL AMONT = TOTAL AMONT + 5888800 WHERE id = 212 and SETTLE_NO is null AND STATUS = '180' | update trans_oder SET collect_code = '202404020100001' where status ='02'and trans_no ='2024840200300003215'AND request datetime < DATE ADD( 2024-84-02 16:13:42',interval 1 hour) AND reguestdate time > DATE ADD('2024-04-02 16:13:42',INTERVAL -1 hour) |
上面涉及订单表和汇总表, 事务1更新汇总表,事务2更新订单表
事务1 持有唯一索引订单号,等待主键索引
事务2 持有主键索引,等待唯一索引IUX_TRANS_NO
粗绿一看 确实是互相持有对方的锁. 仔细一看不对啊,最后两个UPDATE语句更新的表只是不同的,而且都是单张表更新.
看唯一索引忽然明白,事务其它语句更新了对方的表,才会导致持有对方的锁.
MYSQL死锁信息锁类型:
lock_modes | RR | RC |
X locks rec but not gap | 记录锁 | 记录锁 |
gap | 间隙锁 | 间隙锁 |
rec | NEXT-KEY |
上面是MSYQL死锁信息时候显示锁类型,里面隐藏的指向.尤其是REC需要根据隔离级别来区别,表达意思词不达意! 也就是说死锁信息输出不够全面,又容易误导.要是增加当前事务的隔离级别,然后REC,GAP,NEXT-KEY 三种简单明了. 比让人猜字谜好多了!
通过SQL日志审计页面根据线程ID找到该事务的全部SQL分析.非云环境应该是开启了通用日志.
事务2 | 用到的索引 | |
1 | update trans_order SET status = '02', pay_time = '2024-04-02 16:14:44', last_update_time = '2024-04-02 16:14:45.08', where status in ('01', '06', '10') and trans_no = '2024040200300003216' AND request_date_time < DATE_ADD('2024-04-02 16:13:43', interval 1 hour) AND request_date_time > DATE_ADD('2024-04-02 16:13:43', INTERVAL -1 hour); | IUX_TRANS_NO |
2 | UPDATE TRANS_COLLECT SET TOTAL_AMONT = TOTAL_AMONT + 50000.00 TRANS_COUNT = TRANS_COUNT + 1 WHERE id = 212 and SETTLE_NO is null AND STATUS = '100'; | PRIMARY |
事务1 | 索引 | |
1 | update trans_order SET status = '02', pay_time = '2024-04-02 16:14:44', last_update_time = '2024-04-02 16:14:45.076', where statusin ('01', '06', '10') and trans_no = '2024040200300003215' AND request_date_time < DATE_ADD('2024-04-02 16:13:42', interval 1 hour) AND request_date_time > DATE_ADD('2024-04-02 16:13:42', INTERVAL -1 hour);
| iux_trans_no |
2 | UPDATE TRANS_COLLECT SET TOTAL_AMONT = TOTAL_AMONT + 25000.00, TRANS_COUNT = TRANS_COUNT + 1 WHERE id = 212 and SETTLE_NO isnull ANDSTATUS = '100'; | PRIMARY |
3 | UPDATE USER_ACCOUNT SET UNSETTLE_AMOUNT = UNSETTLE_AMOUNT + 24750.00 WHERE user_NO = '010003'; | idx_user_no |
4 | update trans_ordre SET collect_code = '202404020100001' where status = '02' and`trans_no` = '2024040200300003215' AND request_date_time < DATE_ADD('2024-04-02 16:13:42', interval 1 hour) AND request_date_time > DATE_ADD('2024-04-02 16:13:42', INTERVAL -1 hour); | iux_trans_no |
把两个事务SQL根据时间并在一起对照查看
事务1 | 时间 | 持有锁 | 时间 | 事务2 | 持有锁 |
update trans_order SET status = '02', pay_time = '2024-04-02 16:14:44', last_update_time = '2024-04-02 16:14:45.076' where statusin ('01', '06', '10') and trans_no = '2024040200300003215' AND request_date_time < DATE_ADD('2024-04-02 16:13:42', interval 1 hour) AND request_date_time > DATE_ADD('2024-04-02 16:13:42', INTERVAL -1 hour); | 2024-04-02 17:14:45.078 | ||||
UPDATE TRANS_COLLECT SET TOTAL_AMONT = TOTAL_AMONT + 25000.00, TRANS_COUNT = TRANS_COUNT + 1 WHERE id = 212 and SETTLE_NO isnull ANDSTATUS = '100'; | 2024-04-02 17:14:45.082 | index PRIMARY trx id 164136664 lock_mode X locks rec but not gap | 2024-04-02 17:14:45.082 | update trans_order SET status = '02', pay_time = '2024-04-02 16:14:44', last_update_time = '2024-04-02 16:14:45.08' where status in ('01', '06', '10') and trans_no = '2024040200300003216' AND request_date_time < DATE_ADD('2024-04-02 16:13:43', interval 1 hour) AND request_date_time > DATE_ADD('2024-04-02 16:13:43', INTERVAL -1 hour); | index iux_trans_no X locks rec but not gap |
UPDATE user_ACCOUNT SET UNSETTLE_AMOUNT = UNSETTLE_AMOUNT + 24750.00 WHERE user_NO = '010003'; | 2024-04-02 17:14:45.083 | 2024-04-02 17:14:45.083 | UPDATE TRANS_COLLECT SET TOTAL_AMONT = TOTAL_AMONT + 50000.00,TRANS_COUNT = TRANS_COUNT + 1 WHERE id = 212 and SETTLE_NO is null AND STATUS = '100'; | X locks rec but not gap waiting index PRIMARY of table `trans_collect` | |
update SET collect_code = '202404020100001' where status = '02' and`trans_no` = '2024040200300003215' AND request_date_time < DATE_ADD('2024-04-02 16:13:42', interval 1 hour) AND request_date_time > DATE_ADD('2024-04-02 16:13:42', INTERVAL -1 hour); | 2024-04-02 17:14:45.084 | X locks rec but not gap waiting |
根据对比 事务1执行了4个更新语句, 事务2执行了2个更新语句.就产生了死锁.其实事务1和事务2执行的语句都是一样的,也就是4个更新语句,只是事务2执行到第2条更新语句时候就与事务1发生了死锁.
这4条UPDATE语句功能是这样的
1 更新订单为成功的,根据状态和订单号以及最近1小时间隔
2 更新汇总表,根据ID=212 进行金额累加,以及计数器++
3 更新商户账户待结算金额
4 更新订单表的,把汇总号更新到具体订单对应的位置
事务1
1 更新订单为成功的,根据状态和订单号以及最近1小时间隔
持有订单表IUX_TRANS_NO的锁
2 更新汇总表,根据ID=212 进行金额累加,以及计数器++
持有汇总表的主键索引锁 持有中
3 更新商户账户待结算金额
持有对应的锁
4 更新订单表的,把汇总号更新到具体订单对应的位置
要持有订单表IUX_TRANS_NO的锁 等待状态
事务2
1 更新订单为成功的,根据状态和订单号以及最近1小时间隔
持有订单表IUX_TRANS_NO的锁 持有中
2 更新汇总表,根据ID=212 进行金额累加,以及计数器++
持有汇总表的主键索引锁 等待状态
3 更新商户账户待结算金额
未执行
4 更新订单表的,把汇总号更新到具体订单对应的位置
未执行
这些我们很清楚了,
事务1
第2条SQL 持有主键索引 汇总表的.
第4条SQL等待IUX_TRANS_NO 索引锁,
事务2
第1条持有 IUX_TRASN_NO 锁
第2条 等待汇总表 主键锁
再看一下具体SQL 得知
事务1 第2条 主键索引记录锁 ID=212 ; 第4条 TRASN_NO="...3215"
事务2 第1条 唯一索引 锁定 TRASN_NO="....3216" 第2条 想锁定 ID=212
似乎接近了真相,业务上一个订单包含多个商品, 订单包就是212 ,商品就是交易号. 很多情况下业务叫法和表命名是无法对齐的. 实际上订单表应该叫商品订单表... 不说了! 这里ID=212就是总订单, TRASN就是具体商品交易订单. LTS 把ID=212的总订单里面的商品交易分拆到不同的线程中并发执行.导致死锁的发生.
下面是唯一索引结构,由时间和交易号组成,只所以要做成唯一,原本是非分区表,是有唯一索引的,不过是TRASN_NO字段.现在要做成分区表,原本另外个项目跟开发沟通,分区无法保证全局唯一性,要保证的话只能开发应用端确保这个逻辑.后来这个新的开发人员水平不高,新业务强烈建议数据库去保证.
UNIQUE KEY `iux_trans_no` (`trans_no`,`request_date_time`)
USING BTREE,
这里我们回顾下 事务1的第1条SQL也是要持有IUX_TRASNO,
按理说 事务2的第1条SQL应该等待IUX_TRASNO,然后它获得了,奇怪不?
应该说 事务1要么释放了IUX_TRASNO的锁,否则事务2是无法加到锁的.
不过按理论来说 事务必须等待结束后才会去释放锁的.
违背了理论,实际上又释放了. 这很矛盾啊?
要么是MYSQL的BUG ,有这可能,去年一个网络直播,辩论MYSQL和PG谁是第一开源数据库? 姜老师和冯若航 怼骂! 其中MYSQL事务的部分提交.
也许是我认知不到位,听说MYSQL是两层架构,SERVER层和引擎层,引擎层找数据,加锁,返回服务层,服务层不要,引擎就释放该记录锁.
有这条规矩,大概率是这条了.事务1 对TRASN_NO="....3215" 加锁,同时对TRASN_NO="...3216"加锁.然后返回给服务层,服务层判断不需要,就要求引擎去释放"...3216" 所以事务2才能加上TRASN_NO='3216'锁
事务1 第4条 更新3215,顺便要加一下3216记录锁来判断下. 结果被事务2阻塞了. 为此就发生了死锁.
有什么优化办法?
1 我们调整SQL执行顺序,把两个更新唯一索引放在1和2的位置,把主键索引放在第3个位置.这样加锁时间非常短, 不过只是暂时型解决,只要两个事务的时间间隔越短,还是会爆发死锁.
2 原来业务单表唯一索引,次新业务分区表普通索引,新业务分区表唯一索引为什么原理的业务不会,次新的业务不会,反而新业务就会?
问题就出在组合字段的唯一索引. 实时上把分区表唯一索引 改成普通索引就没有了死锁发生了.
奇怪不奇怪?
MYSQL 8.0.X RC隔离级别下 二级组合字段唯一索引会
按照理论来说只加X锁,顶多加个下一个不符记录的记录锁.
新旧业务索引都会加下一个不符合记录的X锁,偏偏组合唯一索引就会.
所以我们要请出丁奇,丁老师出马..........................................
我也从极客时间购买并学习过丁奇老师的<MYSQL45讲>,说真的挺不错,从原理上了解MYSQL的通俗易读的课程. 不过很多生产实践遇到的问题,并不能全部从该课程获得解释!
正好丁奇老师开课培训主讲生产实践中遇到的问题,并且大家可以选择自己感兴趣的单元学习.
《MySQL进阶单元课》每单元50¥,按需按单元选。
报名方式:+私聊 丁奇
1. 近期直播课:
11月2日(本周六) 19:00
第14单元《读写分离:扩展性和一致性》
2. 前14单元大纲: