Excel 海量數(shù)據(jù)導(dǎo)入數(shù)據(jù)庫實(shí)操技巧
前言
在當(dāng)今數(shù)據(jù)驅(qū)動的時(shí)代,處理大規(guī)模數(shù)據(jù)是眾多企業(yè)和項(xiàng)目面臨的常見挑戰(zhàn)。其中,將百萬級數(shù)據(jù)從Excel導(dǎo)入到數(shù)據(jù)庫是一個(gè)典型的場景,它涉及到高效的數(shù)據(jù)讀取、優(yōu)化的數(shù)據(jù)庫寫入以及一系列性能和錯(cuò)誤處理問題。
面臨的挑戰(zhàn)
內(nèi)存溢出
當(dāng)處理百萬級數(shù)據(jù)時(shí),直接將整個(gè)Excel文件加載到內(nèi)存中進(jìn)行處理是不現(xiàn)實(shí)的,這極有可能導(dǎo)致內(nèi)存溢出錯(cuò)誤。傳統(tǒng)的Excel處理方式,如Apache POI的某些實(shí)現(xiàn),在面對大數(shù)據(jù)量時(shí),由于需要將大量數(shù)據(jù)存儲在內(nèi)存中,容易出現(xiàn)內(nèi)存不足的情況。
性能瓶頸
百萬級數(shù)據(jù)的讀取和插入操作會非常耗時(shí)。單線程處理如此龐大的數(shù)據(jù)量,效率極其低下。從Excel讀取數(shù)據(jù)的速度、數(shù)據(jù)在內(nèi)存中的處理速度以及向數(shù)據(jù)庫插入數(shù)據(jù)的速度,都可能成為性能瓶頸。此外,頻繁的數(shù)據(jù)庫交互,如逐條插入數(shù)據(jù),會大大增加數(shù)據(jù)庫的負(fù)載,進(jìn)一步降低整體性能。
異常處理
在數(shù)據(jù)讀取和導(dǎo)入過程中,可能會遇到各種異常情況。例如,Excel數(shù)據(jù)格式錯(cuò)誤、數(shù)據(jù)不一致、數(shù)據(jù)庫連接異常、數(shù)據(jù)重復(fù)等問題。如何妥善處理這些異常,保證數(shù)據(jù)的完整性和導(dǎo)入過程的穩(wěn)定性,是實(shí)現(xiàn)過程中需要重點(diǎn)考慮的問題。
技術(shù)選型
EasyExcel
EasyExcel采用了流式讀取技術(shù),它不會將整個(gè)Excel文件一次性加載到內(nèi)存中,而是按行從磁盤逐個(gè)讀取數(shù)據(jù)并解析。這種方式大大減少了內(nèi)存的占用,使得處理百萬級數(shù)據(jù)成為可能。例如,在解析一個(gè)包含百萬行數(shù)據(jù)的Excel文件時(shí),傳統(tǒng)的POI方式可能會因?yàn)閮?nèi)存不足而失敗,而EasyExcel可以高效地逐行處理數(shù)據(jù),避免內(nèi)存問題。
多線程處理
為了提升性能,引入多線程技術(shù)是關(guān)鍵。多線程可以應(yīng)用在兩個(gè)主要場景:多線程讀取Excel文件和多線程實(shí)現(xiàn)數(shù)據(jù)插入。通過生產(chǎn)者 - 消費(fèi)者模式,多個(gè)線程同時(shí)讀取Excel數(shù)據(jù),并將讀取到的數(shù)據(jù)交給多個(gè)線程進(jìn)行插入操作,從而提高整體的數(shù)據(jù)處理速度。例如,我們可以創(chuàng)建一個(gè)固定大小的線程池,每個(gè)線程負(fù)責(zé)讀取Excel文件中的一個(gè)Sheet頁數(shù)據(jù),或者負(fù)責(zé)將一批數(shù)據(jù)插入到數(shù)據(jù)庫中。
數(shù)據(jù)庫批量插入
在數(shù)據(jù)插入方面,利用數(shù)據(jù)庫的批量插入功能可以顯著提升速度。相比于逐條插入數(shù)據(jù),批量插入可以減少數(shù)據(jù)庫交互次數(shù),降低數(shù)據(jù)庫負(fù)載。不同的數(shù)據(jù)庫有不同的批量插入方式,例如在 MySQL 中,可以使用INSERT INTO ... VALUES (...) , (...)這種語法形式進(jìn)行批量插入;在Oracle中,可以使用INSERT ALL語法。同時(shí),結(jié)合MyBatis等持久層框架,也可以方便地實(shí)現(xiàn)批量插入操作。
實(shí)現(xiàn)步驟
數(shù)據(jù)讀取
通過配置ReadListener,實(shí)現(xiàn)對Excel數(shù)據(jù)的逐行讀取。在ReadListener的invoke方法中,對每一行數(shù)據(jù)進(jìn)行處理。例如:
public class MyDataModelListener extends AnalysisEventListener<MyDataModel> {
    private static final int 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 {
            // 處理無效數(shù)據(jù),例如記錄日志或跳過
        }
        if (batch.size() >= BATCH_SIZE) {
            processBatch();
        }
    }
    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
        if (!batch.isEmpty()) {
            processBatch();
        }
    }
    private void processBatch() {
        myDataService.saveBatch(batch);
        batch.clear();
    }
    private boolean validateData(MyDataModel data) {
        // 實(shí)現(xiàn)數(shù)據(jù)校驗(yàn)邏輯
    }
}如果Excel文件包含多個(gè)Sheet頁,可以通過線程池并發(fā)讀取各個(gè)Sheet。以下是實(shí)現(xiàn)并發(fā)讀取多個(gè)Sheet的示例代碼:
String filePath = "/users/yian/workspace/excel/test.xlsx";
// 需要讀取的sheet數(shù)量
int numberOfSheets = 20;
// 創(chuàng)建一個(gè)固定大小的線程池,大小與sheet數(shù)量相同
ExecutorService executor = Executors.newFixedThreadPool(numberOfSheets);
for (int sheetNo = 0; sheetNo < numberOfSheets; sheetNo++) {
    int finalSheetNo = sheetNo;
    executor.submit(() -> {
        EasyExcel.read(filePath, MyDataModel.class, new MyDataModelListener(myDataService))
              .sheet(finalSheetNo)
              .doRead();
    });
}
executor.shutdown();
try {
    executor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
} catch (InterruptedException e) {
    e.printStackTrace();
}數(shù)據(jù)插入
在數(shù)據(jù)讀取過程中,當(dāng)收集到一定數(shù)量的數(shù)據(jù)(例如1000條)后,將這些數(shù)據(jù)批量插入到數(shù)據(jù)庫中??梢允褂?/span>MyBatis的批量插入功能,通過在Mapper.xml文件中配置批量插入語句來實(shí)現(xiàn):
<insert id="saveBatch" parameterType="list">
    INSERT INTO your_table (column1, column2, column3)
    VALUES
    <foreach collection="list" item="item" separator=",">
        (#{item.column1}, #{item.column2}, #{item.column3})
    </foreach>
</insert>錯(cuò)誤處理
- 數(shù)據(jù)校驗(yàn):在數(shù)據(jù)讀取階段,對每一行數(shù)據(jù)進(jìn)行格式校驗(yàn)和業(yè)務(wù)邏輯校驗(yàn)。例如,檢查數(shù)據(jù)是否符合特定的格式要求、是否滿足業(yè)務(wù)規(guī)則等。如果數(shù)據(jù)校驗(yàn)失敗,記錄錯(cuò)誤日志并跳過該數(shù)據(jù),或者將錯(cuò)誤數(shù)據(jù)存儲到一個(gè)單獨(dú)的表中,以便后續(xù)處理。
 - 異常捕獲與處理:在數(shù)據(jù)讀取和插入過程中,捕獲可能出現(xiàn)的異常,如數(shù)據(jù)庫連接異常、插入失敗等。對于數(shù)據(jù)庫插入異常,可以采用事務(wù)回滾的方式保證數(shù)據(jù)的一致性。同時(shí),記錄詳細(xì)的異常信息,以便排查問題。例如:
 
@Transactional
public void saveBatch(List<MyDataModel> dataList) {
    try {
        // 執(zhí)行批量插入操作
        myMapper.saveBatch(dataList);
    } catch (Exception e) {
        // 記錄異常日志
        log.error("數(shù)據(jù)插入失敗", e);
        // 回滾事務(wù)
        throw new RuntimeException("數(shù)據(jù)插入失敗", e);
    }
}














 
 
 
 
 
 
 