NPOI 導出Sqlite 百萬級數據到Excel 文件

一、前言,最近做項目遇到個奇怪的需求,由於每天的數據量有10k左右,一個月就有30K 左右,一年就數據庫就有兩百多萬條記錄,幾年下來就可能有上千萬條記錄。客戶的需求要求插入和更新的速度不能大於1s,使用SqLITE 實測在千萬級時,操作需要時間長,結果不滿足客戶需求。

       於是,就採用分表儲存數據,每年一個表,是實測後,發現時間符合客戶需求內。這個問題解決了,還有一個問題就是報表問題,他們要求將每年的幾百萬數據備份存儲到Excel 表格中,o(╥﹏╥)o,這個纔是坑。幾百萬的數據庫存儲到表格中,這的需要多長時間啊。後面實測,兩百五十萬,導出至Excel 中,只能100多秒。

二、關於Excel 的HSSFWorkbook 和XSSFWorkbook 和SXSSFWorkbook

1. Excel 2003及以下的版本,其文件後綴是.xls。 HSSFWorkbook一張表最大支持65536行數據,256列,即一張表最多包含6萬多條數據,因此,Excel 2003完全不可能符合百萬數據導出的需求。^_^

2. Excel 2007- 更高的版本,其文件後綴是.xlsx。XSSFWorkbook一張表最大支持1048576行,16384列,即一張表最多包含104萬多條數據,因此符合。但是,雖然這時導出100萬數據能滿足要求,但使用XSSF測試後發現偶爾還是會發生堆溢出,程序總是崩潰,拋出內存不夠的異常,100多萬數據就將內存榨乾了,XSSFWorkbook所以也不適合百萬數據的導出。

3. 後來,經過網上查找,發現在最新的NPOI 和POI中,POI3.8之後新增加了一個類,SXSSFWorkbook,在POI3.8的SXSSF包是XSSF的一個擴展版本。支持流處理,在生成大數據量的電子表格且堆空間有限時使用。SXSSF通過限制內存中可訪問的記錄行數來實現其低內存利用,當達到限定值時,新一行數據的加入會引起老一行的數據刷新到硬盤。當內存中限制行數爲100,當行號到達101時,行號爲0的記錄刷新到硬盤並從內存中刪除,當行號到達102時,行號爲1的記錄刷新到硬盤,並從內存中刪除,以此類推。^_^

4. 在使用SXSSFWorkbook 導出數據過程中,C盤的 AppData\Local\Temp\poifiles (如下圖)下會在產生臨時文件的,而且會產生兩種臨時文件,一種是爲每個sheet頁生成一個 xml 臨時文件,一種是最終導出時生成的完整.xlsx 文件,打開這個目錄一看,發現有幾G臨時文件存在 o(╥﹏╥)o。

5. 在一片關於在Java中使用POI 導出百萬級數據的文章中,提到 《poi實現百萬級數據導出》,臨時文件的刪除可以使用

workbook.write(fileOut);     write()方法中包含刪除 .xlsx 文件的方法,在它的finally代碼塊裏,具體可以去查看源碼

workbook.dispose();          dispose()方法就是用來刪除那些 xml 格式的臨時文件的

6. 後面發現我使用250W 條數據測試,分5個sheet ,每個sheet 50W 條數據,發現在一些低配置的電腦或者筆記本,還是存在內存不足異常。後面查看代碼,發現一開始是這樣子寫的,雖然不是每個表100W數據,但是5個表,加起來還是有250W多數據。

因此,需要在細節上處理一下,《poi實現百萬級數據導出》提到:^_^

 每創建完一個sheet頁就會生成一個xml文件  但是所有的 xml 文件都是空的,只有調用workbook.write(fileOut); 方法時,纔會往xml中寫數據,也就是說之前構造的幾百萬數據都在內存中,這是很危險的行爲,當達到一定量時可能就會有內存溢出的風險,所以要記得在每個sheet頁構造完成之後都手動把數據刷到磁盤當中((SXSSFSheet)sheet).flushRows();

其實write()方法中也是for循環調用的flushRows()方法。

所以,每次寫完50W條數據後,手動調用((SXSSFSheet)sheet).flushRows() 這個方法,將數據刷新到磁盤中,這樣子就可以避免內存不夠用的情況。^_^

