Spring Boot 多线程写入Excel和数据库校验demo

该demo采用Spring Boot,FastExcel,Mybatis-plus一起来实现百万级数据的导入和校验,入库,写的不好,纯粹记录。。多线程采用编程式事务来实现。多线程事务的实现,复制了大佬文章,地址如下:https://blog.youkuaiyun.com/2401_85910670/article/details/144256236
cs 真是dog东西,在我不知道的情况下,把文章都变成了VIP可见,他妈的,VIP可见,老子没分到一分钱,还白嫖老子写的东西,不愧是李家公司带出来b

代码如下,insertByAi这个方法可以忽略

import cn.hutool.core.thread.ThreadUtil;
import cn.idev.excel.FastExcel;
import cn.idev.excel.context.AnalysisContext;
import cn.idev.excel.event.AnalysisEventListener;
import com.example.mailtest.bean.TempTableVO;
import com.example.mailtest.bean.UserTest;
import com.example.mapper.UserTestMapper;
import com.google.common.base.Stopwatch;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import lombok.Builder;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.stereotype.Service;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.transaction.support.TransactionTemplate;

import javax.sql.DataSource;
import java.io.File;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor(onConstructor_ = @__(@Autowired))
@Slf4j
public class ExcelServer {
    private final DataSource dataSource;
    private final UserTestMapper userTestMapper;
    private final ExecutorService executorService = ThreadUtil.newExecutor(10, 20, 10000);


    /**
     * 该函数用于从Excel文件中读取数据,并进行一系列处理,最终将处理后的数据批量插入数据库。
     * 函数的主要步骤包括:
     * 1. 读取Excel文件并解析数据。
     * 2. 对解析后的数据进行过滤和转换。
     * 3. 使用多线程对数据进行校验和处理。
     * 4. 将处理后的数据批量插入数据库。
     * 5. 记录整个过程的耗时。
     * 该函数使用事务注解,是因为在使用编程式事务后,查询数据库的校验会报找不到链接的异常,猜测原是因为事务的原因,加上注解可以使用查询。
     */
    @Transactional
    public void excelTest() {
        // 启动计时器,用于记录整个过程的耗时
        Stopwatch started = Stopwatch.createStarted();
        // 用于存储从Excel中解析出的临时数据
        final Set<TempTableVO> voList = new CopyOnWriteArraySet<>();
        // 用于存储转换后的用户数据
        final Set<UserTest>[] userList = new HashSet[]{new HashSet<>()};
        // 用于存储异步任务的列表
        List<CompletableFuture<Void>> completableFutures1 = Lists.newArrayListWithCapacity(1000);
        // 用于存储数据库结果的Map
        Map<String, Object> resultMap = Maps.newHashMapWithExpectedSize(1024 * 512);
        // 用于存储去重后的用户数据
        final Set<UserTest> newList = new CopyOnWriteArraySet<>(new HashSet<>());
        // 读取Excel文件并解析数据
        FastExcel.read(new File("D:\\ujdznqnmfo.xlsx")).head(TempTableVO.class).
                registerReadListener(new AnalysisEventListener<TempTableVO>() {
                    @Override
                    public void invoke(TempTableVO data, AnalysisContext context) {
                        voList.add(data);
                    }

                    @Override
                    public void doAfterAllAnalysed(AnalysisContext context) {
                        if (CollectionUtils.isEmpty(voList)) {
                            return;
                        }
                        log.info("解析完成,數據量:{}", voList.size());
                        for (TempTableVO tempTableVO : voList) {
                            if (StringUtils.isBlank(tempTableVO.getName()) || tempTableVO.getAge() == 0) {
                                continue;
                            }
                            userList[0].add(UserTest.builder().name(tempTableVO.getName()).age(tempTableVO.getAge()).build());

                        }


                    }
                }).sheet().doRead();
        // 通义灵码写的,使用事务模板,并不怎么好用,不能实现数据的回滚,只能保证事务内数据的一致性,但是不能保证事务外数据的一致性。
//        insertByAi(new ArrayList<>(userList[0]), 1000);
        log.info("数据库数据是多少:{}", userList[0].size());
        // 将userList中的数据分批次处理
        List<List<UserTest>> lists = Lists.partition(new ArrayList<>(userList[0]), 1000);
        // 对分批次的数据进行校验
        check(lists, resultMap, completableFutures1);
        // 等待所有异步任务完成,并对处理后的数据进行去重和批量插入
        CompletableFuture.allOf(completableFutures1.toArray(new CompletableFuture[0])).thenAccept(unused -> {
            for (List<UserTest> list : lists) {
                Set<UserTest> userTests = list.stream().filter(userTest -> !resultMap.containsKey(userTest.getAge() + "-" + userTest.getName())).collect(Collectors.toSet());
                if (CollectionUtils.isNotEmpty(userTests)) {
                    newList.addAll(userTests);
                }
            }
            log.info("数据库去重后的数据是多少:{}", newList.size());
        }).thenAccept(unused -> insertBatch(new ArrayList<>(newList), 1000));
        long elapsed = started.stop().elapsed(TimeUnit.SECONDS);
        log.info("耗时:{}秒", elapsed);


    }

