NPOI導出大量數據的避免OOM解決方案【SXSSFWorkbook】


    一、NPOI的基本知識

      碰到了導出大量數據的需求場景:從數據讀取數據大約50W,然后再前端導出給用戶,整個過程希望能較快的完成。如果不能較快完成,可以給與友好的提示。

      大量數據的導出耗時的主要地方:

      1、從數據庫獲取大量數據。如果一般百萬級別左右的,走索引的查詢,一般5秒左右可以把數據查出來。

      2、把查出來的數據,通過NPOI組裝成excel。這個過程一般耗時,且消耗資源,很容易出現OOM。

      了解一下NPOI基本知識,因為NPOI是從JAVA的POI的.NET版本,所以可以看看POI的信息也是類的

                  1、基於事件模式的操作(eventmodel)  

                  2、基於用戶態模式的操作(usermodel)

                  3、基於SXSSF的操作(SXSSF)

 

             看圖中也可以知道,他們三者的優缺點:

             綜合來看: eventmodel和SXSSF,CPU和內存的利用效率都是good,usermodel比較差;從特性的支持上來看,usermodel一騎絕塵,其次是SXSSF。

             從圖中來看,eventmodel肯定是最復雜的,再過來SXSSF,usermodel最簡單。

             這篇文章主要看研究的是導出:我們主要看看usermodel和SXSSF,eventmodel不支持寫,也就沒有導出這一說法了。  

    二、基於usermodel的XSSFWorkbook進行導出

      1、因為是大數量毫不疑問的只能選擇XSSFWorkbook,導出xlsx格式(07版及以上)

           這里有一個坑點就是: wb.Write(ms);寫完之后會關閉,所以一般擴展一個自定義流來代替內存流來實現。

        public static byte[] Export<T>(List<T> list, string filename)
        {
            IWorkbook wb = new XSSFWorkbook();
            ISheet sheet = wb.CreateSheet(filename);
            SetColumnTitle<T>(sheet, list[0]);
            int index = 1;
            foreach (var model in list)
            {
                Type type = model.GetType();
                var properties = type.GetProperties();
                IRow row = sheet.CreateRow(index++);
                int j = 0;
                for (int i = 0; i < properties.Length; i++)
                {
                    var p = properties[i];
                    object[] objs = p.GetCustomAttributes(typeof(ExcelAttribute), true);
                    if (objs.Length > 0)
                    {
                        object obj = p.GetValue(model, null);
                        if (obj != null)
                        {
                            row.CreateCell(j).SetCellValue(obj.ToString());
                        }
                        else
                        {
                            row.CreateCell(j).SetCellValue("");
                        }
                        j++;
                    }
                }
            }
            byte[] buffer;
            using (NpoiMemoryStream ms = new NpoiMemoryStream())
            {
                ms.AllowClose = false;
                wb.Write(ms);
                ms.Flush();
                buffer = new byte[ms.Length];
                ms.Position = 0;
                ms.Read(buffer, 0, buffer.Length);
                ms.AllowClose = true;     
            }
            wb.Close();

            //強制清空占用內存,因為NPOI占用的內存,不建議手動GC。
            //GcCollectHelper.ClearMemory();

            return buffer;
        }

          並且導出完,占用的內存還一直沒釋放【這是和.NET 的GC機制有關,不是說使用完內存會馬上釋放,如果這樣內存的利用率就100%了】。

          用上面這種方法導出數據,很有可能導致OOM,因為在你點導出的時候,占用的內存和CPU都很高。

          通過Windbg分析Dump得到下面:

                 1、通過!dumpheap -stat 查看clr的托管堆中的各個類型的占用情況。

                   發現有下面這些對象占用了很大的內存,逐個分析,看看是我們代碼的鍋,還是NPOI的鍋

                 2、!DumpHeap /d -mt 00007fff4fff0140     //查看當前方法表               

                 3、!DumpObj /d 0000024587be7c40    //查看當前地址對應的內容

                 發現這些都不是我們自己的程序代碼生成的。因為NPOI生成Excel的大概原理:通過把數據加上你所設置的Excel的workbook,行,列,以及樣式等等生成一個Excel,一次性的把所有數據都加載到內存中。

                 Office Open XML(縮寫:Open XML、OpenXML或OOXML),為由Microsoft開發的一種以XML為基礎並以ZIP格式壓縮的電子文件規范,支持文件表格備忘錄幻燈片等文件格式。

                 本來想基於這些對象,看看能否進行垃圾回收。

                 1、本來想用強制的垃圾回收,但是生產環境一般不敢這么用,因為GC啥時候進行回收,是有它自己的機制的,我們沒必要去打亂。

                 2、那所以說不能強制垃圾回收,能不能加速垃圾回收呢,網上有很多方法說是把對象置位NULL就可以加速垃圾回收。貌似也沒效果,具體原因還在分析中。

     三、基於SXSSFWorkbook導出

       他們提供了一個流式的SXSSFWorkbook版本,這種允許寫入非常大的文件而不耗盡內存,因為在任何時候,只有可配置的行部分被保存在內存中,並且還可以自己定義導出的數據的模板。

      用SXSSFWorkbook 就不要做太多的'Excel式'的操作,比如刪除行,移動行等等,看最開始的那張圖即可。

      SXSSFWorkbook大致原理:借助臨時存儲空間生成Excel。

      如下所示:這個1000的意思是:內存中只放1000行記錄,如果超過1000行,就把數據寫到磁盤中去(以臨時文件的方式存儲,不需要我們去管,這個SXSSFWorkbook導出),這樣就避免內存溢出了。但是這樣可能會讓生成Excel的時間變長了,因為會涉及多次的IO操作。

