首先,我们的项目配置了全局事务,拦截的是ServiceImpl层中以add、save、insert、update等为方法名前缀的方法(主要拦截增删改操作)。场景是这样的,我们有好几个操作是先执行insert方法(同步方法),再执行saveMsg方法(异步方法),这两个方法都能被全局事务拦截到,事务的传播特性是Required,原则上来讲两个方法应该在同一个事物。一开始XxxServiceImpl中的代码大概是这样的:
public void insertXxx() {
// 先执行插入
insert();
// 中间会拼装一部分msg信息
doSomething...
// 然后调用 saveMsg
saveMsg(Msg msg);
}
saveMsg方法是用来拼装一条消息并存入数据库的,消息内容依赖于insert方法中插入的数据,但是有时候会出现消息内容错误的现象,最开始并没有怀疑是事务问题,以为是拼装消息的方法有bug,经过几次debug之后发现即使同一个操作这种问题也会偶现,所以排除了拼装消息这部分代码的bug。其实saveMsg方法本身并不是异步方法,没有加@Async这种注解,所以一开始也没有怀疑是事务的问题,但是这个方法中有个子方法A是在线程池中执行的,所以相当于是个异步方法,最开始saveMsg代码是这样的(作者不是我):
// saveMsg本身并不是异步方法,是因为里面用到了线程池
public void saveMsg(Msg msg) {
// A内部是拼装消息的代码,在线程池中执行的,相当于异步方法,A是在出现问题后才单独抽出来的
A(msg);
}
于是后面就怀疑saveMsg方法没读到insert方法插入的数据,怀疑两个方法不在同一个事物,且有时候saveMag方法的事物比insert方法的事物先执行。于是我也查了一下相关资料,代码改成了这样:
public void saveMsg(Msg msg) {
// 这段代码的作用是保证A方法在前面的事物提交完之后再执行
TransactionSynchronizationManager.registerSynchronization(
new TransactionSynchronizationAdapter() {
@Override
public void afterCommit() {
A(msg);
}
}
);
}
这样改了之后问题解决,也过了一段风平浪静的日子。终于在一个风和日丽的上午,新增一个功能:消息通知某某某,这个功能主要就是要调用saveMsg方法。开开心心的写完接口后,一测试就给我返报了个错,当时我那个懵逼呀。。。具体错误我忘记了,大概就是说在当前没有事务的情况下执行TransactionSynchronizationManager.registerSynchronization(…);这段代码有问题,看到报错我倒是很快反应过来,应该先判断是否存在事务。于是,又改了一版:
public void saveMsg(Msg msg) {
boolean synchronizationActive = TransactionSynchronizationManager.isSynchronizationActive();
if (synchronizationActive) {
TransactionSynchronizationManager.registerSynchronization(
new TransactionSynchronizationAdapter() {
@Override
public void afterCommit() {
A(msg);
}
}
);
} else {
A(msg);
}
}
终于,问题解决,到写这篇博客的时候已经过去几个月,再也没有出现问题。
回过头来想,当时如果考虑全面一点,一开始就判断事务是否存活,也就不会出现第二次的问题,改第一个问题的时候,局限于调用saveMsg之前一定会有insert操作,根本没有想过后面会有这种直接调用saveMsg方法的,当然这也是受了网上别人也没写这个判断的影响,也许他们的场景不同,不过也算是一次成长吧。