多线程入库事务回滚实现
本文基于CyclicBarrier
和 CompletableFuture
实现springboot
框架下的多线程事务管理。
内容阐述均基于springboot
框架
一、问题点
一般我们入库时都会基于 @Transactional
注解管理我们的事务,但是在多线程入库的场景下该注解不能生效,此时就需要我们手动的去处理事务了。
二、具体实现
1.全局线程池配置
#自定义线程池 (配置在application.yml中)
thread-pool:
# 核心线程数
core-size: 16
# 最大线程数
max-size: 32
# 队列容量
queue-capacity: 1000
# 线程池名称
name: v_eco_thread_pool
/**
* 多线程配置类 配置在 config 下
* 启动异步方法注解
* @author daniel
*/
@Data
@Configuration
@ConfigurationProperties(prefix = "thread-pool")
public class ThreadPoolConfig {
private Integer coreSize;
private Integer maxSize;
private Integer queueCapacity;
private String name;
@Bean(name = "myThreadPool")
public Executor myThreadPool() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(coreSize);
executor.setMaxPoolSize(maxSize);
executor.setQueueCapacity(queueCapacity);
executor.setThreadNamePrefix(name);
// 该策略丢弃队列中最旧的任务,然后尝试重新提交被拒绝的任务。如果该策略与任务提交速度不匹配,可能会导致一些任务永远得不到执行。
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardOldestPolicy());
return executor;
}
}
2.手动事务管理实现
/**
* @author daniel
* @date 2023-09-05 13:12:28
*/
@Slf4j
@Service("statisticsService")
public class StatisticsServiceImpl extends ServiceImpl<StatisticsDao, StatisticsEntity> implements StatisticsService {
@Qualifier("myThreadPool")
@Autowired
private Executor executor;
@Autowired
private DataSourceTransactionManager transactionManager;
@Override
public void saveStatisticInfo() {
// 需要入库的数据 list(这里的list 替换成实际需要入库的list)
List<T> list = new ArrayList();
// 这里可以修改某个数据的长度,模拟数据长度过长 报错场景 判断事务是否回滚
// list.get(100).setData("11111111111111111");
// 这里将多条数据拆分成多个list 多线程入库
List<List<T>> partition = Lists.partition(list, 200);
// 通过 CyclicBarrier 来控制线程提交
int taskCount = partition.size();
// 总共要处理的线程数
CyclicBarrier cyclicBarrier = new CyclicBarrier(taskCount);
// 设置共享变量 记录多线程入库状态 (默认为false)
AtomicBoolean atomicBoolean = new AtomicBoolean(false);
// 事务定义
DefaultTransactionDefinition definition = TransactionUtils.getTransactionDefinition();
// 多线程入库
long start = System.currentTimeMillis();
// 基于CompletableFuture入库,这里借用了 join()阻塞主线程,等所有数据入库完成再返回。不需要返回的可以直接用线程池
CompletableFuture.allOf(partition.stream()
.map(statisticsEntities -> CompletableFuture.runAsync(() -> {
// 获取事务状态 开启事务
TransactionStatus status = transactionManager.getTransaction(definition);
try {
// 这里加判断 如果前面有任务报错 后续的任务就没必要继续执行了,直接去同步点等待事务回滚。
if (!atomicBoolean.get()) {
// 这里的入库是用的mybatisPlus的savebatch,直接this调自身的Transactional注解不生效
this.saveBatch(statisticsEntities);
}
} catch (Exception exception) {
exception.printStackTrace();
atomicBoolean.getAndSet(true);
}
// 等待其他线程到达同步点
try {
cyclicBarrier.await();
} catch (Exception e) {
e.printStackTrace();
}
// 判断 事务是提交 还是 回滚
if (atomicBoolean.get()) {
transactionManager.rollback(status);
} else {
transactionManager.commit(status);
}
}, executor)).toArray(CompletableFuture[]::new)).join();
log.info("save success, total time = {} ms",(System.currentTimeMillis()-start));
}
}
说明:
- 上述例子中是将一个list拆分成多个list模拟多线程入库,实际使用中还可以借助
CompletableFuture
的多任务编排特性来实现不同的数据集合并发入库。
三、附录(使用场景以及优势)
多线程入库是指在数据库操作过程中使用多个线程并行执行入库操作,以提高数据处理的效率和性能。以下是一些使用多线程入库的常见场景和优势:
使用场景:
- 大规模数据导入:当需要将大量数据导入数据库时,使用多线程入库可以将数据分成多个批次并行插入,提高导入速度。
- 数据同步:在数据库间进行数据同步时,可以使用多线程入库将数据从源数据库并行复制到目标数据库,减少同步时间。
- 日志处理:当需要将大量日志数据写入数据库时,多线程入库可以提高写入速度,避免日志积压。
优势:
- 提高性能:多线程入库利用了多核处理器的并行计算能力,可以同时处理多个数据入库任务,加快数据处理速度,提高系统性能和响应时间。
- 增加吞吐量:使用多线程入库可以同时处理多个数据库连接和事务,提高数据库的并发处理能力,增加系统的吞吐量。
- 减少等待时间:多线程入库可以同时执行多个数据库插入操作,减少了等待其他插入操作完成的时间,从而提高整体的执行效率。
- 资源利用率高:通过合理配置线程数目,可以充分利用系统资源,提高系统的资源利用率,降低了数据库资源的浪费。
- 可扩展性强:多线程入库是一种可扩展的方法,可以根据需求增加或减少线程数目,以适应不同规模和负载的数据入库需求。
需要注意的是,使用多线程入库时需要注意线程安全性和并发控制,避免出现数据竞争和冲突。同时,合理的线程数目选择和数据库的配置也是确保多线程入库效果的关键