Bootstrap

线程池与事务的细节问题

很多人在使用事务的时候,基本都是在方法上添加@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的事务管理特点

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添加操作还是不能回滚的

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

解决思路

根据spring事务特点我们知道父子线程的数据源连接connect是不同的,但是我们可以通过线程池的特点来解决上述问题

解决子线程出现异常,让父线程回滚

线程池在执行submit时,会将异常放入futureTask中,可以利用这个特点来解决。

案例:

@Service
@Transactional
public class PayService implements IPayService {
    
    @Autowired
    private PayMapper payMapper;

    @Autowired
    private AccountService accountService;

    private ExecutorService executor = Executors.newSingleThreadExecutor();

    @Override
    public Integer testTransactionThread(Pay pay) {

        Long id = pay.getId();
        Future<Integer> future = executor.submit(() -> {
            Account account = new Account();
            account.setId(id);
            return accountService.insert(account);
        });
        int insert = payMapper.insert(pay);
        try {
            Integer integer = future.get();
        } catch (InterruptedException e) {
            throw new RuntimeException("子线程account执行中断异常");
        } catch (ExecutionException e) {
            throw new RuntimeException("子线程account执行异常");
        }
        return insert;
    }
}

这样当子线程account操作出现异常时,父线程的异常是可以成功回滚的,但是future存在一个问题,就是当executor.submit中的任务没有执行完的话,future.get()是一直被阻塞住的,那这启不和同步执行没什么区别了吗?并且这还涉及到线程上下文的切换,效率还不如同步呢

所以说适合使用future的场景有两种

两个任务

存在两个逻辑A和B,这两个可以并行执行,不需要A执行完后再执行B,就可以使用future。比如A耗时1s,B耗时2s,执行的总耗时就是2s

当B执行出现异常后,A和B都可以回滚

多个任务

比如说存在4个任务逻辑 A B C D,常规执行的话 A -> B -> C -> D,如果进行优化的话,A是主线程, B C D 可以放在线程中异步执行。

统计耗时

假设 A任务的耗时 1s,B任务的耗时 2s,C任务的耗时 3s,D任务的耗时 4s

  • 常规执行的耗时: A任务的耗时 + B任务的耗时 + C任务的耗时 + D任务的耗时  为 1 + 2 + 3 + 4 = 10s
  • 使用future的耗时:A任务的耗时 + B/C/D任务的耗时 (取决于B C D哪个任务时间长)  为 1 + 4 = 5s

而B C D 的任务其中有一个异常的话,主线程都会回滚。但是要注意,并不能实现所有子线程回滚比如B出现异常,A和B可以回滚,C和D是不能回滚的

上文介绍了Spirng的事务管理器是ThreadLocal范围,也就是A B C D 本质都是使用各自自己的事务,所以不能一次都回滚

;