多线程模拟分布式事务批量插入-Oracle批量获取主键ID

目录

1.多线程批量插入,并实现分布式事务

2.程序升级改造

参考:


系统开发中遇到这样的需求,需要使用Excel表格导入十几万条数据,一开始使用单线程,把所有数据分割为100条一组,每次批量插入100条,效率更高,但是单线程耗时很长,插入十几万条数据的话,要使用十分钟左右,用户等待时间过长。

怎样进行优化呢?多线程几乎是唯一的选择,同时还要注意多线程插入数据时多事务的一致性问题,多事务的一致性问题可以通过模拟二阶段提交的方式,来实现分布式事务的一致性。

下面的例子,我们使用多线程分布式事务批量导入大量数据到Oracle数据库,当然Mysql数据库也一样,甚至更简单。 

1.多线程批量插入,并实现分布式事务

要先插入主表数据,获取到主表的主键ID后,构建子表数据,然后将子表数据分为10组,扔给线程池10个任务,有几组数据就创建几个线程池任务

使用编程式事务和CountDownLatch结合的方式实现多线程的分布式事务

这里为什么使用CountDownLatch,而不使用CyclicBarrier? 因为这里主线程也要进行数据的入库操作,不光子线程中的数据入库,所以具有总分总的这种特点,主线程要等待子线程都执行完毕,等待子线程的结果,子线程也要等待主线程的执行结果,双方要互相等待,故而采用CountDownLatch。

