问题场景:
一个功能使用多线程并发处理数据,如果子线程发生异常,主线程捕获异常,并往外抛出,一直抛到最外层带事务的调用方,将某些数据状态进行回滚。这个带事务的函数里调用了多个带事务的函数。
由于外部函数已带有@Transaction注解,则直接将调用多线程那部分层层封装出一个函数出来,并加上@Transaction注解,throws exception,在可能会出现异常的地方用try-catch住异常,层层往外抛。外部函数在调用该函数的地方catch住异常。以此实现该目标函数回滚,外部函数不回滚的效果。
出现问题:
当子线程发生异常时,主线程没有抛出异常,部分 代码如下:
try{
for (int i = 0; i < xxx; i++) {
// 错误信息
final List<BizException> err = new ArrayList<BizException>();
pool.execute(new Runnable() {
@Override
public void run() {
try {
dealUWEngineService(a,b,c);
} catch (BizException e) {
err.add(e);
}
}
});
}
}
while (true) {
if (err.size() > 0) {
throw new Exception("xxx");
}
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
if (pool.getActiveCount()==0) {
break;
}
}
} finally {
if (pool != null) {
// 关闭线程池
pool.shutdown();
}
}直接在子线程throw异常时无法传递到主线程的,所以使用一个list集合,来记录子线程有没有抛出异常,进行主子线程通信,然后再进行轮询看err里面有没有出现异常,如果线程池中的活动线程为0,则说明子线程都已经执行完毕,则退出轮询,但是getActiveCount()判断具有时效性,具体原因,可自行了解。
但是debug的时候发现,当过了err的大小判断时,此时子线程还没有跑完,或者出现异常的子线程还没有执行,然后等到getActiveCount时,线程执行完毕,活动线程为0,退出了轮询,直接默认没有异常出现。所以该方案不完善。
问题解决:
在finally里面再对err做一次size判断,或者减小sleep时间,这样会对性能产生一些影响。
问题出现:
当目标函数顺利抛出异常时,最外部函数所有操作都进行了回滚,并且spring内部抛出异常:
- 当整个方法中每个子方法都没报错时,整个方法执行完才提交事务,如果某个子方法有异常,spring将该事务标志为rollback only,在被标记后和将该异常抛出前,想去执行数据库的话是不允许的。
- 此时即使没有将异常抛出,但是如果继续去操作数据库的话,一样是会报Transaction rolled back because it has been marked as rollback-only的异常,倘若就是想将该异常记录到数据库该怎么办?重新启动一个独立事务去处理。
- 如果这个子方法没有将异常往上整个方法抛出或整个方法未往上抛出,那么该异常就不会触发事务进行回滚,事务就会在整个方法执行完后就会提交,这时就会造成Transaction rolled back because it has been marked as rollback-only的异常,如果我们往上抛了改异常,spring就会获取异常,并执行回滚。
这里就跟spring中的事务传递机制有关,spring事务有7种传播行为:
- PROPAGATION.REQUIRED:如果当前没有事务,就创建一个新事务,如果当前存在事 务,就加入该事务,该设置是最常用的设置。
- PROPAGATION.SUPPORTS:支持当前事务,如果当前存在事务,就加入该事务,如果 当前不存在事务,就以非事务执行。
- PROPAGATION.MANDATORY:支持当前事务,如果当前存在事务,就加入该事务,如 果当前不存在事务,就抛出异常。
- PROPAGATION.REQUIRES_NEW:创建新事务,无论当前存不存在事务,都创建新事 务。
- PROPAGATION.NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把 当前事务挂起。
- PROPAGATION.NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。
- PROPAGATION.NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事 务,则执行与PROPAGATION_REQUIRED类似的操作。
问题解决:
我catch住了异常,但是不能将该异常往外抛,这样会全部回滚,不抛又会报错。
- 新建一个不带事务的函数去调用目标函数,然后外部再去调用该函数?经验证,不行。。。
这样实现的效果和直接调用目标函数没区别,too simple。
- 所以我们需要修改一下目标函数的事务传播机制。PROPAGATION.REQUIRES_NEW 这种事务传播机制很明显符合我们的需求,新建一个新的事务来处理目标函数,成功解决问题。
注意事项:
由于此时新建了事务,如果原来的事务和新的事务操作的记录有重叠的部分,会容易发生死锁,所以要多加小心(我就是因为在新建事务前,给select加了锁,导致新事务无法获取锁,所以得把该操作和到新事务中),关于如何有效避免死锁,可以自行了解。