多线程事务处理方案
1、编程式事务 + 手动回滚
1. 子任务用编程式事务执行
使用 TransactionTemplate
(Spring 提供)或 PlatformTransactionManager
。
@Autowired
private TransactionTemplate transactionTemplate;
AtomicBoolean hasError = new AtomicBoolean(false);
List<CompletableFuture<Void>> futures = new ArrayList<>();
futures.add(CompletableFuture.runAsync(() -> {
transactionTemplate.execute(status -> {
try {
// 子任务1逻辑
} catch (Exception e) {
hasError.set(true);
status.setRollbackOnly();//是 Spring 编程式事务中用来标记当前事务需要回滚的方法。
}
return null;
});
}));
futures.add(CompletableFuture.runAsync(() -> {
transactionTemplate.execute(status -> {
try {
// 子任务2逻辑
} catch (Exception e) {
hasError.set(true);
status.setRollbackOnly();
}
return null;
});
}));
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
// 处理失败场景
if (hasError.get()) {
throw new RuntimeException("子任务有失败,事务全部回滚");
}
2、❗ 注意:
- 每个子线程中的
transactionTemplate.execute()
是独立事务; - 它们各自能回滚自己,但不能自动协调;
- 所以需要你在主线程手动判断是否需要“补偿”或“告警”。
2、通过共用同一个Spring的事务的Aconnection
我们假设,有这么一段代码,方法a和方法b他们都开起了事务,然后在方法a当中呢,嵌套的调用了方法b,事务默认的嵌套行为,是外层a没有事务,就会开启一个事务,外层有事务呢,就会融入到
外层的事务当中,所以说上面图片中的两个方法呢,就会使用同一个事务,那这个事务的传播行为,他是如何实现的呢?
- 在Spring的底层,当他发现你使用了
@Transactional
的注解,就会为你创建一个动态代理的对象(图上标的方法a),那这个动态代理对象呢,就会开启一个事务。
- 那开启事务的本质呢其实就是通过
JDBC
的connection
调用setAutoCommit(false)
,就相当于开启了一个事务。
- 开启事务完成之后,他会把这个
connection
存到Threadlocal
当中(如上图)
为啥要存到
Threadlocal
当中呢?
- 其实就是为了你的嵌套方法,能够拿到你的外层事务的
connection
对象(如下图)
- 因为只有用同一个
connection
,才能保证用同一个事务,所以当外层事务存完connection
之后呢,即第3步执行完成之后,就会执行本身的insert()方法,从而就会去执行数据库操作完了之后就会嵌套调用方法b,那在方法b的动态代理当中呢,他就会拿到你的外层事务的这个
Threadlocal
当中的那个connection
,从而去执行自身方法中update
方法,但是方法b他发现
Threadlocal
当中有值呢,他并不会提交事务,而是统一的交给外层事务进行提交,这样呢保证了事务的传播行为,那到这里应该明白了,为啥Spring的多线程事务会失效了吧?
,下面我们将A调用B方法改成多线程
假如说方法A,是通过异步线程的方式,调用的方法b,此时方法b就无法拿到外层事务的
Threadlocal
当中的connection
了,因为Threadlocal
他是绑定在线程上面的,所以我们在A方法当中通过
new Thread()
产生一个子线程之后呢,他这里再想通过Threadlocal
当中去拿connection
,就拿不到了。因为Threadlocal是绑定在线程上面的。(如下图)
此时那不到,就会开启一个事务,自己去存
connection
到Threadlocal
当中,那这个时候方法A和方法B就会各用各的事务了(如下图)
实现步骤
如果我们要保证,多线程事务的一致性,可以在创建一个异步线程之后,我手动的往对应的子线程哪个
Threadlocal
当中呢,把外层事务的这个connection对象
给他存进去那么这样一来方法 B是不是就能够从自己的Threadlocal当中去拿到
connection对象
了,从而是不是就可以用同一个事务了。那关键是外层事务的这个
connection
,应该这么获取呢,获取到了又该如何存到子线程Threadlocal
当中去呢?我们可以通过源码的
DataSourceeTransactionmanager
类中,在这个类里面有一个doBegin方法,这个方法就是Spring底层在开启事务的时候,会进行调用的。
在源码当中可以看到,他是通过
datasoource
来获取一个数据库连接,那这个datasoource
,其实就是从Spring的容器当中获取到的
并且在下面的代码中会通过
setAutocommit
来去开启一个事务
最后呢会把这个
connection
,通过这个事务同步管理器,调用bindresource
方法,把它绑定到这个Threadlocal`当中
把它绑定到这个
Threadlocal
当中,并且这个Threadlocal
存的是一个Map
,所以我们的那个connection
呢,他就会存到这个Value当中,而这个Key就是spring容器当中的datasoource
。
所以如果我们要获取
connection
对象的话,只需要调用事务同步管理器的getResource方法,然后把Spring容器当中的datasoource
把他传进来,是不是就可以获取到了,然后我们在手动的去调用
bindresource
方法,是不是就可以往Threadlocal
当中,在手动的在存一遍了
代码
@Resource
private Datasource datasource;
@Thransactional(rollbackFor = Exception.class)
public void a() throws InterruptedException {
ConnectionHolder connectionHolder = (ConnectionHolder)
TransactionSynchronizationManager.getResource(datasource);
System.out.println("当前线程的数据库连接信息:" + connectionHolder.getTargetConnection());
Thread thread = new Thread(() -> {
TransactionSynchronizationManager.bindResource(datasource, connectionHolder);
userService.b();// 调用另外一个服务的方法,该方法也会在同一个事务中执行
});
thread.start();
thread.join(); // 等待线程执行完毕,确保事务完成
}
@Thransactional
public void b() {
updateById();
}