    public void insertBatch(List<UserTest> excelList, int batchSize) {
        AtomicBoolean isError = new AtomicBoolean(false);
        try {
            int size = excelList.size();
            log.info("数据量是多少:{}", size);
            DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager(dataSource);
            List<CompletableFuture<Void>> completableFutures = Lists.newArrayListWithCapacity(1000);

            List<TransactionStatus> transactionStatuses = Collections.synchronizedList(Lists.newArrayListWithCapacity(1000));
            List<TransactionSource> sourceList = Collections.synchronizedList(Lists.newArrayListWithCapacity(1000));
            CompletableFuture<Void> async;
            List<List<UserTest>> lists = Lists.partition(excelList, batchSize);


            for (int i = 0; i < lists.size(); i++) {
                final List<UserTest>[] voList = new List[]{lists.get(i)};
                int finalI = i;
                async = CompletableFuture.runAsync(() -> {
                    try {
                        // 1.开启新事物
                        DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
                        definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
                        definition.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
                        transactionStatuses.add(dataSourceTransactionManager.getTransaction(definition));
                        // 2.复制资源
                        sourceList.add(TransactionSource.copyTransactionSource());
                        // 执行操作
                        log.info("子线程开始开始,线程名称:{}", Thread.currentThread().getName());
                        // 数据库使用了insert ignore插入,采用了联合索引二次去重,实际应用并不大
                        int inserted = userTestMapper.insertUser(voList[0]);
                        log.info("子线程名称:{},插入了{}条", Thread.currentThread().getName(), inserted);
                    } catch (Exception e) {
                        log.error("异步更新出错了,批次:{}", finalI, e);
                        isError.set(true);
                        // 退出其他任务
                        completableFutures.forEach(future -> future.cancel(true));
                    }
                }, executorService);
                completableFutures.add(async);
            }

            CompletableFuture.allOf(completableFutures.toArray(new CompletableFuture[0])).join();
            int flag = transactionStatuses.size();
            if (isError.get()) {
                log.error("出错了,开始回滚");
                for (int i = 0; i < flag; i++) {
                    sourceList.get(i).autoWiredTransactionResource();
                    dataSourceTransactionManager.rollback(transactionStatuses.get(i));
                    sourceList.get(i).removeTransactionResource();
                }
            } else {
                long orElse = completableFutures.size();
                log.info("成功了,开始更新,更新了:{}条", orElse);
                for (int i = 0; i < flag; i++) {
                    sourceList.get(i).autoWiredTransactionResource();
                    dataSourceTransactionManager.commit(transactionStatuses.get(i));
                    sourceList.get(i).removeTransactionResource();
                }
            }
        } catch (Exception e) {
            log.error("出错了", e);
        }

    }
    /**
     * 该函数用于并行处理多个用户测试列表,通过数据库校验每个列表中的用户信息,并将结果存储在结果映射中。
     *
     * @param lists 包含多个用户测试列表的列表,每个列表中的用户信息将被并行处理。
     * @param resultMap 用于存储数据库校验结果的映射,键为年龄和姓名的组合,值为对应的临时表数据。
     * @param completableFutures1 用于存储所有异步任务的列表,确保所有任务完成后才能继续后续操作。
     * &#064;Transactional  注解确保该函数在事务管理下执行,保证数据一致性。
     */
    @Transactional
    public void check(List<List<UserTest>> lists, Map<String, Object> resultMap, List<CompletableFuture<Void>> completableFutures1) {

        lists.parallelStream().forEach(list -> {
                    Set<Integer> integerSet = list.stream().map(UserTest::getAge).collect(Collectors.toSet());
                    Set<String> nameSet = list.stream().map(UserTest::getName).collect(Collectors.toSet());
                    List<TempTableVO> tables = userTestMapper.getTempTables(integerSet, nameSet);
                    if (CollectionUtils.isNotEmpty(tables)) {
                        CompletableFuture<Void> runAsync = CompletableFuture.runAsync(() -> {
                            log.info("数据库校验开始,子线程:{},查出来了吗:{}", Thread.currentThread().getName(), tables.size());
                            ConcurrentHashMap<String, TempTableVO> hashMap = tables.stream().collect(Collectors.toMap(e -> e.getAge() + "-" + e.getName(),
                                    v -> v, (v1, v2) -> v1, ConcurrentHashMap::new));
                            resultMap.putAll(hashMap);
                            log.info("数据库校验结束,子线程:{},数据是多少:{},Map的值是多少:{}", Thread.currentThread().getName(), tables.size(), resultMap.size());
                        }, executorService);
                        completableFutures1.add(runAsync);
                    }
                }
        );
    }


