• JAVA如何实现把百万级数据导入到数据库
  • 发布于 1天前
  • 25 热度
    0 评论
  • Dock
  • 0 粉丝 59 篇博客
  •   
处理大规模数据的时候,把百万级Excel数据导入数据库,是很多人都会碰到的事。这里面门道不少,今天就跟大家好好聊聊实操技巧。

1、先说说遇到的麻烦
处理百万级Excel数据导入,常会碰到几个头疼问题:
内存扛不住:直接把整个Excel加载到内存,数据量一大就容易溢出。比如用Apache POI的某些方式,数据全堆在内存里,百万行数据根本顶不住。
速度慢得让人着急:单线程处理的话,读数据、插数据库都慢。尤其是一条一条往数据库插,不仅数据库压力大,整体效率也低。

各种报错不好处理:Excel里数据格式不对、前后不一致,数据库连接突然断了,或者数据重复,这些情况都可能让导入中断,还容易导致数据乱掉。


2、选对工具和方法很关键
要解决这些问题,选对技术工具和方法很重要:
用EasyExcel处理Excel:它用的是流式读取,不会把整个文件一次性加载到内存,而是一行一行从磁盘读、解析,内存占用能省很多。百万行的Excel,用传统POI可能直接内存溢出,EasyExcel逐行处理就稳得多。
多线程能提速:多线程可以同时处理任务,这里主要用在两个地方:多线程读Excel,多线程往数据库插数据。用生产者-消费者模式,多个线程一起读数据,读到的数据交给其他线程插库,整体速度能提一大截。比如建个线程池,每个线程负责读一个Sheet,或者负责插一批数据。

数据库批量插入更高效:别一条一条插,批量插入能减少和数据库的交互次数,数据库压力小了,速度也快了。不同数据库批量插入语法不一样,MySQL可以用INSERT INTO ... VALUES (...) , (...),Oracle能用INSERT ALL;搭配MyBatis这类框架,批量插入也很好实现。


3、具体怎么操作 先看读数据
用EasyExcel的ReadListener逐行读数据,先定义一个监听器来处理每一行数据:
public  class  MyDataModelListener extends  AnalysisEventListener<MyDataModel> {
    // 每攒够1000条数据,就批量处理一次,这个数量可以根据实际情况调整
    private static finalint  BATCH_SIZE = 1000;
    // 用来存批量数据的列表
    private  List<MyDataModel> batch = new   ArrayList<>();
    // 操作数据库的服务类,通过它把数据存到数据库
    private  MyDataService myDataService;

    // 初始化监听器时,传入数据库服务,方便后续调用存储方法
    public  MyDataModelListener(MyDataService myDataService) {
        this.myDataService = myDataService;
    }

    // 每读一行数据,就会触发这个方法
    @Override
    public  void  invoke(MyDataModel data, AnalysisContext context) {
        // 先校验数据是否有效,比如检查必填字段、格式是否正确等
        if (validateData(data)) {
            // 有效数据就加到批量列表里
            batch.add(data);
        } else {
            // 无效数据可以记日志,或者单独存起来后续处理,这里先简单跳过
        }
        // 当批量列表里的数据够1000条了,就调用处理方法
        if (batch.size() >= BATCH_SIZE) {
            processBatch();
        }
    }

    // 整个Excel读完后,会触发这个方法
    @Override
    public  void  doAfterAllAnalysed(AnalysisContext context) {
        // 剩下没处理的少量数据,最后再处理一次,避免遗漏
        if (!batch.isEmpty()) {
            processBatch();
        }
    }

    // 批量处理数据,这里是调用服务类的批量存储方法
    private  void  processBatch() {
        myDataService.saveBatch(batch);
        // 存完后清空列表,准备接收下一批数据
        batch.clear();
    }
    // 堆代码 duidaima.com
    // 数据校验的方法,根据自己的业务逻辑写校验规则
    private  boolean  validateData(MyDataModel data) {
        // 比如检查某个必填字段是否为空,或者格式是否符合要求
        // 符合条件返回true,否则返回false
        return true; // 这里只是示例,实际要写具体校验逻辑
    }
}
如果Excel有多个Sheet,用线程池同时读多个Sheet,效率更高:
// Excel文件的路径
String filePath = "/users/workspace/excel/test.xlsx";
// 要读取的Sheet数量
int  numberOfSheets = 20;
// 建一个和Sheet数量一样的线程池,每个线程处理一个Sheet
ExecutorService executor = Executors.newFixedThreadPool(numberOfSheets);
// 循环每个Sheet,让线程池里的线程去处理
for (int  sheetNo = 0; sheetNo < numberOfSheets; sheetNo++) {
    // 因为lambda表达式里不能直接用循环变量,所以定义一个final变量
    int  finalSheetNo = sheetNo;
    // 把读取Sheet的任务提交给线程池
    executor.submit(() -> {
        // 读指定的Sheet,用上面定义的监听器处理数据
        EasyExcel.read(filePath, MyDataModel.class , new   MyDataModelListener(myDataService))
              .sheet(finalSheetNo)  // 指定要读取的Sheet编号
              .doRead();  // 开始读取
    });
}
// 所有任务提交完,关闭线程池,不再接受新任务
executor.shutdown();
try {
    // 等待所有线程都执行完,这里设置最长等待时间为无限长,直到所有任务完成
    executor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
} catch (InterruptedException e) {
    e.printStackTrace();
}
4、数据读出来了 怎么存进数据库
攒够一批数据(比如1000条)就批量插入数据库,用MyBatis的话,在Mapper.xml里这么配置:
<!-- 批量插入的方法,参数是list集合 -->
<insert id = "saveBatch" parameterType = "list">
    INSERT INTO your_table (column1, column2, column3)
    VALUES
    <!-- 循环list里的每个item,用逗号分隔每个值组 -->
    <foreach collection = "list" item = "item" separator = ",">
        (#{item.column1}, #{item.column2}, #{item.column3})
    </foreach>
</insert>
这里的your_table是你的表名,column1, column2, column3是表的字段名,item.column1这些对应实体类的属性名,要和你的实际情况对应上。

5、过程中报错了怎么办
数据校验要做好:读数据的时候,每一行都要检查。比如格式对不对、有没有满足业务规则。不对的数据要么记日志跳过,要么存到单独的错误表,后面再处理。
异常捕获不能少:读数据和插数据的时候,可能会报错,比如数据库连不上、插入失败,这时候要捕获异常,保证数据一致:
// 加事务注解,确保批量插入要么全成功,要么全失败,保持数据一致性
@Transactional
public  void  saveBatch(List<MyDataModel> dataList) {
    try {
        // 调用mapper的批量插入方法
        myMapper.saveBatch(dataList);
    } catch (Exception e) {
        // 记日志,方便后面排查问题
        log.error("数据插入失败", e);
        // 抛异常,触发事务回滚,之前插的会被撤销,避免数据乱掉
        throw  new   RuntimeException("数据插入失败", e);
    }
}
按上面的步骤来,百万级数据导入数据库,内存不会崩,速度也能提上来,出了问题也有办法查。你们可以试试,挺好用的。
用户评论