一、问题复现
批量插入时,使用多线程对插入数据实现分批插入,在service层使用@Transactional注解,对应方法中线程池中开辟的子线程抛出异常时,没有回滚事务。
二、原因分析
- 事务管理范围不正确:
@Transactional
注解仅对当前方法有效,如果在方法内创建新的线程或使用线程池等异步操作,该方法之外的代码将无法受到事务的管理。因此,在使用多线程进行批量操作时,需要确保整个批量操作处于同一事务管理范围内。 - Spring事务和Java线程池机制的互动问题:在使用
ThreadPoolExecutor
进行批量操作时,线程池中的线程和Spring管理的事务并不是同一个线程,这可能会导致事务管理器感知不到线程中的异常,从而导致事务未能回滚。
三、解决办法
弃用注解样事务,改为手动管理事务。
1 SqlSession sqlSession = SpringContextUtils.getBean(SqlSessionTemplate.class).getSqlSessionFactory()
2 .openSession();
3 Connection connection = sqlSession.getConnection();
4 OfflineExpressRecordExtMapper extMapper = sqlSession.getMapper(OfflineExpressRecordExtMapper.class);
5
6 // 批量插入
7
8 int taskCount = (int) Math.ceil((double) beanList.size() / THREAD_HANDLE);
9 ThreadPoolExecutor executor = SpringContextUtils
10 .getBean("offlineExpressRecordThreadPoolExecutor", ThreadPoolExecutor.class);
11 try {
12 connection.setAutoCommit(false);
13 ArrayList<Future<?>> futures = new ArrayList<>();
14 for (int i = 0; i < taskCount; i++) {
15 int start = i * THREAD_HANDLE;
16 int end = (i + 1) * THREAD_HANDLE > beanList.size() ? beanList.size() : (i + 1) * THREAD_HANDLE;
17 List<OfflineExpressRecord> threadHandleList = beanList.subList(start, end);
18 Future<?> task = executor.submit(() -> extMapper.saveBatch(threadHandleList));
19 futures.add(task);
20 }
21 // 等待插入完成,检验异常
22 for (Future<?> future : futures) {
23 future.get();
24 }
25 connection.commit();
26 } catch (Exception e) {
27 log.error("批量导入存储数据过程中出现异常", e);
28 connection.rollback();
29 throw e;
30 } finally {
31 connection.close();
32 }
四 、优化思路
- 优化导入时机
初时,是将所有数据导入到一个list中,然后使用线程池分批导入,存在内存泄漏以及效率相对较慢的风险。可以优化为异步导入,读取数据到list中时,去判断list中数据的数量,达到临界值后,在线程池中开启一个新的线程直接去执行插入(同样要使用手动管理事务)。即提高了插入的效率(读取数据时即可进行插入),也避免了内存泄漏的风险(list中数据最多只有临界值的数量)
2.存在潜在bug
(1). 内部状态:SqlSession
实现中包含了许多与当前数据库连接和事务相关的内部状态。这些状态是特定于当前线程的,因此在多个线程之间共享同一个 SqlSession
实例会导致状态混乱和线程安全问题。
(2). 数据库连接:SqlSession
通常持有一个数据库连接。如果多个线程共享同一个 SqlSession
实例,就会出现竞争条件,影响数据库操作的正确性和性能。
因此,正确的做法时是让每个线程独享一个SqlSession,并使用一个线程之间共享的变量去标记是否需要回滚,如使用AtomicBoolean(rollbackFlag)
使用AtomicBoolean的原因:AtomicBoolean类内部值存储变量使用了volatile做修饰,确保了变量修改在线程间的及时可见,变量操作使用了CAS,确保变量操作的原子性。
CAS是乐观锁的原理,通过对比操作前的旧值和即将操作时变量的值,如果值改变则表示有其他线程在操作变量,将再次进行操作尝试。CAS有ABA问题,不过对本次测试不影响。
执行过程中,当一个线程出现异常,设置rollbackFlag为true,提交事务时,根据rollbackFlag对子线程的事务判断commit or rollback,未执行完的线程直接在逻辑中直接return取消执行即可。
-
线程死锁风险
当请求批量插入的风险比较高时,可能会出现数据库死锁的情况
例如100万数据处理,每个线程处理1000数据量,则需要请求1000个线程。在耗尽线程池中有限的线程资源后,未请求到资源的线程请求进入等待队列,等待线程释放。考虑事务机制,需要在所有线程事务都结束后才结束事务等待,释放线程资源。此处造成死锁。
可能的解决方案:
(1).限制队列的等待数量,将线程池的等待队列数量设置到一个比较理想的值,减少出现死锁的可能性,引入超时机制,对每次批量插入设置插入实际,当获取不到数据库资源时,批量回滚所有事务释放资源
(2)接口限流,根据服务器、数据库资源配置,结合nacos及滑动窗口对接口实现动态限流,避免不同请求之间的子线程出现死锁的情况