业务场景
希望将步骤 1 和步骤 2 并行执行,
然后确保步骤 1 和步骤 2 执行成功后,再执行步骤 3,
等到步骤 3 执行完毕后,再提交全部事务
1 | public void removeAuthorityModuleSeq(Integer authorityModuleId, IAuthorityService iAuthorityService, IRoleAuthorityService iRoleAuthorityService) { |
解决异步执行
@Async
Spring 提供的异步执行任务能力并不足以解决当前的需求。
@Async 注解原理简单来说,就是扫描 IOC 中的 bean,给方法上标注有 @Async 注解的 bean 进行代理,代理的核心是添加一个 MethodInterceptor,即AsyncExecutionInterceptor。该方法拦截器负责将方法真正的执行包装为任务,放入线程池中执行。
CompletableFuture
1 | public void removeAuthorityModuleSeq(Integer authorityModuleId, IAuthorityService iAuthorityService, IRoleAuthorityService iRoleAuthorityService) { |
多线程事务一致性
事务管理大体分为三个流程: 事务创建、事务执行、事务结束。
事务创建涉及到一些属性的配置,例如:
事务的隔离级别
事务的传播行为
事务的超时时间
是否为只读事务
…
由于涉及属性颇多,并且后期还有可能进行扩展,因此必须通过一个类来封装这些属性,在 Spring 中对应 TransactionDefinition。
有了事务相关属性定义后,我们就可以利用 TransactionDefinition 来创建一个事务了。
在 Spring 中局部事务由 PlatformTransactionManager 负责管理,创建事务也是由 PlatformTransactionManager 负责提供。
如果我们希望追踪事务的状态,例如事务已完成,事务回滚等,那么就需要一个事务状态类贯穿当前事务的执行流程,在 Spring 中由 TransactionStatus 负责完成。
1 | TransactionStatus getTransaction(@Nullable TransactionDefinition definition) |
对于常见的数据源而言,通常需要记录的事务状态有如下几点:
当前事务是否是新事务
当前事务是否结束
当前事务是否需要回滚(通过标记来判断,因此我也可以在业务流程中手动设置标记为 true 来让事务在没有发生异常的情况下进行回滚)
当前事务是否设置了回滚点(savePoint)
事务的执行过程就是具体业务代码的执行流程。
事务的结束分为两种情况:需要进行事务回滚或者事务正常提交,如果是事务回滚,还需要判断 TransactionStatus 中的 savePoint 是否被设置了。
声明式事务
声明式事务就是使用我们常见的 @Transactional 注解完成的。声明式事务优点就在于让事务代码与业务代码解耦,通过 Spring 中提供的声明式事务使用,我们也可以发觉我们只需要编写业务代码即可。而事务的管理基本不需要我们操心,Spring 帮我们自动完成了。
之所以那么神奇,本质还是依靠 Spring 框架提供的 Bean 生命周期相关回调接口和 AOP 结合完成的,简述如下:
通过自动代理创建器依次尝试为每个放入容器中的 bean 尝试进行代理;
尝试进行代理的过程对于事务管理来说,就是利用事务管理涉及到的增强器 advisor,即 TransactionAttributeSourceAdvisor;
判断当前增强器是否能够应用与当前 bean 上,怎么判断呢? 当然是 advisor 内部的 pointCut;
如果能够应用,那么好,为当前 bean 创建代理对象返回,并且往代理对象内部添加一个 TransactionInterceptor 拦截器。
此时我们再从容器中获取,拿到的就是代理对象了,当我们调用代理对象的方法时,首先要经过代理对象内部拦截器链的处理,处理完后,最终才会调用被代理对象的方法(这里其实就是责任链模式的应用)。
对于被事务增强器 TransactionAttributeSourceAdvisor 代理的 bean 而言,代理对象内部会存在一个 TransactionInterceptor,该拦截器内部构造了一个事务执行的模板流程:
1 | protected Object invokeWithinTransaction(Method method, @Nullable Class << ? > targetClass, |
编程式事务
在前面,我们已经解决了任务异步并行执行的难题,下面我们要解决如何确保 Spring 在多线程环境下也能保持事务一致性。
声明式事务并不能解决我们当前的问题,那就只能求助于编程式事务了。
那么编程式事务是什么样子呢?
其实上面 TransactionInterceptor 给出的那套模板流程,就是编程式事务使用的模范案例,简化上面的模板流程,使用如下:
1 | public class TransactionMain { |
编程式事务解决问题
下面我给出一份看似正确的解决方案:
1 | import lombok.RequiredArgsConstructor; |
1 | public void test() { |
任务正常都执行完毕,事务进行提交,但是会抛出异常,导致事务回滚:
1 | No value for key [HikariDataSource (HikariPool-1)] bound to thread [main] |
一次事务的完成通常都是默认在当前线程内完成的,又因为一次事务的执行过程中,涉及到对当前数据库连接 Connection 的操作,因此为了避免将 Connection 在事务执行过程中来回传递,我们可以将 Connextion 绑定到当前事务执行线程对应的 ThreadLocalMap 内部,顺便还可以将一些其他属性也放入其中进行保存,在 Spring 中,负责保存这些 ThreadLocal 属性的实现类由 TransactionSynchronizationManager 承担。
TransactionSynchronizationManager 类内部默认提供了下面六个 ThreadLocal 属性,分别保存当前线程对应的不同事务资源:
1 | //保存当前事务关联的资源--默认只会在新建事务的时候保存当前获取到的DataSource和当前事务对应Connection的映射关系--当然这里Connection被包装为了ConnectionHolder |
那么上面抛出的异常的原因也就很清楚了,无法在 main 线程找到当前事务对应的资源,原因如下:
开启新事务时,事务相关资源都被绑定到了 thread-cache-pool-1 线程对应的 threadLocalMap 内部,而当执行事务提交代码时,commit 内部需要从 TransactionSynchronizationManager 中获取当前事务的资源,显然我们无法从 main 线程对应的 threadLocalMap 中获取到对应的事务资源,这也就是异常抛出的原因。
解决方法:用 CopyTransactionResource 将事务资源在两个线程间来回复制。
1 | import lombok.Builder; |
增加异常抛出,测试是否能够保证多线程间的事务一致性:
1 | (classes = UserMain.class) |
事务都进行了回滚,数据库数据没变。