起因是最近两天收到了线上项目的告警通知,隔一段时间会出现几笔MySQL的死锁Deadlock的错误,错误日志如下:

updating database. Cause: com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Deadlock found when trying to get lock;
try restarting transaction ### The error may exist in com/.../XXXMapper.java (best guess)
### The error may involve com.....MsgFrameSmsMapper.update-Inline
### The error occurred while setting parameters
### SQL: UPDATE msg SET success_count=success_count+1 WHERE (batch_name = ?)
### Cause: com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction ;
Deadlock found when trying to get lock;
try restarting transaction; nested exception is com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Deadlock found when trying to get lock;
...

看了下日志位置以及执行的sql定位到了功能模块,其模块的功能概括如下图:

三方服务会给我们推送批次号以及状态的回调通知,一次通知中可能会包含多个批次号,每个批次号可能会产生多个子状态,每个子状态都需要通知到我们,我们根据子状态来决定来更新表中的数量字段。

库使用的引擎是InnoDB,batch_name加了索引,找DBA使用 SHOW ENGINE INNODB STATUS 命令查询了下mysql死锁的日志:

LATEST DETECTED DEADLOCK
------------------------
2025-05-27 10:00:29 0x7fbbb7ce7700
*** (1) TRANSACTION:
TRANSACTION 29608052, ACTIVE 0 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 42 lock struct(s), heap size 8400, 47 row lock(s), undo log entries 75
MySQL thread id 11897, OS thread handle 140446306776832, query id 54085329 10.16.2.173 user_sms updating
UPDATE msg SET success_count=success_count+1 WHERE (batch_name = '1927183012550696960')
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 188 page no 81 n bits 248 index ix_batch_name of table `ums_sms`.`msg` trx id 29608052 lock_mode X locks rec but not gap waiting
Record lock, heap no 98 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
0: len 19; hex 31393237313833303132353530363936393630; asc 1927183012550696960;;
1: len 8; hex 1abebabd41802001; asc A ;; *** (2) TRANSACTION:
TRANSACTION 29608051, ACTIVE 0 sec starting index read
mysql tables in use 1, locked 1
31 lock struct(s), heap size 3520, 44 row lock(s), undo log entries 79
MySQL thread id 11906, OS thread handle 140444219373312, query id 54085339 10.16.2.173 user_sms updating
UPDATE msg SET success_count=success_count+1 WHERE (batch_name = '1927183018338836480')
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 188 page no 81 n bits 248 index ix_batch_name of table `ums_sms`.`msg` trx id 29608051 lock_mode X locks rec but not gap
Record lock, heap no 78 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
0: len 19; hex 31393237313832393939393830333731393638; asc 1927182999980371968;;
1: len 8; hex 1abebaba54402001; asc T@ ;; Record lock, heap no 93 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
0: len 19; hex 31393237313833303038333934313435373932; asc 1927183008394145792;;
1: len 8; hex 1abebabc49802001; asc I ;; Record lock, heap no 98 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
0: len 19; hex 31393237313833303132353530363936393630; asc 1927183012550696960;;
1: len 8; hex 1abebabd41802001; asc A ;; Record lock, heap no 99 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
0: len 19; hex 31393237313833303132383639343638313630; asc 1927183012869468160;;
1: len 8; hex 1abebabd54401001; asc T@ ;; Record lock, heap no 100 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
0: len 19; hex 31393237313833303133383637373038343136; asc 1927183013867708416;;
1: len 8; hex 1abebabd90001001; asc ;; Record lock, heap no 141 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
0: len 19; hex 31393237313833303139323533313934373532; asc 1927183019253194752;;
1: len 8; hex 1abebabed0c02001; asc ;; Record lock, heap no 157 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
0: len 19; hex 31393237313833303231303339393732333532; asc 1927183021039972352;;
1: len 8; hex 1abebabf3b802001; asc ; ;; *** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 188 page no 81 n bits 248 index ix_batch_name of table `ums_sms`.`msg` trx id 29608051 lock_mode X locks rec but not gap waiting
Record lock, heap no 129 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
0: len 19; hex 31393237313833303138333338383336343830; asc 1927183018338836480;;
1: len 8; hex 1abebabe9a802001; asc ;;

意思就是:

事务1 (29608052) 尝试执行sql语句:

UPDATE msg SET success_count=success_count+1 WHERE (batch_name = '1927183012550696960');

等待锁定的记录是batch_name = '1927183012550696960'

事务2(29608051) 执行了几个批次的update语句,其中包括:

