生产环境长事务造成的mysql死锁分析及解决

死锁日志

*************************** 1. row ***************************
  Type: InnoDB
  Name: 
Status: 
=====================================
2023-06-13 14:11:44 0x7f1febad8700 INNODB MONITOR OUTPUT
=====================================
Per second averages calculated from the last 25 seconds
-----------------
BACKGROUND THREAD
-----------------
srv_master_thread loops: 27984349 srv_active, 0 srv_shutdown, 27539402 srv_idle
srv_master_thread log flush and writes: 55523751
----------
SEMAPHORES
----------
OS WAIT ARRAY INFO: reservation count 23061421
OS WAIT ARRAY INFO: signal count 67625116
RW-shared spins 0, rounds 90130459, OS waits 21606013
RW-excl spins 0, rounds 168864564, OS waits 391828
RW-sx spins 10886910, rounds 129682783, OS waits 864374
Spin rounds per wait: 90130459.00 RW-shared, 168864564.00 RW-excl, 11.91 RW-sx
------------------------
LATEST DETECTED DEADLOCK
------------------------
2023-06-03 18:20:06 0x7f1fc46e5700
*** (1) TRANSACTION:
TRANSACTION 225089659, ACTIVE 1 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 320 lock struct(s), heap size 41168, 4 row lock(s)
MySQL thread id 5619472, OS thread handle 139774417090304, query id 1142417448 172.16.10.121 application updating
UPDATE `saas_application`.`tenant_account_statistics`
    SET settlement   = settlement - 198.00,
        to_account   = to_account + 198.00,
        withdrawable = withdrawable + 198.00
    WHERE shop_id = 1531550363716599810
      AND belong_id = 1501384359112056834
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 494 page no 5 n bits 136 index index_shopid_belongid of table `saas_application`.`tenant_account_statistics` trx id 225089659 lock_mode X locks rec but not gap waiting
Record lock, heap no 40 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
 0: len 8; hex 154128f0f8f2c002; asc  A(     ;;
 1: len 8; hex 14d5fd1eb433a002; asc      3  ;;
 2: len 4; hex 00000031; asc    1;;

*** (2) TRANSACTION:
TRANSACTION 225089658, ACTIVE 1 sec fetching rows, thread declared inside InnoDB 676
mysql tables in use 1, locked 1
323 lock struct(s), heap size 41168, 5 row lock(s), undo log entries 2
MySQL thread id 5619462, OS thread handle 139774416279296, query id 1142417475 172.16.10.121 application updating
UPDATE `saas_application`.`tenant_account_statistics_details`
    SET status                 = 1,
        actual_settlement_time = NOW()
    WHERE order_no = 'O2305101612286510'
      and `type` = 0
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 494 page no 5 n bits 136 index index_shopid_belongid of table `saas_application`.`tenant_account_statistics` trx id 225089658 lock_mode X locks rec but not gap
Record lock, heap no 40 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
 0: len 8; hex 154128f0f8f2c002; asc  A(     ;;
 1: len 8; hex 14d5fd1eb433a002; asc      3  ;;
 2: len 4; hex 00000031; asc    1;;

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 727 page no 473 n bits 152 index PRIMARY of table `saas_application`.`tenant_account_statistics_details` trx id 225089658 lock_mode X locks rec but not gap waiting
Record lock, heap no 33 PHYSICAL RECORD: n_fields 24; compact format; info bits 0
 0: len 4; hex 80004bbc; asc   K ;;
 1: len 6; hex 00000cd4ab0b; asc       ;;
 2: len 7; hex f6000001ca0110; asc        ;;
 3: len 4; hex 8000aa75; asc    u;;
 4: len 17; hex 4f32333035313031363131343938333437; asc O2305101611498347;;
 5: len 4; hex 80000000; asc     ;;
 6: SQL NULL;
 7: len 8; hex 94d5fd1eb433a002; asc      3  ;;
 8: len 8; hex 9520fd639933b002; asc    c 3  ;;
 9: len 18; hex e5a4a9e9a290e59586e59f8ee5b9b3e58fb0; asc                   ;;
 10: len 8; hex 954128f0f8f2c002; asc  A(     ;;
 11: len 30; hex e5a4a9e9a290e88cb6e59381e799bde88dafe99b86e59ba2e58685e8b4ad; asc                               ; (total 33 bytes);
 12: len 8; hex 94d5fd1eb433a002; asc      3  ;;
 13: len 6; hex e680bbe5ba97; asc       ;;
 14: len 1; hex 80; asc  ;;
 15: len 4; hex 80000000; asc     ;;
 16: len 1; hex 80; asc  ;;
 17: len 5; hex 800000c600; asc      ;;
 18: len 5; hex 99b0150304; asc      ;;
 19: SQL NULL;
 20: SQL NULL;
 21: SQL NULL;
 22: len 20; hex 4f32333035313030383131353139363031313233; asc O2305100811519601123;;
 23: len 21; hex 423233303531303136313135323038333032313030; asc B23051016115208302100;;

根据sql找到代码

image.png

分析对应的死锁日志

事务1死锁

image.png

从这段分析出,这是一个事务,事务id = 225089659 ,
死锁的语句是:涉及的表是 tenant_account_statistics 这张表

UPDATE `saas_application`.`tenant_account_statistics`
    SET settlement   = settlement - 198.00,
        to_account   = to_account + 198.00,
        withdrawable = withdrawable + 198.00
    WHERE shop_id = 1531550363716599810
      AND belong_id = 1501384359112056834

涉及到 lock_mode X locks 是排他锁,行锁的一种。对应锁的索引是 index index_shopid_belongid 这是一个联合索引,shop_id 和 belong_id 的联合索引,

分析

1.此时该事务, 在尝试获取表 tenant_account_statistics 的 index index_shopid_belongid 对应非聚合索引的行锁。

事务2死锁

image.png

从这段分析出,这是一个事务,事务id = 225089658 ,
死锁的语句是:涉及的表是 tenant_account_statistics_details 这张表

UPDATE `saas_application`.`tenant_account_statistics_details`
    SET status                 = 1,
        actual_settlement_time = NOW()
    WHERE order_no = 'O2305101612286510'
      and `type` = 0

涉及到 lock_mode X locks 是排他锁,行锁的一种。对应锁的索引是 index index_shopid_belongid 这是一个联合索引,shop_id 和 belong_id 的联合索引。

分析

1.这个事务操作获取到了表 tenant_account_statistics 的 index index_shopid_belongid 索引对应的行锁。
2.这个事务操作此时等待表 tenant_account_statistics_details 的 primary 主键索引

分析代码

image.png

settlement(jsons)代码如下


image.png

死锁分析

1.首先查看系统的数据库,mysql的隔离级别是read-commited ,读已提交,所以所有的查询都不会加锁。直接排除代码里面大事务里面的查询,造成的行锁问题。
2.那么只有update造成的行锁互相竞争,我们将事务中update操作的sql都查看一遍。发现就是更新两张表,一张是 tenant_account_statistics_details ,一张表是 tenant_account_statistics 。
3.分析死锁产生的流程:先看下面的执行sql

## SQL的执行顺序,也就是加锁的顺序
## ===============================================111111
UPDATE tenant_account_statistics_details
    SET order_time      = #{orderTime},
        settlement_time = #{settlementTime}
    WHERE order_no = #{orderNo}
      AND type = 0
      AND status = 0
## ================================================222222
UPDATE `saas_application`.`tenant_account_statistics`
    SET settlement   = settlement - #{amount},
        to_account   = to_account + #{amount},
        withdrawable = withdrawable + #{amount}
    WHERE shop_id = #{shopId}
      AND belong_id = #{belongId}
## =================================================333333
UPDATE `saas_application`.`tenant_account_statistics_details`
SET status                 = 1,
    actual_settlement_time = NOW()
WHERE order_no = #{orderNo}
  and `type` = #{type}
image.png

事务在执行的时候,会同时去抢夺 tenant_account_statistics_details 和 tenant_account_statistics 两张表对应记录的行锁,并且持有的锁只有在事务提交或回滚后才会释放。
事务1和事务2同时执行,并且操作两张表的同一条记录的时候,极端的情况是:
事务1获得了 tenant_account_statistics_details 表的对应记录的行锁,等待 tenant_account_statistics 表的行锁。
事务2获得了 tenant_account_statistics 表的对应记录的行锁,等待 tenant_account_statistics_details 表对应的行锁。
此时形成了 两个事务相互等待对方的锁释放,无法回滚,无法提交,造成了死锁,数据库在超时后,会回滚undolog中操作影响小的事务。

死锁解决

死锁的原因找到了,然后就是解决思路:
1.首先避免同一时间操作对应表的对应的同一条记录。业务的逻辑无法避免这块,除非重新设计。
2.其次如何让事物操作所消耗的时间尽可能的短,这样就能减少两个事务操作同一条记录的冲突行为。排查一下sql,以及对应表的索引,发现

UPDATE tenant_account_statistics_details
    SET order_time      = #{orderTime},
        settlement_time = #{settlementTime}
    WHERE order_no = #{orderNo}
      AND type = 0
      AND status = 0

这个sql语句中根据 order_no去做更新操作,而order_no不是索引,所以会引起全表扫描,从而增加了这行sql的执行时间,从而也加大了整个事务的执行时间,从而提高了两个事务同时操作两张表对应的同一条数据概率,造成死锁的几率也大大提高了。
于是我给 tenant_account_statistics_details 表添加了 order_no 索引,再本地做了并发是500的压测,发现不再出现死锁现象。从而解决了业务上死锁造成的数据错误问题。
3.最后,我们还可以在这个大事务上加一个分布式锁,让所有的事务串行化执行,这样肯定也不会造成死锁,但是会减少接口的并发吞吐量。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,826评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,968评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,234评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,562评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,611评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,482评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,271评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,166评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,608评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,814评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,926评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,644评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,249评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,866评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,991评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,063评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,871评论 2 354

推荐阅读更多精彩内容