    /**
     * 通过异步批量插入用户测试数据。
     * 该方法将传入的用户测试数据列表按批次大小进行分割,并使用多线程异步执行插入操作。
     * 每个批次的插入操作都在独立的事务中执行,确保数据的一致性和完整性。
     * 如果在插入过程中发生错误,所有已插入的数据将回滚。
     *
     * @param excelList 用户测试数据列表,包含待插入的用户测试数据。
     * @param batchSize 每个批次的大小,用于将数据列表分割成多个小批次进行插入。
     */
    public void insertByAi(List<UserTest> excelList, int batchSize) {
        // 初始化错误标志、异步任务列表、事务状态列表和事务源列表
        AtomicBoolean isError = new AtomicBoolean(false);
        List<CompletableFuture<Void>> completableFutures = Lists.newArrayListWithCapacity(1000);
        List<TransactionStatus> transactionStatuses = Collections.synchronizedList(Lists.newArrayListWithCapacity(1000));
        List<TransactionSource> sourceList = Collections.synchronizedList(Lists.newArrayListWithCapacity(1000));
        DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager(dataSource);

        try {
            // 获取数据列表的大小并记录日志
            int size = excelList.size();
            log.info("数据量是多少:{}", size);

            // 配置事务模板,设置事务传播行为和隔离级别
            TransactionTemplate template = new TransactionTemplate(dataSourceTransactionManager);
            template.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
            template.setIsolationLevel(TransactionDefinition.ISOLATION_REPEATABLE_READ);

            // 将数据列表按批次大小进行分割,并异步执行每个批次的插入操作
            CompletableFuture<Void> async;
            List<List<UserTest>> lists = Lists.partition(excelList, batchSize);
            for (int i = 0; i < lists.size(); i++) {
                List<UserTest> voList = lists.get(i);
                int finalI = i;
                async = CompletableFuture.runAsync(() -> {
                    template.execute(status -> {
                        // 记录当前事务状态和事务源
                        transactionStatuses.add(dataSourceTransactionManager.getTransaction(template));
                        sourceList.add(TransactionSource.copyTransactionSource());
                        try {
                            log.info("子线程开始开始,线程名称:{}", Thread.currentThread().getName());
                            // 执行插入操作并记录插入的行数
                            int inserted = userTestMapper.insertUser(voList);
                            log.info("子线程名称:{},插入了{}条", Thread.currentThread().getName(), inserted);
                            // 模拟错误发生
                            if (finalI > 200) {
                                throw new RuntimeException("我就要在这个时候出错了");
                            }
                            return inserted;
                        } catch (Exception e) {
                            // 记录错误并标记事务为回滚
                            log.error("异步更新出错了,批次:{}", finalI, e);
                            isError.set(true);
                            status.setRollbackOnly();
                            throw new RuntimeException(e);
                        }
                    });
                }, executorService);
                completableFutures.add(async);
            }

            // 等待所有异步任务完成
            CompletableFuture.allOf(completableFutures.toArray(new CompletableFuture[0])).join();

            // 根据是否发生错误进行回滚或提交操作
            int flag = transactionStatuses.size();
            if (isError.get()) {
                log.error("出错了,开始回滚");
                for (int i = 0; i < flag; i++) {
                    sourceList.get(i).autoWiredTransactionResource();
                    dataSourceTransactionManager.rollback(transactionStatuses.get(i));
                    sourceList.get(i).removeTransactionResource();
                }
            } else {
                // 计算成功插入的总行数并提交事务
                Integer orElse = completableFutures.stream().mapToInt(e -> {
                    try {
                        return e.thenApply(v -> batchSize).get();
                    } catch (InterruptedException | ExecutionException ex) {
                        log.error("出错了", ex);
                        return 0;
                    }
                }).sum();
                log.info("成功了,开始更新,更新了:{}条", orElse);
                for (int i = 0; i < flag; i++) {
                    sourceList.get(i).autoWiredTransactionResource();
                    dataSourceTransactionManager.commit(transactionStatuses.get(i));
                    sourceList.get(i).removeTransactionResource();
                }
            }
        } catch (Exception e) {
            log.error("哪里出粗了", e);
        }
    }

    @Builder
    private static class TransactionSource {
        private Map<Object, Object> sources = Maps.newConcurrentMap();

        private Set<TransactionSynchronization> synchronizations = Sets.newLinkedHashSet();
        private String currentTransactionName;

