一、背景
实际的项目开发中,我们可能会遇到一个项目连接多个数据源的情况, 以为添加了@Transactional 注解就行了,加了注解之后会发现数据源并没有切换成功。原因:Spring框架提供的事务处理是针对数据库来说的,框架或者数据库本身并没有提供跨多个库的事务处理机制,所以使用@Transactional注解开启事务之后,当前spring的上下文中只能处理一个数据源的事务。
电商系统场景,一个订单提交操作,可能涉及到库存、会员、物流、优惠券等相关业务数据库的读写操作。理想的处理方式是按照业务的维度来分库分服务,各个服务之间通过seata这类分布式事务中间件来统一管理数据库的事务,由TC来收集各个服务执行情况,出错后通知各个服务统一进行回滚。而实际情况系统建设初期,很难提前预知未来公司的业务发展趋势,因此一开始不太可能会进行比较重的架构设计,也不会花重金聘请一个架构师来进行系统规划建设,基本上都是从单库单服务不断发展起来的。只有当单库的性能出现瓶颈的时候才会考虑加库加服务。然而很多业务的迭代还得依赖原有的业务系统,新服务想要承接一些业务需要操作老系统的数据库。
在这样的背景下,就会出现一个服务上挂载多个数据源的情况,而多数据源面临的一个问题就是事务一致性问题, 这跟分布式事务还不太一样,相当于一个运用节点要同时处理多个数据库事务。
二、spring 事务机制
既然spring提供的事务能力范围只在一个数据源内,那把各个业务数据源的事务隔离出来最后再统一管理不就行了。我们只需要在此基础上再封装一层,增加一层类似于协调者的作用,协助进行事务的提交和回滚管理,主要作用是收集各个数据源模块的业务代码执行状态,从而确定是否需要进行回滚。我这里想到的一个思路是多线程模型和aop后置增强,把各个业务模块涉及到的数据源上下文(事务)的操作单独隔离在一个子线程中,子线程的业务处理完成后借助aop的增强能力,后置处理实现子线程与子线程、子线程与主线程之间的执行状态数据共享,即知己知彼、有没有异常发生,如果出现异常则各自回滚即可。 多线程之间数据传递常规处理是继承Thread类,重写run方法时把共享数据传递进来,但这个对我们的业务代码具有一定的侵入性,不够优雅,在这里我比较倾向aop的增强能力来实现通用性,线程之间的共享数据则轮训本地内存缓存来实现。
比如生成订单的操作为主线程,添加日志、库存扣减、日志记录等所有与订单相关的操作独立在各自的子线程中,都有基于当前数据源的事务处理能力(也就是被spring 的@Transactional标记过),同时子线程之间要能获取到彼此的执行情况,比如主线程a和子线程bcd ,当a执行失败或者bcd中任意一个线程执行失败时,主线程a和其余子线程都要能知道这个异常的发生,并回滚各自的事务。
三、实现思路
到这里简单总结一下:
-
- 我们不去干预spring原有的事务能力;
-
- 使用多线程模型进行业务数据源上下文隔离;
-
- 将主线程以及子线程包装在一个线程上下文中,借助aop切面和缓存实现线程数据共享和调度管理;
基于上面的理解,在这里我们引入两个注解和切面:
- 主线程事务注解及切面
- 子线程事务注解及切面
以下是具体的代码实现以及调用示例
3.1 主线程事务注解及切面
3.2 子线程事务注解及切面
3.2 调用示例
四、目前存在的问题
4.1、切面代码性能问题
切面中为了同步主子线程的执行状态数据,代码中存在轮训的处理机制。
这部分可以优化,我的想法是借助缓存的key过期监听事件来处理,key的过期分为超时过期和手动删除过期,正常的执行结束是手动删除过期,key的值设置为子线程的数量,超时过期时间为30s,每个子线程执行完后该值-1,当值为0时同时删除key,key的监听事件即可捕获到,如果cause是删除过期,说明所有子线程正常执行结束,主子线程同步执行状态,提交各自事务,如果是expired,说明过期时子线程还未执行完成,整个线程上下文直接判定失败,回滚各自的事务。
4.2、子线程数量激增
并发访问时会导致子线程数量激增,这里做了压测,如果一个提交请求涉及到的数据源事务较多,子线程数量太多的情况下,可能会存在有的子线程在事务过期时间内还没有全部执行完的情况。
需要根据机器性能、数据源数量以及服务节点数等设置一个合适的阈值,虽然切面中也做了轮训超时次数限制,但框架层的异常直接仍给业务层,提示不够友好。
4.3、数据库压力大
在并发小的情况下,这种并行化的事务机制,会一定程度的提高接口响应速度,但如果请求较多时,数据库压力也会增大,高并发场景慎用。