IWorkbook wb = new SXSSFWorkbook(1000);

        方法一、直接用SXSSFWorkbook(1000)  ---從數據庫查詢所有數據出來,放到內存后

               測試結果:CPU和內存相比usermodel要好很多,時間稍微的優點延長。                 

        方法二、使用帶分頁SXSSFWorkbook的方式----從數據庫按分頁查詢數據,生成臨時文件,刷盤,最后生成完整的Excel。

 
        public static byte[] ExportStreamAsPage<T>(string filename, int pageSize,Func<int,int,List<T>> action) where T : new()
        {
            IWorkbook wb = new SXSSFWorkbook(pageSize);
            ISheet sheet = wb.CreateSheet(filename);

            ItsmProvider itsmProvider = new ItsmProvider();

            //設置標題
            SetColumnTitle<T>(sheet, new T());

            var type = typeof(T);
            int pageIndex = 1;
            Boolean hasNext = true;

            //記錄循環次數
            while (hasNext)
            {
                var datas = action.Invoke(pageIndex,pageSize);
                SetRowContent<T>(type, sheet, pageIndex, pageSize, datas);

                //不包含任何數據的時候,就退出
                if (!datas.Any())
                {
                    break;
                }
                //說明已經到了最后一頁。
                if (datas.Count() < pageSize)
                {
                    hasNext = false;
                }
                pageIndex++;
            }
            byte[] buffer;
            using (NpoiMemoryStream ms = new NpoiMemoryStream())
            {
                ms.AllowClose = false;
                wb.Write(ms);
                ms.Flush();
                buffer = new byte[ms.Length];
                ms.Position = 0;
                ms.Read(buffer, 0, buffer.Length);
                ms.AllowClose = true;
            }
            wb.Close();
            return buffer;

        }


        public static void SetRowContent<T>(Type type, ISheet sheet, int pageIndex, int pageSize, List<T> list)
        {
            int start = (pageIndex - 1) * pageSize + 1;
            foreach (var model in list)
            {
                var properties = type.GetProperties();

                IRow row = sheet.CreateRow(start);
                int j = 0;
                for (int i = 0; i < properties.Length; i++)
                {
                    var p = properties[i];
                    object obj = p.GetValue(model, null);
                    if (obj != null)
                    {
                        row.CreateCell(i).SetCellValue(obj.ToString());
                    }
                }
                start++;
            }
        }

        上面這個方法,更加的省內存,不過時間上確實慢了,用時間換空間的一種做法。

        方法三、使用帶分頁SXSSFWorkbook的方式--多線程導出多sheet

         本來的想法是想着,一個sheet開多個線程來繪制excel的,但是這樣實現不了,因為Sheet不是線程安全的,我強行給他加鎖變成線程安全,這樣就失去了多線程意義了。我們的業務場景確實用多個sheet來輸出大量數據也能接受,所以最后就采用了這種方案。

   public static byte[] ExportStreamByMultiSheet<T>(string filename,int recordCount, int pageSize, Func<int, int, List<T>> action) where T : new()
        {
            IWorkbook wb = new SXSSFWorkbook(pageSize);
            var type = typeof(T);

            //開啟多線程(開啟固定線程)方法
       var excelTasks=new List<Task>(); int fixThreadCount = 10; //開啟10個固定數量 for(int i=1;i< (recordCount / pageSize)+1; i++) { ISheet sheet = wb.CreateSheet(filename+i); SetColumnTitle<T>(sheet, new T()); excelTasks.Add( Task.Factory.StartNew(()=> SetExcel(pageSize, action, sheet, type, i) ) ); if (excelTasks.Count >= fixThreadCount) { Task.WaitAny(excelTasks.ToArray()); //等待任何一個完成 excelTasks = excelTasks.Where(d => d.Status != TaskStatus.RanToCompletion).ToList(); } }
            Task.WaitAll(excelTasks.ToArray());
            byte[] buffer;
            using (NpoiMemoryStream ms = new NpoiMemoryStream())
            {
                ms.AllowClose = false;
                wb.Write(ms);
                ms.Flush();
                buffer = new byte[ms.Length];
                ms.Position = 0;
                ms.Read(buffer, 0, buffer.Length);
                ms.AllowClose = true;
            }
            wb.Close();
            return buffer;

        } 

四、總結

        大數據量導出防止OOM方法就是:SXSSFWorkbook,最理想的還是把這種大量數據導出做成獨立的服務,部署到單獨的機器上進行導出。

五、測試程序

       1、用最新版的.NET 6 

       2、github地址: https://github.com/gdoujkzz/ExcelWebDemo.git

       3、主要用到技術點:

                    前端:

                         在wwwroot文件夾下,用vscode打開,安裝是Live-server插件,即可鼠標右鍵 Open With Live-Server;

                         技術點:Vue+ElementUI+Axios;

                    后端:

                          在.NET 6跑,因為沒有solution文件,大家VS添加現有項目即可跑起來。

                          技術點:用委托封裝了導出分頁操作,分頁的具體操作由前端傳過來。多線程:開啟固定現場數量,進行導出。 

                          Nuget包:Sqlsugar,NPOI,以及Mysql.Data

                   數據庫:

                           內附Mysql版本的數據文件,大家造點數據即可。

                  其他:

                           windbg的基本使用【從一線碼農大佬那偷師,謝謝大佬的分享】

    

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM