问题:

多线程破坏了方法事务的原子性。Spring 是使用ThreadLocal来存储Connection的,不同的线程Connection肯定不一样。所以我们加载Service 方法上的 @Transactional 是不起作用的。

思路:

每一个线程的事务是原子性的,所有的线程事务都提交了,这个操作的事务才是完成了。

代码:

  1. 明确子线程是执行业务的线程,主线程是控制事务的线程。
package com.xcr.thread.entity;

import lombok.Data;
import lombok.experimental.Accessors;

import java.util.concurrent.CountDownLatch;

/**
 * @author L
 * @Description
 * @Date 2021/8/25 14:51
 */
@Data
@Accessors(chain = true)
public class TransactionInfo {

    /**
     * 主线程控制器(子线程用)
     */
    private CountDownLatch mainLatch;
    /**
     * 子线程控制器(主线程用)
     */
    private CountDownLatch threadLatch;
    /**
     * 判断是否需要回滚
     */
    private Boolean rollBack;
}
  1. 使用countDownLatch阻断线程执行,当子线程全部执行完 threadLatch.await(); 子线程控制器将不再等待。
public static void executeThreadPool(List list){
        if(CollectionUtils.isEmpty(list)){
            return;
        }
        //通过ThreadPoolExecutor构造函数自定义参数创建
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                CPU_NUM * 2,
                CPU_NUM * 2 + 1,
                KEEP_ALIVE_TIME,
                TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<>(QUEUE_CAPACITY),
                new ThreadPoolExecutor.CallerRunsPolicy());

        // 主线程控制器(子线程用)
        CountDownLatch mainLatch = new CountDownLatch(1);
        // 子线程控制器(主线程用)
        CountDownLatch threadLatch = null;
        // 判断是否需要回滚
        Boolean rollBack = Boolean.FALSE;

        TransactionInfo transactionInfo = new TransactionInfo();
        transactionInfo.setMainLatch(mainLatch);
        transactionInfo.setRollBack(rollBack);

        long start = System.currentTimeMillis();
        try {
            List<List> split = Lists.partition(list, 1);
            threadLatch = new CountDownLatch(split.size());
            transactionInfo.setThreadLatch(threadLatch);
            for (int i = 0; i < split.size(); i++) {
                List threadList = split.get(i);
                executor.execute(new SaveDataTask(batchService, threadList, i, transactionInfo));
            }
            threadLatch.await();
        }catch (Exception e){
            log.error("主线程异常,回滚所有数据");
            rollBack = Boolean.FALSE;
        }finally {
            mainLatch.countDown();
            long end = System.currentTimeMillis();
            log.info("完成耗时:"+(end - start));
            executor.shutdown();
        }
    }
  1. 当某一个子线程抛出异常,将transactionInfo中的rollBack设置为true。当主线程开关打开,发现每个子线程还有一段代码,是需要回滚事务的,所以虽然主线程这个时候也走完了,但是所有子线程的事务都回滚了。
package com.xcr.thread.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.xcr.thread.dao.UserMapper;
import com.xcr.thread.entity.TransactionInfo;
import com.xcr.thread.service.SaveBatchService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import com.xcr.thread.entity.User;
import java.util.List;

/**
 * @author L
 * @Description 用于插入数据的业务实现
 * @Date 2021/8/25 14:56
 */
@Slf4j
@Service("saveBatchService")
public class SaveBatchServiceImpl extends ServiceImpl<UserMapper, User> implements SaveBatchService {

    @Override
    @Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRED)
    public void batchSaveData(TransactionInfo transactionInfo, List list, Integer batch) throws Exception{
        Exception ex = null;
        try {
            if(batch == 2){
                System.out.println(batch/0);
            }
            this.saveBatch(list);
        } catch (Exception e) {
            log.error("保存失败需要回滚", e);
            transactionInfo.setRollBack(Boolean.TRUE);
            ex = new RuntimeException(e.getMessage());
        } finally {
            // 执行完成 等待主线程通知回滚
            transactionInfo.getThreadLatch().countDown();
            // 等待主线程通知
            transactionInfo.getMainLatch().await();
            if (transactionInfo.getRollBack()) {
                // 出现异常 需要回滚
                log.error("出现异常 需要连带回滚事务");
                throw ex == null ? new RuntimeException("出现异常 需要连带回滚") : ex;
            }
        }
    }
}
2021-09-26 19:10:10.166  INFO 10720 --- [nio-8111-exec-1] c.x.t.util.TransactionalExecutorUtil     : 完成耗时:3285
2021-09-26 19:10:10.166 ERROR 10720 --- [pool-2-thread-4] c.x.t.service.impl.SaveBatchServiceImpl  : 出现异常 需要连带回滚事务
2021-09-26 19:10:10.166 ERROR 10720 --- [pool-2-thread-3] c.x.t.service.impl.SaveBatchServiceImpl  : 出现异常 需要连带回滚事务
2021-09-26 19:10:10.227 ERROR 10720 --- [pool-2-thread-3] com.xcr.thread.task.SaveDataTask         : 第2批次插入失败。Exception:/ by zero
2021-09-26 19:10:10.271 ERROR 10720 --- [pool-2-thread-5] com.xcr.thread.task.SaveDataTask         : 第4批次插入失败。Exception:出现异常 需要连带回滚
2021-09-26 19:10:10.271 ERROR 10720 --- [pool-2-thread-4] com.xcr.thread.task.SaveDataTask         : 第3批次插入失败。Exception:出现异常 需要连带回滚
2021-09-26 19:10:10.279 ERROR 10720 --- [pool-2-thread-1] com.xcr.thread.task.SaveDataTask         : 第0批次插入失败。Exception:出现异常 需要连带回滚
2021-09-26 19:10:10.283 ERROR 10720 --- [pool-2-thread-2] com.xcr.thread.task.SaveDataTask         : 第1批次插入失败。Exception:出现异常 需要连带回滚
  1. 主要的代码都在上面,下面是一些其他不重要的代码。
package com.xcr.thread.task;


import com.xcr.thread.entity.TransactionInfo;
import com.xcr.thread.service.SaveBatchService;
import lombok.extern.slf4j.Slf4j;

import java.util.List;

/**
 * @author L
 * @Description 批量插入数据业务执行(子线程)
 * @Date 2021/6/11 18:05
 */
@Slf4j
public class SaveDataTask implements Runnable{
    private SaveBatchService saveBatchService;
    private Integer batch;
    private List list;
    private TransactionInfo transactionInfo;
    public SaveDataTask(SaveBatchService baseService, List list, Integer batch, TransactionInfo transactionInfo){
        this.saveBatchService = baseService;
        this.batch = batch;
        this.list = list;
        this.transactionInfo = transactionInfo;
    }
    @Override
    public void run() {
        try {
            saveBatchService.batchSaveData(transactionInfo, list, batch);
            log.error("线程名称" + Thread.currentThread().getName() + "--->> 第" + this.batch + "批次插入成功");
        }catch (Exception e){
            log.error("第" + this.batch + "批次插入失败。Exception:" + e.getMessage());
        }
    }
}

总结:

其实这是分布式事务的内容,主流的分布式框架seata,下一篇会写。
小弟第一次写博客,希望大家多多指导。