實際使用過程如下,導出250W 數據不成問題,經過多次測試,在配置差的筆記本和臺式機都是可以成功導出250W 數據

O(∩_∩)O哈哈~     ^_^

  /// <summary>
        /// 導出數據到Execl中
        /// </summary>
        private void ExportExeclFile()
        {
            lock (mLockObject)
            {
                mIsExport = true;
            }

            bool mIsExit = false;

            WriteLogcat("開始備份 ...", 0);
            ShowMessage(2, string.Format("{0:N3}", 0));

            ISheet sheet = null;                    //工作表對象
            IRow row = null;                       //行對象
            ICell cell = null;                     //列對象

            //備份整個數據庫
            List<String> tabList = null;
            DataTable db = null;

            int totalLines = 0, writeLine = 0;
            int totolRecord = 0, year = 0, singleWriteRecord = 50 * 10000, offset = 0;

            SXSSFWorkbookBean bean = null;
            SXSSFWorkbook workbook = null;

            CardRecordService cardRecordService = CardRecordService.GetInstance();
            CheckCardRecordService checkCardRecordService = CheckCardRecordService.GetInstance();

            //新建表頭   -- 50萬條記錄一張sheet
            try
            {
                //1. 獲取數據庫中,所有表格
                WriteLogcat("正在獲取數據 ...", 0);

                if (mMode == 0)
                    tabList = checkCardRecordService.GetRecordTables();
                else
                    tabList = cardRecordService.GetRecordTables();

                if (tabList == null)
                {
                    WriteLogcat("數據庫爲空 ...", 1);
                    goto Finish;
                }

                lock (mLockObject)
                {
                    if (mIsExport == false)
                    {
                        mIsExit = true;
                        goto Finish;
                    }
                }

                year = mYear;

                //2. 每個表格,分別備份到一個execl.xlsx 文件中  -- 指定年份的表
                if (mMode == 0)
                    WriteLogcat("正在備份 " + year + " 年檢測卡數據...", 2);
                else
                    WriteLogcat("正在備份 " + year + " 年髮卡數據...", 2);

                if (mMode == 0)
                    totolRecord = checkCardRecordService.GetBackupTotalRecords(year);
                else
                    totolRecord = cardRecordService.GetBackupTotalRecords(year);

                totalLines = totolRecord;

                ShowMessage(2, String.Format("{0:N3}", totalLines));

                lock (mLockObject)
                {
                    if (mIsExport == false)
                    {
                        mIsExit = true;
                        goto Finish;
                    }
                }

                //3.開始保存 50萬 一個sheet
                offset = 0;
                mWrittenLines = 0;

                bean = NpoiUtils.CreateSXSSFWorkbook();             //每個Execl文件對應一個對象
                workbook = bean.getSxssfworkBook();

                ICellStyle style = bean.getStyle();
                ICellStyle dataStyle = bean.getDataStyle();
                ICellStyle dataStyle2 = bean.getDataStyle2();

                while (totalLines > 0)
                {
                    sheet = workbook.CreateSheet("記錄" + (offset + 1).ToString());              //建立新的sheet對象

                    WriteLogcat("正在備份 " + year + " 年 sheet " + (offset + 1) + " 數據 ...", 2);

                    //4.創建表頭
                    row = sheet.CreateRow(0);

                    for (int jk = 0; jk < mSheetTitle.Length; jk++)
                    {
                        cell = row.CreateCell(jk);

                        cell.SetCellValue(mSheetTitle[jk]);
                        cell.CellStyle = style;
                        sheet.SetColumnWidth(jk, 30 * 256);
                    }

                    if (totalLines >= singleWriteRecord)
                    {
                        totalLines -= singleWriteRecord;
                        writeLine = singleWriteRecord;
                    }
                    else
                    {
                        writeLine = totalLines;
                        totalLines = 0;
                    }

                    //5.獲取數據
                    if (mMode == 0)
                        db = checkCardRecordService.Report(year, offset, writeLine);
                    else
                        db = cardRecordService.Report(year, offset, writeLine);

                    if (db == null || db.Rows.Count == 0)
                        goto Finish;

                    offset++;

                    //6.寫表格
                    for (int kk = 0; kk < db.Rows.Count; kk++)
                    {
                        row = sheet.CreateRow(kk + 1);

                        for (int lk = 0; lk < mSheetTitle.Length; lk++)
                        {
                            cell = row.CreateCell(lk);

                            if (lk == 0)
                                cell.SetCellValue((kk + 1).ToString());
                            else
                                cell.SetCellValue(db.Rows[kk][lk].ToString());

                            cell.CellStyle = dataStyle2;
                            sheet.SetColumnWidth(lk, 30 * 256);
                        }

                        //防止點擊停止
                        lock (mLockObject)
                        {
                            mWrittenLines++;

                            if (mIsExport == false)
                            {
                                mIsExit = true;
                                goto Finish;
                            }
                        }
                    }

                    //防止點擊停止
                    lock (mLockObject)
                    {
                        if (mIsExport == false)
                            goto Finish;
                    }

                    //在每個sheet頁構造完成之後都手動把數據刷到磁盤當中,否則有可能出現內存不足異常((SXSSFSheet)sheet).flushRows();
                    //其實write()方法中也是for循環調用的flushRows()方法。
                    ((SXSSFSheet)sheet).flushRows();
                }

                //轉爲字節數組  
                MemoryStream stream = new MemoryStream();

                workbook.Write(stream);
                var buf = stream.ToArray();
               
                if (mMode == 0)
                    WriteLogcat("正在寫入 " + mExeclFilePath + "\\" + year + "檢測卡記錄.xlsx ...", 2);
                else
                    WriteLogcat("正在寫入 " + mExeclFilePath + "\\" + year + "髮卡記錄.xlsx ...", 2);

                //保存爲Excel文件  
                if (mMode == 0)
                {
                    using (FileStream fs = new FileStream(mExeclFilePath + "\\" + year + "檢測卡記錄.xlsx", FileMode.Create, FileAccess.Write))
                    {
                        fs.Write(buf, 0, buf.Length);
                        fs.Flush();
                    }
                }
                else
                {
                    using (FileStream fs = new FileStream(mExeclFilePath + "\\" + year + "髮卡記錄.xlsx", FileMode.Create, FileAccess.Write))
                    {
                        fs.Write(buf, 0, buf.Length);
                        fs.Flush();
                    }
                }

                if (mMode == 0)
                    WriteLogcat("寫入 " + mExeclFilePath + "\\" + year + "檢測卡記錄.xlsx 完畢...", 2);
                else
                    WriteLogcat("寫入 " + mExeclFilePath + "\\" + year + "髮卡記錄.xlsx 完畢...", 2);


                //防止點擊停止
                lock (mLockObject)
                {
                    if (mIsExport == false)
                    {
                        mIsExit = true;
                        goto Finish;
                    }
                }

                if (stream != null)
                {
                    stream.Close();
                }

                //關閉所有流
                if (workbook != null)
                {
                    workbook.Close();
                    workbook.Dispose();
                }
            }
            catch (Exception e)
            {
                mIsExit = true;
                LogcatService.WriteLogcat(e);

                ShowMessage(0, e.Message);

                goto Finish;
            }

            //2.清空對應年份的表,然後重新創建表格
            try
            {
                if (mMode == 0)
                {
                    WriteLogcat("開始清空 " + year + " 年檢測卡數據 ....", 2);
                    WriteLogcat("此操作需要時間長,請耐心等待 ^_^ ...", 2);

                    checkCardRecordService.ClearAllRecords(year);
                }
                else
                {
                    WriteLogcat("開始清空 " + year + " 年髮卡數據 ....", 2);
                    WriteLogcat("此操作需要時間長,請耐心等待 ^_^ ...", 2);

                    cardRecordService.ClearAllRecords(year);
                }
            }
            catch (Exception e1)
            {
                mIsExit = true;
                LogcatService.WriteLogcat(e1);

                ShowMessage(0, e1.Message);

                goto Finish;
            }

        Finish:
            {
                //關閉所有流
                if (workbook != null)
                {
                    workbook.Close();
                    workbook.Dispose();            //方法就是用來刪除那些 xml 格式的臨時文件的
                }

                if (mIsExit)
                    WriteLogcat("停止備份 ...", 1);
                else
                {
                    ShowMessage(1, string.Format("{0:N3}", totolRecord));
                    WriteLogcat("備份完成 ...", 0);
                }

                lock (mLockObject)
                {
                    mIsExport = false;
                }
            }

            ProcessCloseExportThread();
        }

 

 

參數大神文章:

poi實現百萬級數據導出

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章