而CyclicBarrier用在主线程不需要进行数据入库的情况下,只需要所有子线程互相等待全部完成数据入库操作。

    @Autowired
    private PlatformTransactionManager manager;

    @Resource
    @Qualifier("commonThreadPool")
    private ThreadPoolExecutor threadPoolExecutor;

    @Override
    public int importData(List<ParentData> ParentDataList, Long groupId, String year){

        //插入新的数据
        //进行分组插入,每次批量插入100条
		//使用编程式事务,开始、提交、回滚事务
        TransactionDefinition definition = new DefaultTransactionDefinition();
        TransactionStatus transaction1 = manager.getTransaction(definition);
        List<List<ParentData>> ParentDataListList = ListSplitUtil.getSumArrayListsss(ParentDataList);
        try{
            for(List<ParentData> list: ParentDataListList) {
                parentDataMapper.insertParentDataBatch(list);
            }
        }catch (Exception e){
		    //异常回滚事务
            manager.rollback(transaction1);
            return 0;
        }

        //取出上面插入的所有信息(为了获取到主键ID)
        ParentData parentDataQuery = new ParentData();
        parentDataQuery.setGroupId(groupId);
        parentDataQuery.setDelFlag("2");
        List<ParentData> parentDataList = parentDataMapper.selectParentDataList(parentDataQuery);
        List<SunData> insertSunDataList = new ArrayList<>();
		
        for (ParentData groupStaff: parentDataList) {
		    //为每个主数据创建5个子数组
            for (int i=0; i<5; i++){
                SunData SunData = new SunData();
                SunData.setYear(year);
                SunData.setStatus("2");
                SunData.settParentDataId(groupStaff.getId());
                SunData.setUserCode(groupStaff.getUserCode());
                insertSunDataList.add(SunData);
            }
        }

        //如果子数据的数量小于1000,那么没必要使用多线程
        try{
            if (insertSunDataList.size() < 1000){
                List<List<SunData>> sunDataListList = ListSplitUtil.getSumArrayListsss(insertSunDataList);
                for (List<SunData> list: sunDataListList) {
                    SunDataMapper.insertSunDataBatch(list);
                }
                manager.commit(transaction1);
                return ParentDataList.size();
            }
        }catch (Exception e){
		    //异常回滚事务
            manager.rollback(transaction1);
            return 0;
        }

        //多线程分布式事务,批量插入子表数据
        //线程安全原子变量,用于记录分布式全局事务是否应该回滚
        AtomicReference<Boolean> rollback = new AtomicReference<>(false);
		//获取当前线程中ThreadLocal中的http请求信息(肯定包括用户信息)
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();

        //将子表数据分为10组,每组大小为subListSize
        int totalSize = insertSunDataList.size();
        int subListSize = totalSize/10 + (totalSize % 10 > 0 ? 1 : 0 );
        List<List<SunData>> totalsunDataListList = new ArrayList<>();
        for (int j = 0; j<totalSize; j+=subListSize){
            List<SunData> subBehaviorList = insertSunDataList.subList(j,Math.min(j + subListSize, totalSize));
			//存储每组数据
            totalsunDataListList.add(subBehaviorList);
        }

		//一共有几组数据,就扔给线程池几个任务
        int nThreads = totalsunDataListList.size();
		//主线程 CountDownLatch
         CountDownLatch mainCountDownLatch = new CountDownLatch(1);
		//子线程 CountDownLatch
        CountDownLatch countDownLatch = new CountDownLatch(nThreads);
        for (int i=0; i< nThreads; i++){
            final List<SunData> inputList = totalsunDataListList.get(i);
            threadPoolExecutor.execute(() -> {
                // 将当前请求的上下文传递给子线程,主要是当前登录用户的信息
                RequestContextHolder.setRequestAttributes(requestAttributes, true);
                //编程式事务
                TransactionStatus transaction = manager.getTransaction(definition);
                try {
                    //继续将数据分组,100条为1组,并插入数据到Oracle数据库
                    List<List<SunData>> sunDataListList = ListSplitUtil.getSumArrayListsss(inputList);
                    for (List<SunData> list: sunDataListList) {
                        SunDataMapper.insertSunDataBatch(list);
                    }
                }catch (Exception e){
                    rollback.set(true);
                }
                //计数-1
                countDownLatch.countDown();

                //阻塞子线程,等待所有子线程完成插入操作(未提交或回滚)
                try {
                    mainCountDownLatch.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                if (rollback.get()){
                    manager.rollback(transaction);
                    return;
                }
                manager.commit(transaction);
            });
        }

        try {
            //计数为0的时候,阻塞结束
            countDownLatch.await();
            //执行主线程,唤醒子线程执行事务提交或回滚操作
            mainCountDownLathc.countDown();

            if (rollback.get()){
                manager.rollback(transaction1);
                return 0;
            }
            manager.commit(transaction1);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }finally {
//            executorService.shutdown();
        }
        success = ParentDataList.size();
        return success;
    }

上面代码用到的工具类,将List数据按照指定大小分组

import org.apache.commons.collections.ListUtils;

import java.util.ArrayList;
import java.util.List;

public class ListSplitUtil {
    /**
     * 按照1000每组对数据进行分组
     * @param list
     * @param <T>
     * @return
     */
    public static  <T>  List<List<T>> getSumArrayList(List<T> list){
        List<List<T>> objectlist = new ArrayList<>();
        int iSize = list.size()/1000;
        int iCount = list.size()%1000;
        for(int i=0;i<=iSize;i++){
            List<T> newObjList = new ArrayList<>();
            if(i==iSize){
                for(int j =i*1000;j<i*1000+iCount;j++ ){
                    newObjList.add(list.get(j));
                }
            }else{
                for(int j =i*1000;j<(i+1)*1000;j++ ){
                    newObjList.add(list.get(j));
                }
            }
            if(newObjList.size()>0){
                objectlist.add(newObjList);
            }
        }
        return objectlist;
    }

    /**
     * 按照500每组对数据进行分组
     * @param list
     * @param <T>
     * @return
     */
    public static  <T>  List<List<T>> getSumArrayLists(List<T> list){
        List<List<T>> objectlist = new ArrayList<>();
        int iSize = list.size()/500;
        int iCount = list.size()%500;
        for(int i=0;i<=iSize;i++){
            List<T> newObjList = new ArrayList<>();
            if(i==iSize){
                for(int j =i*500;j<i*500+iCount;j++ ){
                    newObjList.add(list.get(j));
                }
            }else{
                for(int j =i*500;j<(i+1)*500;j++ ){
                    newObjList.add(list.get(j));
                }
            }
            if(newObjList.size()>0){
                objectlist.add(newObjList);
            }
        }
        return objectlist;
    }

    /**
     * 按照200每组对数据进行分组
     * @param list
     * @param <T>
     * @return
     */
    public static  <T>  List<List<T>> getSumArrayListss(List<T> list){
        List<List<T>> objectlist = new ArrayList<>();
        int iSize = list.size()/200;
        int iCount = list.size()%200;
        for(int i=0;i<=iSize;i++){
            List<T> newObjList = new ArrayList<>();
            if(i==iSize){
                for(int j =i*200;j<i*200+iCount;j++ ){
                    newObjList.add(list.get(j));
                }
            }else{
                for(int j =i*200;j<(i+1)*200;j++ ){
                    newObjList.add(list.get(j));
                }
            }
            if(newObjList.size()>0){
                objectlist.add(newObjList);
            }
        }
        return objectlist;
    }

    /**
     * 按照100每组对数据进行分组
     * @param list
     * @param <T>
     * @return
     */
    public static  <T>  List<List<T>> getSumArrayListsss(List<T> list){
        List<List<T>> objectlist = new ArrayList<>();
        int iSize = list.size()/100;
        int iCount = list.size()%100;
        for(int i=0;i<=iSize;i++){
            List<T> newObjList = new ArrayList<>();
            if(i==iSize){
                for(int j =i*100;j<i*100+iCount;j++ ){
                    newObjList.add(list.get(j));
                }
            }else{
                for(int j =i*100;j<(i+1)*100;j++ ){
                    newObjList.add(list.get(j));
                }
            }
            if(newObjList.size()>0){
                objectlist.add(newObjList);
            }
        }
        return objectlist;
    }

    /**
     * 按照 输入变量count每组,对数据进行分组
     * @param list
     * @param <T>
     * @return
     */
    public static  <T>  List<List<T>> getSumArrayList(List<T> list,Integer count){
        List<List<T>> objectlist = ListUtils.synchronizedList(new ArrayList<>());
        int iSize = list.size()/count;
        int iCount = list.size()%count;
        for(int i=0;i<=iSize;i++){
            List<T> newObjList = ListUtils.synchronizedList(new ArrayList<>());
            if(i==iSize){
                for(int j =i*count;j<i*count+iCount;j++ ){
                    newObjList.add(list.get(j));
                }
            }else{
                for(int j =i*count;j<(i+1)*count;j++ ){
                    newObjList.add(list.get(j));
                }
            }
            if(newObjList.size()>0){
                objectlist.add(newObjList);
            }
        }
        return objectlist;
    }
}

2.程序升级改造

上面的程序要先批量插入主表数据,全部插入完成后再从数据库中取出,以此来获取主键ID,这是因为批量插入数据的时候,无法返回主键ID。

子表需要用到主表的主键ID。

怎样在批量插入的时候返回主键ID呢?先为要插入的数据获取到主键ID(通过该表的序列),然后再批量插入数据

xml:批量获取主键ID

    <!-- 获取待插入集合,包含主建ID -->
    <select id="initInsertBatch" parameterType="com.bruce.test.q51.domain.ParentData" resultMap="ParentDataResult">
        select t.* from (
        <foreach collection="list" index="index" item="item" separator="union all">
            select
            get_seq_next('seq_parent_data') as id,
            #{item.userName} as user_name,
            #{item.userCode} as user_code,
            #{item.userMail} as user_mail,
            #{item.deptName} as dept_name,
            #{item.position} as position,
            #{item.post} as post,
            #{item.grade} as grade,
            #{item.nationality} as nationality,
            from dual
        </foreach>
        )t
    </select>

Mapper:

    /**
     * 获取待插入集合包含主建ID
     * @param entities
     * @return
     */
    public List<ParentData> initInsertBatch(@Param("list") List<ParentData> entities);

service:

    @Autowired
    private PlatformTransactionManager manager;

    @Resource
    @Qualifier("commonThreadPool")
    private ThreadPoolExecutor threadPoolExecutor;

    @Override
    public int importData(List<ParentData> ParentDataList, Long groupId, String year){

        //插入新的数据
        //进行分组插入,每次批量插入100条
		//使用编程式事务,开始、提交、回滚事务
        TransactionDefinition definition = new DefaultTransactionDefinition();
        TransactionStatus transaction1 = manager.getTransaction(definition);
        List<List<ParentData>> ParentDataListList = ListSplitUtil.getSumArrayListsss(ParentDataList);
        try{
		    for (int i=0; i<ParentDataListList.size(); i++){
			    //先批量获取主键ID
                List<ParentData> listGe = parentDataMapper.initInsertBatch(ParentDataListList.get(i));
                //重新赋值(带主键ID的数据),并释放原来数据(垃圾回收)
                ParentDataListList.set(i,listGe);
                parentDataMapper.insertParentDataBatch(listGe);
            }
        }catch (Exception e){
		    //异常回滚事务
            manager.rollback(transaction1);
            return 0;
        }

        //取出上面插入的所有信息(为了获取到主键ID)
        ParentData parentDataQuery = new ParentData();
        parentDataQuery.setGroupId(groupId);
        parentDataQuery.setDelFlag("2");
        List<ParentData> parentDataList = parentDataMapper.selectParentDataList(parentDataQuery);
        List<SunData> insertSunDataList = new ArrayList<>();
		
        for(List<ParentData> list: ParentDataListList) {
			for (ParentData groupStaff: list) {
				//为每个主数据创建5个子数组
				for (int i=0; i<5; i++){
					SunData SunData = new SunData();
					SunData.setYear(year);
					SunData.setStatus("2");
					SunData.settParentDataId(groupStaff.getId());
					SunData.setUserCode(groupStaff.getUserCode());
					insertSunDataList.add(SunData);
				}
			}
		}

        //如果子数据的数量小于1000,那么没必要使用多线程
        try{
            if (insertSunDataList.size() < 1000){
                List<List<SunData>> sunDataListList = ListSplitUtil.getSumArrayListsss(insertSunDataList);
                for (List<SunData> list: sunDataListList) {
                    SunDataMapper.insertSunDataBatch(list);
                }
                manager.commit(transaction1);
                return ParentDataList.size();
            }
        }catch (Exception e){
		    //异常回滚事务
            manager.rollback(transaction1);
            return 0;
        }

        //多线程分布式事务,批量插入子表数据
        //线程安全原子变量,用于记录分布式全局事务是否应该回滚
        AtomicReference<Boolean> rollback = new AtomicReference<>(false);
		//获取当前线程中ThreadLocal中的http请求信息(肯定包括用户信息)
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();

        //将子表数据分为10组,每组大小为subListSize
        int totalSize = insertSunDataList.size();
        int subListSize = totalSize/10 + (totalSize % 10 > 0 ? 1 : 0 );
        List<List<SunData>> totalsunDataListList = new ArrayList<>();
        for (int j = 0; j<totalSize; j+=subListSize){
            List<SunData> subBehaviorList = insertSunDataList.subList(j,Math.min(j + subListSize, totalSize));
			//存储每组数据
            totalsunDataListList.add(subBehaviorList);
        }

		//一共有几组数据,就扔给线程池几个任务
        int nThreads = totalsunDataListList.size();
		//主线程 CountDownLatch
         CountDownLatch mainCountDownLatch = new CountDownLatch(1);
		//子线程 CountDownLatch
        CountDownLatch countDownLatch = new CountDownLatch(nThreads);
        for (int i=0; i< nThreads; i++){
            final List<SunData> inputList = totalsunDataListList.get(i);
            threadPoolExecutor.execute(() -> {
                // 将当前请求的上下文传递给子线程,主要是当前登录用户的信息
                RequestContextHolder.setRequestAttributes(requestAttributes, true);
                //编程式事务
                TransactionStatus transaction = manager.getTransaction(definition);
                try {
                    //继续将数据分组,100条为1组,并插入数据到Oracle数据库
                    List<List<SunData>> sunDataListList = ListSplitUtil.getSumArrayListsss(inputList);
                    for (List<SunData> list: sunDataListList) {
                        SunDataMapper.insertSunDataBatch(list);
                    }
                }catch (Exception e){
                    rollback.set(true);
                }
                //计数-1
                countDownLatch.countDown();

                //阻塞子线程,等待所有子线程完成插入操作(未提交或回滚)
                try {
                    mainCountDownLatch.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                if (rollback.get()){
                    manager.rollback(transaction);
                    return;
                }
                manager.commit(transaction);
            });
        }

        try {
            //计数为0的时候,阻塞结束
            countDownLatch.await();
            //执行主线程,唤醒子线程执行事务提交或回滚操作
            mainCountDownLathc.countDown();

            if (rollback.get()){
                manager.rollback(transaction1);
                return 0;
            }
            manager.commit(transaction1);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }finally {
//            executorService.shutdown();
        }
        success = ParentDataList.size();
        return success;
    }

 Note:因为目前实现了批量插入的时候获取到主键ID,因此现在不再需要主线程插入主表数据,再查表获取到所有主键ID,然后使用多线程插入子表数据的方式。而是可以直接把主表数据分为10份,然后全部在子线程中处理就可以了,在子线程中批量插入主表数据(总数的十分之一)时可以直接获取到主键ID,然后构建子表数据,最后再批量插入子表数据就可以了。

并且这时候可以使用CyclicBarrier代替CountDownLatch,更简单的实现多线程的分布式事务

参考:

1.在所有线程中,只使用了一个transactionManager,也就是说所有线程使用同一个事务,这样行的通吗?因为本人是每个线程一个独立的事务去实现的

java多线程之CountDownLatch_java countdownlatch控制多线程事务提交-CSDN博客

2. 批量获取主键ID参考文章(光看第一篇文章不好用,下面2篇文章结合看)

mybatis批量插入oracle并返回主建ID_mybatis 实现oracle 批量插入并返回主键-CSDN博客

ORA-02287:此处不允许序号(sequence number not allowed here) 的避免以及强制实现-CSDN博客

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
SpringBoot 执行批量插入可以通过使用JdbcTemplate的batchUpdate()方法来实现。如果数据量很大,可以采用多线程和多批量插入的方式来提高插入效率。 下面是一个示例代码: ```java @Service public class UserService { @Autowired private JdbcTemplate jdbcTemplate; public void batchInsert(List<User> userList) throws InterruptedException { int batchSize = 1000; int threadCount = Runtime.getRuntime().availableProcessors(); ExecutorService executorService = Executors.newFixedThreadPool(threadCount); List<List<User>> splitList = ListUtils.splitList(userList, batchSize); CountDownLatch countDownLatch = new CountDownLatch(splitList.size()); for (List<User> list : splitList) { executorService.execute(() -> { try { batchInsert(list); } finally { countDownLatch.countDown(); } }); } countDownLatch.await(); executorService.shutdown(); } private void batchInsert(List<User> userList) { String sql = "INSERT INTO user (name, age) VALUES (?, ?)"; jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() { @Override public void setValues(PreparedStatement ps, int i) throws SQLException { User user = userList.get(i); ps.setString(1, user.getName()); ps.setInt(2, user.getAge()); } @Override public int getBatchSize() { return userList.size(); } }); } } ``` 这个示例中,我们先将数据按照固定大小分成多个批次,然后采用多线程的方式,每个线程执行一个批次的插入操作。我们通过CountDownLatch来控制所有线程都执行完毕后再退出程序。这样可以确保所有数据都被正确地插入到数据库中。 注意,这里的ListUtils.splitList()方法是一个自定义的分割List的工具类,具体实现可以参考Guava库中的Lists.partition()方法。另外,为了简化代码,这里省略了User实体类的定义。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值