1、背景
最近接受到接口优化的任务,查看代码逻辑后发现在批量处理数据耗时长,想到使用多线程处理批量数据,又要保持原来的事务一致性。从网上检索出很多方法,经过尝试记录了2中方法。
2、实现方法
(1)、方法一
@Component
@Slf4j
public class MultiThreadingTransactionManager {
/**
* 数据源事务管理器
*/
@Autowired
private DataSourceTransactionManager dataSourceTransactionManager;
@Autowired
private ThreadPoolTaskExecutor executorService;
private long timeout = 120;
public boolean execute(List<Runnable> runnableList,List<String> factorySchema) {
/**
* 用于判断子线程业务是否处理完成
* 处理完成时threadCountDownLatch的值为0
*/
CountDownLatch mainCountDownLatch = new CountDownLatch(1);
/**
* 用于等待子线程全部完成后,子线程统一进行提交和回滚
* 进行提交和回滚时mainCountDownLatch的值为0
*/
CountDownLatch threadCountDownLatch = new CountDownLatch(runnableList.size());;
/**
* 是否提交事务,默认是true,当子线程有异常发生时,设置为false,回滚事务
*/
AtomicBoolean isSubmit = new AtomicBoolean(true);
for(int i =0;i<runnableList.size();i++){
int finalI=i;
executorService.execute(() ->{
log.info("子线程: [" + Thread.currentThread().getName() + "]");
// 判断别的子线程是否已经出现错误,错误别的线程已经出现错误,那么所有的都要回滚,这个子线程就没有必要执行了
if (!isSubmit.get()) {
log.info("整个事务中有子线程执行失败需要回滚, 子线程: [" + Thread.currentThread().getName() + "] 终止执行");
// 计数器减1,代表该子线程执行完毕
threadCountDownLatch.countDown();
return;
}
SchemaContextHolder.setSchema(factorySchema.get(finalI));
// 开启事务
DefaultTransactionDefinition defaultTransactionDefinition = new DefaultTransactionDefinition();
TransactionStatus transactionStatus = dataSourceTransactionManager.getTransaction(defaultTransactionDefinition);
try {
// 执行业务逻辑
runnableList.get(finalI).run();
} catch (Exception exception) {
// 发生异常需要进行回滚,设置isSubmit为false
isSubmit.set(false);
log.info("子线程: [" + Thread.currentThread().getName() + "]执行业务发生异常,异常为: " + exception.getMessage());
throw new ServiceException(exception.getMessage());
} finally {
// 计数器减1,代表该子线程执行完毕
threadCountDownLatch.countDown();
}
try {
// 等待主线程执行
mainCountDownLatch.await();
} catch (Exception exception) {
log.info("子线程: [" + Thread.currentThread().getName() + "]等待提交或回滚异常,异常为: " + exception.getMessage());
throw new ServiceException(exception.getMessage());
}
try {
// 提交
if (isSubmit.get()) {
dataSourceTransactionManager.commit(transactionStatus);
log.info("子线程: [" + Thread.currentThread().getName() + "]进行事务提交");
} else {
dataSourceTransactionManager.rollback(transactionStatus);
log.info("子线程: [" + Thread.currentThread().getName() + "]进行事务回滚");
}
} catch (Exception exception) {
log.info("子线程: [" + Thread.currentThread().getName() + "]进行事务提交或回滚出现异常,异常为:" + exception.getMessage());
throw new ServiceException(exception.getMessage());
}
});
}
// 等待子线程全部执行完毕
try {
// 若计数器变为零了,则返回 true
boolean isFinish = threadCountDownLatch.await(timeout, TimeUnit.SECONDS);
if (!isFinish) {
// 如果还有为执行完成的就回滚
isSubmit.set(false);
log.info("存在子线程在预期时间内未执行完毕,任务将全部回滚");
}
} catch (Exception exception) {
log.info("主线程发生异常,异常为: " + exception.getMessage());
throw new ServiceException(exception.getMessage());
} finally {
// 计数器减1,代表该主线程执行完毕
mainCountDownLatch.countDown();
}
// 返回结果,是否执行成功,事务提交即为执行成功,事务回滚即为执行失败
return isSubmit.get();
}
}
(2)、方法二
@Component
public class MultiThreadingTransactionManager {
private final Logger log = LoggerFactory.getLogger(getClass());
@Autowired
private ThreadPoolTaskExecutor executorService;
@Autowired
DataSource multipleDataSource;
/**
* 生成一个事务管理器
*/
private DataSourceTransactionManager genTransactionManager() {
return new DataSourceTransactionManager(multipleDataSource);
}
/**
* 执行的是无返回值的任务
*
* @param tasks 异步执行的任务列表
* @param factorySchemas 异步执行任务需要用到的线程池,考虑到线程池需要隔离,这里强制要求传
*/
public boolean execute(List<Runnable> tasks, List<String> factorySchemas) {
try {
// 参数有效性校验
if (tasks == null || tasks.size() == 0) {
return false;
}
DataSourceTransactionManager transactionManager = genTransactionManager();
// 用于标记是否存在线程发生了异常
AtomicBoolean ex = new AtomicBoolean();
// 混合事务定义信息map,用于存储 TransactionStatus & TransactionResource
Map<Integer, Map<TransactionStatus, TransactionResource>> multiplyTransactionDefinition = new ConcurrentHashMap<>();
// 开辟多线程,将CompletableFuture存储到taskFutureList,方便后面的管理
List<CompletableFuture<?>> taskFutureList = new ArrayList<>(tasks.size());
for(int i=0;i<tasks.size();i++){
int finalI = i;
taskFutureList.add(CompletableFuture.runAsync(() -> {
try {
// 1.填充值(事务状态 & 事务资源)到混合事务定义map中
multiplyTransactionDefinition.put(
tasks.get(finalI).hashCode(),
new HashMap<TransactionStatus, TransactionResource>(1){
{
put(openNewTransaction(transactionManager,factorySchemas.get(finalI)), TransactionResource.copyTransactionResource());
}
}
);
// 2.异步任务执行
tasks.get(finalI).run();
} catch (Throwable throwable) {
// 打印异常
throwable.printStackTrace();
// 异常标记
ex.set(Boolean.TRUE);
// 其他任务还没执行的不需要执行了
taskFutureList.forEach(completableFuture -> completableFuture.cancel(true));
}
}, executorService)
);
}
try {
// 阻塞直到所有任务全部执行结束---如果有任务被取消,这里会抛出异常,需要捕获
CompletableFuture.allOf(taskFutureList.toArray(new CompletableFuture[]{})).get();
} catch (Exception ignored) {}
// 遍历所有的task做事务的提交或回滚
for (int i = 0; i < tasks.size(); i++) {
// 根据线程task的hashCode获取对应线程事务的状态和资源
Map<TransactionStatus, TransactionResource> transactionStatusAndResource = multiplyTransactionDefinition.get(tasks.get(i).hashCode());
// 根据事务状态和事务资源回滚事务
for (Map.Entry<TransactionStatus, TransactionResource> entry : transactionStatusAndResource.entrySet()) {
// 载入事务资源
entry.getValue().loadTransactionResource();
// 发生了异常则进行回滚操作,否则提交
if(ex.get()) {
// 回滚
transactionManager.rollback(entry.getKey());
log.warn("已完成第 {} 个事务的回滚...", (i + 1));
} else {
// 提交
transactionManager.commit(entry.getKey());
entry.getKey().isNewTransaction();
log.info("已完成第 {} 个事务的提交...", (i + 1));
}
// 卸载事务资源
entry.getValue().unLoadTransactionResource();
}
}
// 返回多线程执行结果(true表示成功,false表示失败)
return !ex.get();
} catch (TransactionException e) {
throw new RuntimeException(e);
}
}
/**
* 开启一个新事务
* @param transactionManager 事务管理器
*/
private TransactionStatus openNewTransaction(DataSourceTransactionManager transactionManager,String factorySchema) {
SchemaContextHolder.setSchema(factorySchema);
// JdbcTransactionManager根据TransactionDefinition信息来进行一些连接属性的设置(包括隔离级别和传播行为等)
DefaultTransactionDefinition transactionDef = new DefaultTransactionDefinition();
// 开启一个新事务---此时autocommit已经被设置为了false,并且当前没有事务,这里创建的是一个新事务
return transactionManager.getTransaction(transactionDef);
}
/**
* 保存当前事务资源,用于线程间的事务资源COPY操作
*/
@Builder
private static class TransactionResource {
// 保存当前事务关联的资源--默认只会在新建事务的时候保存当前获取到的DataSource和当前事务对应Connection的映射关系--当然这里Connection被包装为了ConnectionHolder
// 事务结束后默认会移除集合中的DataSource作为key关联的资源记录
private final Map<Object, Object> resources;
// 下面五个属性会在事务结束后被自动清理,无需我们手动清理
// 事务监听者--在事务执行到某个阶段的过程中,会去回调监听者对应的回调接口(典型观察者模式的应用)---默认为空集合
private Set<TransactionSynchronization> synchronizations;
// 存放当前事务名字
private final String currentTransactionName;
// 存放当前事务是否是只读事务
private final Boolean currentTransactionReadOnly;
// 存放当前事务的隔离级别
private final Integer currentTransactionIsolationLevel;
// 存放当前事务是否处于激活状态
private final Boolean actualTransactionActive;
public TransactionResource(Map<Object, Object> resources, Set<TransactionSynchronization> synchronizations, String currentTransactionName, Boolean currentTransactionReadOnly, Integer currentTransactionIsolationLevel, Boolean actualTransactionActive) {
this.resources = resources;
this.synchronizations = synchronizations;
this.currentTransactionName = currentTransactionName;
this.currentTransactionReadOnly = currentTransactionReadOnly;
this.currentTransactionIsolationLevel = currentTransactionIsolationLevel;
this.actualTransactionActive = actualTransactionActive;
}
/**
* 复制事务资源
*/
public static TransactionResource copyTransactionResource() {
final TransactionResource build = TransactionResource.builder()
//返回不可变集合
.resources(new HashMap<>())
//如果需要注册事务监听者,这里记得修改
.synchronizations(new LinkedHashSet<>())
.currentTransactionName(TransactionSynchronizationManager.getCurrentTransactionName())
.currentTransactionReadOnly(TransactionSynchronizationManager.isCurrentTransactionReadOnly())
.currentTransactionIsolationLevel(TransactionSynchronizationManager.getCurrentTransactionIsolationLevel())
.actualTransactionActive(TransactionSynchronizationManager.isActualTransactionActive())
.build();
build.equipData(TransactionSynchronizationManager.getResourceMap());
return build;
}
public void equipData(Map<Object,Object> from) {
this.resources.putAll(from);
}
/**
* 载入事务所需资源
*/
public void loadTransactionResource() {
resources.forEach((key,item)->{
System.out.println(((DataSource)key));
try{
TransactionSynchronizationManager.bindResource(key,item);
}catch (Exception e){
}
});
//如果是新事务才进行初始化
if(!TransactionSynchronizationManager.isActualTransactionActive()) {
TransactionSynchronizationManager.initSynchronization();
}
TransactionSynchronizationManager.setActualTransactionActive(actualTransactionActive);
TransactionSynchronizationManager.setCurrentTransactionName(currentTransactionName);
TransactionSynchronizationManager.setCurrentTransactionIsolationLevel(currentTransactionIsolationLevel);
TransactionSynchronizationManager.setCurrentTransactionReadOnly(currentTransactionReadOnly);
}
/**
* 卸载事务资源
*/
public void unLoadTransactionResource() {
// 事务结束后默认会移除集合中的DataSource作为key关联的资源记录
// DataSource如果重复移除,unbindResource时会因为不存在此key关联的事务资源而报错
resources.keySet().forEach(key -> {
if (!(key instanceof DataSource)) {
if(TransactionSynchronizationManager.getResource(key)!=null){
TransactionSynchronizationManager.unbindResource(key);
};
}
});
}
}
}
(3)、方法一测试类
@RestController
@RequestMapping("test")
public class TestController {
@Autowired
TestService testService;
@Autowired
MultiThreadingTransactionManager multiThreadingTransactionManager;
@RequestMapping("test")
public String test(){
List<TestBean> list = new ArrayList<>();
list.add(new TestBean("2",1));
list.add(new TestBean("3",2));
List<Runnable> runnableList = new ArrayList<>();
list.forEach(testBean -> runnableList.add(() -> {
testService.insert(testBean);
}));
boolean isSuccess = multiThreadingTransactionManager.execute(runnableList,"db9771");
System.out.println(isSuccess);
return "ok";
};
}
(4)、方法二测试类
@Override
@Transactional
public void test(String id, Integer num) {
String sql1 ="INSERT INTO test (id, num) VALUES (%s, %d)";
List<Runnable> runnables=new ArrayList<>();
runnables.add(()->{
jdbcTemplate.execute(String.format(sql1,"1",2));
});
runnables.add(()->{
jdbcTemplate.execute(String.format(sql1,"3",4));
});
multiThreadingTransactionManager.execute(runnables, Arrays.asList("db6640","db6641"));
}
3、总结
【方法一】中所有子线程在各自线程内开启事务,执行业务逻辑后,判断是否抛错,一旦抛错,会把全局AtomicBoolean置为false。所有子线程完业务代码会等待主线程,全部子线程执行业务结束后,主线程等待结束,判断AtomicBoolean是什么状态,一旦false,所有子线程回滚,否则提交。
【方法二】中每个子线程创建事务,并将事务状态和事务资源存入map中,在子线程创建的事务复制一份,放入到集合中,主线程中遍历map,将复制的事务资源载入事务资源统一提交或回滚。如果像方法二中测试类一样,添加声明式事务@Transactional注解,会造成No value for key [com.course.datasource.DynamicDatasourceConfig$$EnhancerBySpringCGLIB$$3b6ae611@796613b7] bound to thread [http-nio-8081-exec-1]
错误,原因是主线程开启了事务,子线程事务绑定的动态数据源和主线程一致,在遍历map提交事务后,会清理事务资源,主线程声明式事务又会清理一次,导致错误发生,所以方法二不能和声明式事务一起使用。
【区别】方法一,事务开启回滚提交在子线程中进行,通过原子的布尔型状态判断回滚或提交。
方法二,事务开启式子线程中,提交或回滚在主线程中。