UPDATE msg SET success_count=success_count+1 WHERE (batch_name = '1927183012550696960');

也就是事务2已经获得了batch_name = '1927183012550696960'的排它锁,然后等待锁定的记录是batch_name = '1927183018338836480',也就是尝试执行下面的sql语句:

UPDATE msg SET success_count=success_count+1 WHERE (batch_name = '1927183018338836480');

根据mysql的死锁日志可以推断出,事务1在等待batch_name='1927183012550696960'也就是被事务2执行的排它锁(堆号98),事务2在等待batch_name='1927183018338836480'的排它锁(堆号129),而这条记录被事务事务1(或其他事务,但根据日志上下文可推断为事务1)间接依赖或锁定,以此形成了循环依赖。

项目中会通过p6spy打印sql的执行日志,根据traceId看了下批次号1927183012550696960和1927183018338836480都存在于两条MQ消息中,说明这两个批次号都接收到了两次通知,这个三方服务本来只有一家,当时对接的时候约定同一个批次号下的子状态会聚合,不会出现一个批次号通知多次的场景,但是后面随着后面其它厂商的接入,对接的时候没有确定这一点,导致了一个批次号多次推送从而出现了上述的问题,并且这个问题也不是必现,正好要碰到这样的场景才会触发。

好在一条MQ消息中的某一个批次号执行失败,整个MQ消息的事务会回滚,然后其中一条MQ消息进入重试队列,重试之后就不会出现事务争抢的情况,因为MySQL会选择死锁中的一个事务执行成功,根据MySQL日志观察是事务2被回滚了(事务2回滚的日志没有贴出),事务1被提交。

毕竟有这些错误日志告警持续产生,得想办法解决了,首先想到的就是化数据库的锁为redis的锁了,也就是在执行某一个批次号的更新动作时,先使用redis获取这一个批次号的锁,获得锁之后进行操作,否则等待,不过这还牵扯出一个新的问题,就是一条消息里面的多个批次在处理时是一个完整的事务,只有等待所有批次处理完成后,才能释放掉这条消息中每一个批次的锁,初步方案如下:

方案不够好,还有优化的空间,想了下,可以借助MQ的唯一消息id和重试队列来实现,缩小事务范围,将一条消息中的批次号作为一个事务,将循环批次的代码放在事务外面,一个批次处理成功、事务提交成功之后通过redis记录,key是MQ的msgId+批次号,value按需设置,我这边存储的是批次号处理成功时候的时间戳,因为后面重试这条消息里面处理失败的批次号时,跳过处理这条消息里面成功的批次号是根据msgId+批次号去redis里查询的。

项目上使用的MQ是RocketMQ,RocketMQ从4.7.1版本开始,一条消息重试时消息的msgId会变化,但是可以通过读取消息中的属性UNIQ_KEY来获取原始的msgId,RocketMQ的MessageClientIDSetter类中也提供该方法:

public static String getUniqID(final Message msg) {
return msg.getProperty(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX);
}

那么改版后的方案如下:

这个方案的好处就是一个事务里面不会牵扯到多个批次号,化大事物为小事物,也就避免了交叉获取批次号的锁从而发生死锁的问题,也不用使用redis对每一个批次号进行加锁,跨事务操作更新某一个批次号的数据时,其中一个事务会等待另一个事务提交之后执行。

