问题背景
一个接口里需要需要处理多个数据源时,必须分多个事务,多个事务无法回滚。
场景一:当系统为了性能而进行分库之后,我们的一个业务逻辑里就可能出现操作多个数据源的情况(当然,一般情况下在分库的就会考虑到把相关业务的放在一个机器上)。但也可能会出现奇怪的业务把完全不相干的业务揉到了一起。
场景二:对于一些不大的项目,有时候为了省事,几个团队甚至会约定,可以相互直接访问数据库(理论上是不应该的,有安全风险),如果数据库不在一台机器上,就也会出现事务回滚问题。
总之:就是项目不大,没有应用分布式事务框架,遇到的一两个特殊的需要分布式事务回滚的情况时,又不想改整体的数据库连接方案(整体改成分布式事务方案,也没有时间做全量测试)。
解决方案
不同事务的放在不同的线程里,通过线程间通信实现【你报错,我也跟着报错】
技术点
最重要的是:线程间如何通信,这里选择的是使用Callable和FutureTask
设计方案
假如没有事务回滚的问题,其实我们正常写逻辑,一般就是“同步”的,几个业务子模块一个接着一个跑就好了,所以这里选择以队列的形式,让各个线程一个接着一个的启动运行。
但是,前面的子模块要在结束前,监督后面的子模块运行完。
大致就是下图这个样子
当其中任意一个线程报错或超时,就会变成
上代码
这是一个通用的类,可以直接使用(如果使用过程中发现有bug,可以提出来,本人提供技术支持)。
用法步骤
下面通过测试demo,讲讲怎么使用这个类
测试类1(主测试类)
测试类2(被引用的类)
为了方便测试,这里并没有引入spring,用spring其实是一样的
上面两个类,一个相当于Controller,一个相当于Service,Controller的某个接口需要调用service里多个事务。
- 对自己的service接口进行改造,改造成
void xxx(Map param);
的形式。即没有返回值,参数只有一个Map。如果要返回值,请把返回值塞回参数Map里,在Controller里取出来。
2. 在Controller里,先创建一个核心类对象transactionQueue
TransactionQueue transactionQueue = new TransactionQueue();
- 使用transactionQueue创建一个Map参数(请勿自己new Map)
Map param = transactionQueue.param();
- 把自己原本需要往service里传的参数,都放到Map参数里
- 写对应的方法应用(jdk8的新特性写法)
Consumer<Map> f = service对象::方法名;
- 把service方法及参数add到transactionQueue里
transactionQueue.add(f, param, null);
这里一定要注意,f 和 param一定都要是这个service方法的,不同service方法不能共用。第三个参数是超时时间,可以预估一下这个方法要执行多长时间,一般写null就可以,默认给了1000毫秒的运行时间。
7. 重复步骤3,4,5,6, 添加第二个、第三个、第n个 service方法
8. 启动任务队列
transactionQueue.start();
- 对2 ~ 8的代码,用try-catch捕获,然后在finally里释放资源
if (transactionQueue != null) {
transactionQueue.clear();
}
- 至此,controller里的代码就写完了,下面剩最后一步,是在每个service里。
把下面这句话添加到业务逻辑的最后一行(如果原本代码里中间就有return;请自行加参数,实现只有一个出口,或者在每个出口前都加上这句话)。其中param就是我们传入的参数Map,这是一个监控点,是service队列的连接桥梁,负责监控它后面的service运行状况。
TransactionQueue.startMonitor(param);
注意:
- 每个service方法都要有自己的事务(加上
@Transactional(rollbackFor = Exception.class)
) - service方法里不要捕获运行时异常(RuntimeException),有异常通通抛给Controller去处理,否则很可能原本应该报错回滚的也被掩盖住了
- 如果service方法里有非运行时异常,比如demo里的InterruptedException,或者IOException等,一定要捕获(注意只捕获那一句话),如果service处理不了,可以转成RuntimeException抛出去(因为方法上一旦有throws xxxxException,就没办法写方法引用了)
这样,通过这个类就实现了不同service的事务间通讯,任意service方法报错,整个任务就会报错并回滚。
写法上确实有很多局限性,不过,应对为数不多的特例,这点改造也是可以接受的。
对于大规模的分布式场景,请考虑成熟的分布式框架。
结尾
这是本人花了半天时间把工作中的一个解决思路整理出来的通用工具类。如有更好的思路或者发现代码有bug隐患,欢迎交流指正。
其实我确实想找到一种能单独给项目增加JTA分布式事务,而且不对现有项目产生影响的方案(可以在普通事务和JTA分布式事务之间切换,默认还是普通事务),可惜没找到。如果你有现有方案或思路,还请指教,换掉现在这种“朴素的掉渣”的方案。