        private Boolean currentTransactionReadOnly;

        private Integer currentTransactionIsolationLevel;

        private Boolean actualTransactionActive;

        /**
         * 复制当前事务的上下文信息并构建一个新的 {@link TransactionSource} 对象。
         * 该函数通过 {@link TransactionSynchronizationManager} 获取当前事务的相关信息,
         * 包括事务资源、同步操作、事务名称、事务只读状态、事务隔离级别以及事务是否处于活动状态,
         * 并将这些信息封装到一个新的 {@link TransactionSource} 对象中。
         *
         * @return 返回一个包含当前事务上下文信息的 {@link TransactionSource} 对象。
         */
        public static TransactionSource copyTransactionSource() {
            // 使用 TransactionSource 的构建器模式创建一个新的 TransactionSource 对象
            return TransactionSource.builder()
                    // 获取当前事务的资源映射并设置为 TransactionSource 的 sources 属性
                    .sources(TransactionSynchronizationManager.getResourceMap())
                    // 初始化一个空的 LinkedHashSet 作为同步操作集合
                    .synchronizations(new LinkedHashSet<>())
                    // 获取当前事务的名称并设置为 TransactionSource 的 currentTransactionName 属性
                    .currentTransactionName(TransactionSynchronizationManager.getCurrentTransactionName())
                    // 获取当前事务的只读状态并设置为 TransactionSource 的 currentTransactionReadOnly 属性
                    .currentTransactionReadOnly(TransactionSynchronizationManager.isCurrentTransactionReadOnly())
                    // 获取当前事务的隔离级别并设置为 TransactionSource 的 currentTransactionIsolationLevel 属性
                    .currentTransactionIsolationLevel(TransactionSynchronizationManager.getCurrentTransactionIsolationLevel())
                    // 获取当前事务是否处于活动状态并设置为 TransactionSource 的 actualTransactionActive 属性
                    .actualTransactionActive(TransactionSynchronizationManager.isActualTransactionActive()).build();
        }


        /**
         * 自动装配事务资源,确保事务相关的资源绑定和同步状态正确设置。
         * 该函数主要执行以下操作:
         * 1. 遍历所有资源,如果资源尚未绑定到当前事务,则将其绑定。
         * 2. 检查并初始化事务同步状态,确保事务同步处于激活状态。
         * 3. 设置当前事务的相关属性,包括事务名称、只读状态、隔离级别以及事务是否激活。
         */
        public void autoWiredTransactionResource() {
            // 遍历所有资源,确保每个资源都绑定到当前事务
            sources.forEach((k, v) -> {
                if (!(TransactionSynchronizationManager.hasResource(k))) {
                    TransactionSynchronizationManager.bindResource(k, v);
                }
            });

            // 检查并初始化事务同步状态,确保事务同步处于激活状态
            if (!TransactionSynchronizationManager.isSynchronizationActive()) {
                TransactionSynchronizationManager.initSynchronization();
            }

            // 设置当前事务的相关属性
            TransactionSynchronizationManager.setCurrentTransactionName(currentTransactionName);
            TransactionSynchronizationManager.setCurrentTransactionReadOnly(currentTransactionReadOnly);
            TransactionSynchronizationManager.setCurrentTransactionIsolationLevel(currentTransactionIsolationLevel);
            TransactionSynchronizationManager.setActualTransactionActive(actualTransactionActive);
        }


        /**
         * 移除与事务相关的资源绑定。
         * 该方法遍历 `sources` 集合中的所有键,检查每个键是否为 `DataSource` 类型。
         * 如果不是 `DataSource` 类型,并且该键在当前事务上下文中已绑定资源,
         * 则解除该键与资源的绑定。
         */
        public void removeTransactionResource() {
            // 检查 sources 是否为 null
            if (sources == null) {
                return;
            }

            // 遍历 `sources` 集合的所有键
            sources.keySet().forEach(e -> {
                // 如果键不是 `DataSource` 类型,并且已绑定资源,则解除绑定
                if (!(e instanceof DataSource) && TransactionSynchronizationManager.hasResource(e)) {
                    TransactionSynchronizationManager.unbindResource(e);
                }
            });
        }

    }
}


FastExcel的maven依赖

  <!-- easyExcel平替 fast excel -->
        <dependency>
            <groupId>cn.idev.excel</groupId>
            <artifactId>fastexcel</artifactId>
            <version>1.1.0</version>
        </dependency>

遗留问题:

数据库查询使用了并行流,原本是想采用多线程分批次去查,但是在编程式事务的环境下,各种报错,找不到链接,打不开链接,链接提前关闭等等,水平有限,无法解决,遂不得已使用并行流去查询。如有大佬知道,请告知,无比感谢

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值