95%的人都不知道线程池与事务的细节问题

举报
一颗小谷粒 发表于 2024/11/27 15:38:33 2024/11/27
【摘要】 很多人在使用事务的时候,基本都是在方法上添加@Transactional(rollbackFor = Exception.class)注解就完事了。如果有的业务需要异步执行的话,也都是用的线程池来执行,但两者要是遇到一起了,那么遇到的问题可就没有那么简单了,而很多人都不知道其中的细节,生产上产生的问题也很多。就比如 主线程异常了,子线程的数据却没有回滚。或者主线程和子线程中的数据都没有回滚,...

很多人在使用事务的时候,基本都是在方法上添加@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添加操作还是不能回滚的


原因是子线程抛出的异常后并不能被父线程所感知到,那么我们让父线程感应到异常不就可以了吗

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。