记一次数据库or和and优先级引发的连环生产故障

故障描述:

自上个月某个功能改动上线以后,最近生产上连环出现了多个生产故障,故障基本描述如下:
error日志出现数据库连接异常,而实际交易量似乎并没有到达耗尽连接池的地步。
数据中某个字段无值,不符合正常设计推理的流程。

故障描述

数据出现诡异的结果,和关联的业务日志显示的完全不一样,业务日志显示成功,数据库记录却是成功。

场景描述

在详细说明上述故障之前,需要简单描述一样具体的业务场景,大概是这样:

我们系统需要和外部系统进行交互,交互的过程中会在日志和数据库中都记录交易的状态,使用mq消息队列交互。
数据库涉及到的关键字段是:status代表状态,time代表响应时间;
发送请求前,status设置为02,代表“收到请求,本系统处理中”;
发送请求并接到同步响应后,status设置为03,代表“请求成功,外部系统处理中”,同时记录响应时间time;
收到外部系统异步结果,结果是成功,status设置为00,代表“外部系统业务处理成功”;
收到外部系统异步结果,但结果是失败,status设置为01,代表“外部系统业务处理失败”;

原因分析

第一个问题,数据库连接异常,相关人员分析了很久后也没能找出是什么原因,导致当前交易量的情况下就连接耗尽。
第二个问题,因为同步响应才会保存time字段,异步通知不会保存time字段。
正常流程是先有同步响应后修改数据库,而后才是异步通知修改数据库,会进行status的判断。
但是出现问题的时候发现是异步通知先改数据库,同步响应后改,就导致status判断不过。这个问题是程序本身的缺陷所在,算是找到了解决办法。但是是什么原因导致了异步在前,同步在后?这又是一个未解问题。
第三个问题,一样是相关人员定位了很久,无法想通究竟是什么原因导致日志都成功的情况下,数据库字段是失败。

直到这三个问题聚合到一起,关联起来之后,才有数据库比较熟的同事发现了问题所在,这三个诡异的问题,实际上都是同一个问题导致,那就是sql语句中and和or使用不当,出了优先级的隐秘问题。
这个修改数据库的sql在mybits中大概是这样的:

update table1 
<set>
<if test="status!= null">
status=#{status},
</if>
<if test="time != null">
time=#{time},
</if>
</set>
where
<if test="name != null">
name=#{name}
</if>
<if test="age != null">
and age=#{age}
</if>
and status="01" or status="02"

这个sql的问题就在于,and的优先级高于or,同时使用了and和or的情况下,这里or前后的条件本该是整体,却没有用括号括起来。
导致or前边所有and连接的条件成了整体,就直接导致当status=‘01’不成立的情况下,就只剩下了一个条件,status=“02”。
然后这个操作就会更新数据当前status为02的所有记录,由一条变成了n条。
于是乎,就大大增加了数据库的负荷,使得原本可能只需消耗很短时间的update操作变得异常耗时,紧接着就导致后续很多数据库操作陆续从连接池获取连接,并且得不到释放,直到连接耗尽,出现第一个连接异常的问题。
同样的,由于数据库连接的问题,导致同步响应的更新操作一直等待,mq消息就无法消费,产生堆积。又由于同步响应和异步通知的mq队列不同,就导致最终异步通知mq队列先消费,同步响应的mq队列后消费,然后再加上程序本身的缺陷,导致第二个生产故障。
也是因为这样,原本可能根据name条件只update符合条件的一条数据,结果变成了更新所有status是02的数据,就会导致其他失败的订单更新数据库时,把其他订单的status也更新成失败,就会出现其他订单日志和数据库记录不一致的第三个生产故障。

解决方案

实际上分析出问题根本原因后,解决起来就很简单了,最简单的,就是把or优先级低的整体用括号括起来,如下:

update table1 
<set>
<if test="status!= null">
status=#{status},
</if>
<if test="time != null">
time=#{time},
</if>
</set>
where
<if test="name != null">
name=#{name}
</if>
<if test="age != null">
and age=#{age}
</if>
and (status="01" or status="02")

推荐文章