一、前言,最近做項目遇到個奇怪的需求,由於每天的數據量有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();
}
參數大神文章: