记一次数据库问题定位
2019年7月25日 17时09分
记录一次排查数据库insert的问题
引入 #
最近遇到一个服务问题,简单的描述如下:
- 00:00:01服务A成功向数据库中insert了一条记录;
- 00:00:02 服务B执行数据库查询,查询为空。
对于这个问题,我怀疑问题产生可能因为下列原因:
- 服务本身有问题,服务A insert 动作在 服务B查询之后:
- 可能是内部bug
- 可能是事务原因,比如A在inert中开启了事务,00:00:02之后才执行commit。
- 数据库主从延迟,比如服务A写入了主库,服务B却读取了从库,如果主从延迟到1s,那么上述状况属于正常。
接下来就根据这俩思路进行调查,最终还是怀疑db延迟,但是也说下调查中发现的一些有意思的点。
详细问题 #
首先扫一遍代码,确定代码的执行顺序,明确B的查询肯定是在A的insert之后。那么只能是事务问题了。
在调查事务的前提是如何知道代码在执行中开启了事务呢,一个最简单的方案看这里-北京瓜的博客-《启用mysql日志记录执行过的sql》
简单来说就是在mysql中开启sql日志,通过SET autocommit=0 来判断是否开启事务。
我这里复述下开启sql日志的过程,本质上是在mysql中开启日志配置。
临时开启日志的话,下列两个命令看下
SHOW VARIABLES LIKE "general_log%";

如果general_log=OFF的话,执行下面的sql开启即可。
SET GLOBAL general_log_file = '/var/lib/mysql/localhost.log';
SET GLOBAL general_log = 'ON';
开启之后,可以看到所有的sql都会打印到日志中。

事务和非事务的区别,就在于是否 autocommit=1。
回到服务中,查看服务的代码,看到服务代码中使用了@Transactional注解,但是实际的执行当中,没有开启事务。
通过sql日志可以看到,最终还是 SET autocommit=1

但是有意思的地方在于,这一次执行当中,有3次 SET autocommit。
回看代码,发现有一个advice很有意思。
<aop:config>
<!-- 分表默认不加所有的事务-->
<aop:pointcut id="tableMapperOperation"
expression="execution(* com.x.*XMapper.*(..))"/>
<aop:advisor advice-ref="noTxAdvice" pointcut-ref="tableMapperOperation"/>
</aop:config>
<!-- 此处的 transaction-manager 可以不指定,默认找 transactionManager-->
<tx:advice id="noTxAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="*" propagation="NOT_SUPPORTED"/>
</tx:attributes>
</tx:advice>
实际的方法栈是长这样子的:

可以看到,一个简单的controller-service-dao的调用链,使用了spring框架,增加了若干拦截器之后,变成了如此复杂的结构。
注意其中画红框的部分:
- controller层自不必说,肯定是一个代理,因为要统计日志,统一处理异常等。
- service层注意,由于有doInTransaction,这里service本体是个代理,而且还是
xxx$$EnhancerBySpringCGLIB,说明是使用CGLIB生成的代理。上图是使用编程型事务(TransactionTemplate)的调用方法栈截图,下图则是使用了@Transactional 注解引入事务的截图。
- dao层,这就是上面那个noTxAdvice发挥作用的时候了,可以看到,这里的mapper其实是个代理,是使用jdk生成的代理
jdkDynamicAopProxy。
答案比较清晰了,首先使用 @Transactional在service层配置了事务,但是在dao层,又通过xml配置型advice设置了不开事务,最终dao层的代码先执行,没开事务。
要问为啥代码这个样子,挖坑不填,出来混,总是要还的。
more than @Transactional #
既然发现了这么有意思的东西,突然想看下 事务中 不同的 Propagation(事务传播)在数据库层面会如何表示和实现。
Propagation取值 #
REQUIRED(默认值) #
在有transaction状态下执行;如当前没有transaction,则创建新的transaction;
这是最普通的状态,一般这是最开始的入口方法,采取这个方案。
实现在mysql中,就是这么一句:
2019-07-29T13:07:18.364347Z7482451 Connect root@localhost on xxxdatabase using TCP/IP
2019-07-29T13:07:18.366558Z7482451 Query SET NAMES utf8
2019-07-29T13:07:18.367043Z7482451 Query SET autocommit=1
2019-07-29T13:07:18.367370Z7482451 Query SET autocommit=0
本质是创建一个connection,并且设置autocommit=0。
SUPPORTS && NOT_SUPPORTED #
这俩状态是随遇而安的典型,都不会抛异常,安安静静的执行。前者是有事务就用事务,没有就不用,不主动,不拒绝。后者是不管之前有没有,反正到我这里就没有
SUPPORTS:如当前有transaction,则在transaction状态下执行;如果当前没有transaction,在无transaction状态下执行;
NOT_SUPPORTED:在无transaction状态下执行;如果当前已有transaction,则将当前transaction挂起。
这个实现方式就是上文中说过的,重新开连接,并且设置一个autocommit=1。
MANDATORY && NEVER #
这俩状态是强制的典型,不符合预期就抛异常
MANDATORY:必须在有transaction状态下执行,如果当前没有transaction,则抛出异常IllegalTransactionStateException;
NEVER:在无transaction状态下执行;如果当前已有transaction,则抛出异 IllegalTransactionStateException。
REQUIRES_NEW #
创建新的transaction并执行;如果当前已有transaction,则将当前transaction挂起;

从图中可以看出,这是通过创建一个新的TCP连接实现的,本质上和外部的事务已经没有关系了。
这就导致下列的结论
- 内部的方法和外部的不是一个事务,无法保障同时成功失败。
- 内部的方法rollback,外部事务正常不会受到影响。