记一次项目上MySQL死锁Deadlock的排查优化过程的更多相关文章

  1. 记一次排查线上MySQL死锁过程,不能只会curd,还要知道加锁原理

    昨晚我正在床上睡得着着的,突然来了一条短信. 啥,线上MySQL死锁了,我赶紧登录线上系统,查看业务日志. 能清楚看到是这条insert语句发生了死锁. MySQL如果检测到两个事务发生了死锁,会回滚 ...

  2. 记一次线上MySQL数据库死锁问题

            最近线上项目报了一个MySQL死锁(DealLock)错误,虽说对业务上是没有什么影响的,由于自己对数据库锁这块了解不是很多,之前也没怎么的在线上碰到过.这次刚好遇到了,便在此记录一下 ...

  3. 记一次Win上MySQL乱码问题

    Win上MySQL乱码问题 笔记本上的数据库总会时不时的乱码(或者是一直乱码我没注意到?),在谷歌上试了几次错才正确解决,在此记录一下. 在MySQL数据库存储目录找到my.ini,在相应的标签下分别 ...

  4. Mysql死锁如何排查:insert on duplicate死锁一次排查分析过程

    前言 遇到Mysql死锁问题,我们应该怎么排查分析呢?之前线上出现一个insert on duplicate死锁问题,本文将基于这个死锁问题,分享排查分析过程,希望对大家有帮助. 死锁案发还原 表结构 ...

  5. 原创 记录一次线上Mysql慢查询问题排查过程

    背景 前段时间收到运维反馈,线上Mysql数据库凌晨时候出现慢查询的报警,并把原始sql发了过来: --去除了业务含义的sql update test_user set a=1 where id=1; ...

  6. 一次线上mysql死锁分析

    一.现象 发运车次调用发车接口时发生异常,后台抛出数据库死锁日志. 二.原因分析 通过日志可以看出事务T1等待 heap no 8的行锁 (X locks 排他锁) 事务T2持有heap no 8的行 ...

  7. 记一次线上Mysql数据库 宕机

    从发现问题,到最后解决一共消耗两个半小时(7:30~10:00),报警电话16通,警察坐镇,未完待续 ......

  8. 记一次FTP上传文件总是超时的解决过程

    好久没写博,还是重拾记录一下吧. 背景:买了一个阿里云的云虚拟机用来搭建网站(起初不了解云虚拟主机和云服务器的区别,以为都是有SSH功能的,后来发现不是这样样子啊,云虚拟机就是FTP上传网页+MySQ ...

  9. 一则线上MySql连接异常的排查过程

    Mysql作为一个常用数据库,在互联网系统应用很多.有些故障是其自身的bug,有些则不是,这里以前段时间遇到的问题举例. 问题 当时遇到的症状是这样的,我们的应用在线上测试环境,JMeter测试过程中 ...

  10. J2EE Oa项目上传服务器出现的乱码解决过程

    (= =)搞了许久觉得有必要记下来.. 由于我本地的mysql都设置好了,但是服务器的又不能去改它 毕竟还有其他人要用- -: 所以只能是我建的时候去设置一下了, 首先先建数据库 ,表;; creat ...

随机推荐

  1. HarmonyOS Next开发教程之地图定位

    今天分享一下在鸿蒙开发中的地图定位问题,也就是在地图中如何定位自己所在的位置. 关于如何加载显示地图在之前的文章已经详细介绍过,有问题的友友可以点击查看: HarmonyOS NEXT实战教程-实现K ...

  2. K8s 部署一套 MySQL 集群

    一般情况下 Kubernetes 可以通过 ReplicaSet 以一个 Pod 模板创建多个 pod 副本,但是它们都是无状态的,任何时候它们都可以被一个全新的 pod 替换.然而有状态的 pod ...

  3. ISO镜像做yum源

    先上传一个镜像文件 centos-7-x86-1708.iso 挂载 mount -o loop /root/centos-7-x86-1708.iso /file 设置开机自动挂载 vi /etc/ ...

  4. 球体的顶点与索引创建方法----以WebGL为例

    上图,左图为一个球体的三维图,其中一个圆面以θ角(范围为[0,PI])的方式确定,该圆面在x-z坐标平面投影如右图,其中圆面上任意一点又由α确定(范围为[0,2PI]). 假定该球体半径为r,那么球面 ...

  5. SSI注入

    .stm,.shtm和.shtml后缀文件中可以如此执行命令 <!--#exec cmd="ls"-->

  6. 转:基于Redis实现延时队列

    摘要:使用 sortedset,拿时间戳作为score,消息内容作为 key 调用 zadd 来生产消息,消费者用 zrangebyscore 指令获取 N 秒之前的数据轮询进行处理.    前段时间 ...

  7. 解决DevToolsActivePort file doesn't exist

    今天遇到个小问题:selenium 启动 chrome crash,报错:DevToolsActivePort file doesn't exist. 在option中添加一下几行: option = ...

  8. 洛谷 P3945 三体问题

    洛谷 P3945 三体问题 在物竞dalao的帮助下(简化下?)终于A了此题,于是在他的提议下来喷出题人. 接下来看题. 题意分析 模拟三维空间中\(n\)个星体的运动,求\(Ts\)后\(n\)个星 ...

  9. python开发之路【第二章】:python简介和入门

    Python简介 python起源到广泛应用 Python 由吉多・范罗苏姆(Guido van Rossum)缔造.1989 年圣诞季,身处阿姆斯特丹的他,为了打发闲暇时光,决定开发一款新脚本解释程 ...

  10. 做Data+AI的长期主义者 | 倒计时2天...

    <数据资产管理白皮书>下载地址: https://www.dtstack.com/resources/1073/?src=bbs <行业指标体系白皮书>下载地址: https: ...