并发情况下长事务的死锁问题分析及解决方案设计

好久没有更新了啊

Posted by LEODPEN on 2021-04-28

说在一切之前

各种博客死锁问题的分析和demo还蛮多的,但是真正涉及实际业务的并发情况下的死锁场景后,发现情况还蛮丰富的啊。。。

此外,这篇博客不会讲述具体理论,阅读需要一定的前置知识,比如事务隔离级别、相关锁的概念、死锁条件及加锁原因和场景等等。

背景图与正文无关,只是偶然看见,烬和男枪都被换弹阻塞,结局会怎么样呢?

问题出现

场景

公司每天都会有少量线上死锁问题告警(com.mysql.jdbc.exceptions.jdbc4.MySQLTransactionrollbackException)。大致情况是凌晨x点有上游的定时任务调了批量修改券批次的服务接口。相关表有多个,包含1对1、1对n等关系,博客中会对问题进行简化。

前置处理

环境信息获取

到公司数据库管理平台查看相应问题出现频率以及死锁日志。我截取了近几天的死锁日志并进行归类,分出了最常见、最不常见、较一般(简单)情况三类。

此外,确定线上的数据库版本为5.X(是的,就是大家都在用的那个X版本),事务隔离级别为RR。

查看问题影响

查看日志显示的被rollback事务,发现数据库里相关事务最终依旧成功commit执行,猜测是上游收到该请求失败result后进行了再次请求。

因此目前看来对实际业务无影响,只是会每天告警。

问题分析

事务中涉及到多个表,但此处对于其他表的修改不再展示,只展示导致问题出现的两个表的修改,两个表分别为e 表、a(名称已简化),后续i 代表 insert,d代表delete

事务执行流程简示

对流程简图进行展示:

(配图错了,最后一个是向author表插数据)

表结构分析

这里对相应的两个表简化并改名,具体代表什么见上面的加粗示意。

E表

1
2
3
4
5
6
7
8
9
CREATE TABLE `E` (
`ID` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
`cid` INT(11) NOT NULL,
`key` VARCHAR(128) NOT NULL,
`other` VARCHAR(255), # 其他字段不展示,以此字段代替
PRIMARY KEY (`ID`),
UNIQUE KEY `UX_CID_KEY` (`cid`,`key`),
KEY `IX_CID` (`cid`)
)

值得注意的是表中有一个为聚合索引的唯一索引(cid, key)以及一个非唯一索引(cid);

A表

1
2
3
4
5
6
7
8
CREATE TABLE `A` (
`ID` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
`cid` INT(11) NOT NULL,
`author` VARCHAR(128) NOT NULL,
PRIMARY KEY (`ID`),
KEY `IX_AUTHOR` (`author`),
KEY `IX_CID` (`cid`)
)

a表则需要注意的是只有一个非唯一键索引(cid);

情况汇总

具体日志信息不便放出,这里对相关事务的锁持有与锁等待等等情况进行汇总,在后续小节的分情况分析时会给出简化版本等待锁的SQL语句。

情况分类 事务1持有锁 事务1等待锁 事务2持有锁 事务2等待锁 回滚事务
一般(较简单) - index IX_CID of table A ; lock_mode X locks gap before rec insert intention index IX_CID of table A ; lock_mode X index UX_CID_KEY of table E; lock_mode X 2
最常见(次数最多) - index UX_CID_KEY of table E; lock_mode X index UX_CID_KEY of table E; lock mode S index IX_CID of table A; lock_mode X locks gap before rec insert intention 1
最不常见(较极端) - index IX_CID of table E; lock_mode X locks gap before rec insert intention index IX_CID of table E; lock_mode X index UX_CID_KEY of table E; lock_mode X locks rec but not gap 2

分情况分析

高并发场景下,难以完全复现问题,只能通过部分复现以及理论进行推推演;如果有误,请于评论指出!

此外,在具体SQ中,只截取cid后3位;

一般情况(较简单)

最一般情况下,其时SQL分别为:

  • 事务1:insert into A (cid, Author) values (103, ‘null’ );

  • 事务2:delete from E WHERE ( cid = 113 );

第4步再说明:

