目录
系统开发中遇到这样的需求,需要使用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博客