前言:
MySql数据库事务特性AICD,原子性、隔离性、一致性和持久性,本文要介绍的就是一个因为隔离性而导致的问题,该问题不具有普适性,因为带有了一小部分异常被覆盖的前置条件,该条件导致了问题更难发现
背景:
生产上有两个系统,一个接单系统,一个作业系统,作业系统负责接单系统下发的单子的执行任务,作业系统在某道工序,会产生一个任务插入数据库,该任务的执行由接单系统每分钟捞数据库执行,任务的生成和执行是分离的,所以存在一分钟延时,本次需求就是为了延时减到最低(最好立即发送),所以需要作业系统同步去调用接单系统发送任务。
现象:
改造完成后,发现该同步调用,总是RPC超时,而去接单系统查看,超时的原因是每到6秒就会抛异常,有了异常信息应该很好定位,然而异常却看不出任何信息,下面是该异常:
java.lang.reflect.UndeclaredThrowableException
被吃掉的异常 该异常产生的原因和JDK的动态代理、检查异常有关,简而言之,就是没有捕捉检查异常导致异常被包装成为 UndeclaredThrowableException 抛出,从而无法得知原始异常的信息。
因为该次任务有数据库操作,而且通过arths排查发现在数据库每次都跑6s多,刚开始以为索引问题,也以为是切了数据库的问题。但都被排除了
疑点:
问题的排查需要静下来思考的,而且要结合多个方面,如果思考实在没问题就要去了解框架机制和原理,这些都要求对机制比较熟悉,这次其实就是通过一些观察,最终发现了问题。
超时时间 6S :
多试了几次任务调用,发现超时时间都是6S,这个时间比较诡异,一看就知道是某种超时机制触发的,所以首先肯定,是数据库的超时机制。
Select for Update:
我们排查到了,超时发生在某个查询方法
public class OrderDao{ public Order queryById(String Id,Boolean lock){ ////查询,加锁 } }
该方法的查询操作有个参数,lock=true,查看实现是在查询数据的时候,用到了select for update的特性,该特性就是为查找的数据库加上行锁;
这个时候已经感觉到了一些端倪,这一定是数据库死锁了。
原子性:
同数据库的事务:
为了验证我的猜想,去看了作业系统生成任务的代码,该代码在一个事务(事务A)中,而且该事务操作了Order的表;执行任务的接单系统,也是在一个事务中(事务B),该事务也需要查询Order的表,而且该这行数据加了锁,一下子,真相便出来了。
事务时具有原子性的,为了保证这个原子性,数据库利用了 锁 这个计算机概念,保证加了锁的数据行不会被其他事务更改,所以,为了防止意外发生,下发任务和更改单据状态时在一个事物A中,保证了原子性,所以就给当前操作的order加了锁;同样,在事务A中同步调用作业系统产生的事物B为了原子性,为了for update操作必然也需要占有该行锁。因为锁已经被A获取,所以B只能等待,而A又等待B完成,所以就互相等待,导致死锁,超时抛异常;
一个方案:
那么更改一下这个查询,把 lock 参数该为 false,发现可行因为后面已经没有更新的操作了,所以当时这个select就是为了当前读,当前读?为什么要这些写呢?是不是原来的作者写错了呢?继续看下去
意外:
更改后,发现虽然不死锁了,但是查询的数据确实过时的数据,问题转移成为了另外一个问题,为什么是过时的数据呢?
隔离性:
不可行的方案:
其实当时一下子就想到了,我们数据库设置的隔离级别是:可重复度,因为同步调用,事务A还没结束,事务具有隔离性,所以事物B如果不是 select for update,根本不可能读到下发任务前在事务A中更改的数据。所以同步调用的方案是不可行的。只能异步,而且事务B也必须要是快照读,因为假设异步,事务A下发任务后在没有结束的时候事务B执行了,读到的数据还是旧的。
最终方案:
把事物A下发事物B的方案改成了由下发任务,换成rocketMQ通知,这样既能保证任务的快速执行,也能保证数据的正确性。