对于唯一键的插入操作,会找到其后的记录,如果其上有gap锁(此时没有),就会加insert intention lock进行等待;

PS:说明这里insert操作是完整的。

最常见情况

最常见情况下,主要表现为出现S锁,对于其出现,其时SQL分别为:

事务1:delete from E WHERE ( cid = 205 );

事务2:insert into A (cid, Author ) values (203, ‘null’ );

几个可能的问题:

  • 事务2第四步,为什么是持有S锁?

    答:唯一性检测时,发现插入项被加了锁(被前面delete了但是未purge),于是需要获取S锁(锁不止一个记录,因此是范围S锁而不是not gap);

  • 既然是持有S锁,那么可不可能是有重复请求,导致duplicate key(1062)错误,因此持有S锁且不升级X锁?

    答:最开始有猜想是此情况,但是通过查看数据库发现,相应cid对应的e记录并未出现重复记录,故排除此原因(因为e表无唯一性限制,如果重复,则会出现相同记录);

最不常见情况(较极端)

最不常见情况下,其时SQL分别为:

事务1:insert into E (cid, key, other) values (214, ‘xxx’, 'xxx) , (214, ‘yyy’, ‘yyy’) , (214, ‘zzz’, ‘zzz’) , ***;

事务2:delete from E WHERE ( cid = 230 );

需要注意的点:
因为cid既是非唯一索引,又是唯一索引的前缀组成部分,因此insert时候,除了要走唯一键流程,还要走非唯一键流程,这里就是唯一键流程OK,非唯一键流程卡主了;

解决方案设计

限制考虑

解决时考虑限制:

  • 需要考虑到是多表的操作,一般情况下原子性不能丢失(所有成功才算成功);

  • 保证原流程不受影响(出问题切回去,甚至设置批次id级别的灰度试试);

  • 尽量别引入其他中间件(复杂了反而不好);

方案1

修改RR模式为RC模式,消除间隙锁,以大大减低死锁发生概率;

方案2

本来方案2有三种,这里直接给出其中我个人认为最友好的一种;

主要思路:

  • 拆分大事务为小事务,并分情况选取流程;
  • 是否强制原流程应有其他线上配置管理;
  • 加一个简单记录表作为补偿日志表;【表结构可为(id, cid, param_str, addTime, status)】(Gson转换入参bean);
  • 对其他欲接入的有定时修改批次需求的上游一个新接口,里面包含参数(Option,option内暂有一个字段即是否走新流程);【加新接口或原接口内改动】;

改造后流程图:

几个问题

  • 这样设计会不会还有死锁?该怎么避免?

    答:超极端情况下会有,例如情况3;删除IX_CoupongroupId即可,其实也是可以删除的,毕竟已经有了唯一索引了。。。

  • 为什么c等其他表不需要拆出来进行事务?

    答:不需要,看其表结构,cid为主键一部分,删插不会引发死锁;所以这一块还是尽量保证原子性,以求真正处于中间状态的券批次的中间数据少;

  • 拆分出来的三个事务的执行先后顺序怎么确定?
    答:可以更改,图中这样的顺序主要是为了出现极端问题快速失败;

  • 涉及多表的操作,如果出现中间数据状态怎么办(改了一半)?

    答:新流程,只有在e表或a表出现失败现象后出现数据中间状态(非原数据状态、非更新后数据应有状态),概率较低,且考虑到1. 凌晨n点操作; 2. 能通过补偿快速恢复; 因此新流程中信息的完整性与正确性有一定保障;
    老流程则无影响,大事务,不存在中间状态;

  • 什么时候走新流程?

    答:配置管理设置为“新流程可用” && (来自上游定时任务 && 非补偿请求 || 指定为新流程);

  • 如何判断是补偿请求?

    答:来自特定上游的请求,且补偿记录表中有相应cid的记录且status为待补偿;

方案3

鸵鸟法,看起来线上无影响,不如得过且过好吧。

方案对比及思考

建议方案2 或者 3吧,如果上游能改一下代码,则还可以考虑省略加表。

在涉及到并发批量修改(有insert)事务时,最好要少用唯一索引!!!


写在最后

一些参考链接:

如果有误,请一定指出!

开头结果揭晓好吧