95%的人都不知道线程池与事务的细节问题
很多人在使用事务的时候,基本都是在方法上添加@Transactional(rollbackFor = Exception.class)注解就完事了。如果有的业务需要异步执行的话,也都是用的线程池来执行,但两者要是遇到一起了,那么遇到的问题可就没有那么简单了,而很多人都不知道其中的细节,生产上产生的问题也很多。
就比如 主线程异常了,子线程的数据却没有回滚。或者主线程和子线程中的数据都没有回滚,那到底什么时候会回滚,什么时候不会回滚呢?下面我们来详细的介绍
多线程操作数据库的问题
主线程中开启一个子线程,如果子线程出现异常的话,子线程会回滚吗?主线程会回滚吗?
案例:
@Service
@Transactional
public class PayService implements IPayService {
@Autowired
private PayMapper payMapper;
@Autowired
private AccountMapper accountMapper;
private Executor executor = Executors.newSingleThreadExecutor();
@Override
public Integer testTransactionThread(Pay pay) {
int insert = payMapper.insert(pay);
Long id = pay.getId();
executor.execute(() -> {
Account account = new Account();
account.setId(id);
accountMapper.insert(account);
if (id == 2) {
throw new RuntimeException("模拟异常");
}
});
return insert;
}
}
执行结果结果:
Exception in thread "pool-2-thread-1" java.lang.RuntimeException: 模拟异常
at com.example.service.impl.PayService.lambda$testTransactionThread$0(PayService.java:45)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:750)
结果发现主线程的添加pay和子线程的添加account都不会进行回滚
接下来详细介绍造成此问题主要是哪几方面造成
Spring的事务管理特点
spirng的事务管理可参考本人博客,这里列出事务管理器的关键结构信息
TransactionSynchronizationManager
public abstract class TransactionSynchronizationManager {
// 线程私有事务资源
private static final ThreadLocal<Map<Object, Object>> resources =
new NamedThreadLocal<>("Transactional resources");
// 事务同步
private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations =
new NamedThreadLocal<>("Transaction synchronizations");
// 当前事务的名称
private static final ThreadLocal<String> currentTransactionName =
new NamedThreadLocal<>("Current transaction name");
// 当前事务是否只读
private static final ThreadLocal<Boolean> currentTransactionReadOnly =
new NamedThreadLocal<>("Current transaction read-only status");
// 当前事务的隔离级别
private static final ThreadLocal<Integer> currentTransactionIsolationLevel =
new NamedThreadLocal<>("Current transaction isolation level");
// 实际事务是否激活
private static final ThreadLocal<Boolean> actualTransactionActive =
new NamedThreadLocal<>("Actual transaction active");
}
重要总结
- 可以看到
TransactionSynchronizationManager中的关键属性其实都是用ThreadLocal来管理的,而ThreadLocal中的数据都是和线程绑定的 - 重点是resources这个变量,首先这是一个threadlocal的结果,枚举中的Map key类型为连接数据库的数据源dataSource,value类型为ConnectionHolder(可以理解为dataSource的一个connect连接)
- 当方法执行到spring的doBegin开启事务方法,会先从resources获取,以当前数据源dataSource为key,获取value也就是connect存不存在,如果不存在则从dataSource获取一个connect设置进去,如果存在则直接以当前的connect来使用
- 所以在spring事务管理的情况下,父子线程的数据源连接connect是不同的
也就是说父线程和子线程的数据库连接已经不是同一个了,子线程已经脱离了父线程的事务管理范围
Account account = new Account();
account.setId(id);
accountMapper.insert(account);
if (id == 2) {
throw new RuntimeException("模拟异常");
}
这段代码是子线程的执行逻辑,已经脱离了父线程事务的管理,而且直接执行mapper来添加数据,也就是子线程也没有自己的事务,所以即使抛出了异常,子线程也不会回滚
我们先来解决子线程回滚的问题,这里即使抛出异常还没有回滚就是因为子线程压根就没有事务,那怎么使用子线程又能有事务呢?其实很简单,直接用再用一个service对象来调用这个添加方法就可以了,因为事务本质还是切面,spirng在加载的时候,会扫描切面所在的类,接着对这些类增强也就是常说的代理类
在执行线程池的任务执行,service已经是代理增强类了,调用方法也是被事务增强的方法,所以就还是有事务的。既然我们知道思路,下面就来详细实现
解决思路
这里将添加account的逻辑都移动到accountService中,线程池直接调用accountService方法
@Service
@Transactional
public class PayService implements IPayService {
@Autowired
private PayMapper payMapper;
@Autowired
private AccountService accountService;
private Executor executor = Executors.newSingleThreadExecutor();
@Override
public Integer testTransactionThread(Pay pay) {
int insert = payMapper.insert(pay);
Long id = pay.getId();
executor.execute(() -> {
Account account = new Account();
account.setId(id);
accountService.insert(account);
});
return insert;
}
}
@Service
@Transactional
public class AccountService implements IAccountService {
@Autowired
private AccountMapper accountMapper;
@Override
public Integer insert(Account account) {
Integer insert = accountMapper.insert(account);
if (account.getId() == 2) {
throw new RuntimeException("模拟异常");
}
return insert;
}
}
这时accountService在执行insert方法时,就可以开启事务了。可以实现子线程中的account添加中出现异常是可以回滚的。但是我们还只是解决了子线程回滚的问题,父线程中的pay添加操作还是不能回滚的
原因是子线程抛出的异常后并不能被父线程所感知到,那么我们让父线程感应到异常不就可以了吗
- 点赞
- 收藏
- 关注作者
评论(0)