天天看點

POI架構EXCEL解析性能優化背景思考Action其他

背景

在做商品EXCEL的時候,線上發現了Full GC,排查得知是商家搞了一個巨大的excel,單商品釋出接口平均耗時400ms(調用sell耗時200ms左右,系統自身處理商品同步耗時150ms左右),對于3000個商品的釋出,耗時在20min左右,這20min内該excel的記憶體一直未能釋放。

POI架構EXCEL解析性能優化背景思考Action其他

第一時間想到的是POI真坑,真吃記憶體。 事情發生了就想着怎麼處理,

  1. 止血 線上機器分批重新開機,
  2. 馬上加一個excel行數的限制然後釋出 線上半個小時左右就沒有任何問題了。

思考

為什麼poi這麼吃記憶體,poi這麼老了,肯定有人踩過這個坑,撸起袖子,搜poi full gc. 很多文檔将的都太粗糙了,本質沒有說透

原因

  1. excel本質上是xml檔案的集合體。從office 2007起開始使用xml來存檔和資料交換: https://zh.wikipedia.org/wiki/Office_Open_XML
  2. poi預設是使用dom方式解析excel,是以檔案中String的數量越多,其dom樹越大。

解法

由于excel商品釋出不需要動态的更改excel中的資料,是以并不強依賴dom解析,直接換成sax來解析excel就行

Action

poi中sax用法

/**
 * @author zhengqiang.zq
 * @date 2018/05/04 ,參考連結:https://poi.apache.org/spreadsheet/how-to.html#sxssf
 */
public class MyEventUserModel {
    public static ThreadLocal<List<ParsedRow>> local = new ThreadLocal<>();

    public void processOneSheet(String filename) throws Exception {
        OPCPackage pkg = OPCPackage.open(filename);
        XSSFReader r = new XSSFReader(pkg);
        SharedStringsTable sst = r.getSharedStringsTable();

        XMLReader parser = fetchSheetParser(sst);
        //從workbook.xml.res 中擷取所有需要解析的xml檔案,rid1 就是第一個sheet,其target就是該sheet所在的相對路徑
        //<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
        //<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
        // <Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme" Target="theme/theme1.xml"/>
        // <Relationship Id="rId4" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>
        // <Relationship Id="rId5" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings" Target="sharedStrings.xml"/>
        // <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/>
        // <Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet2.xml"/>
        //</Relationships>
        //
        InputStream sheet2 = r.getSheet("rId1");
        InputSource sheetSource = new InputSource(sheet2);
        parser.parse(sheetSource);
        sheet2.close();
    }

    public XMLReader fetchSheetParser(SharedStringsTable sst) throws SAXException {
        XMLReader parser =
            XMLReaderFactory.createXMLReader(
                "org.apache.xerces.parsers.SAXParser"
            );
        ContentHandler handler = new SheetHandler(sst);
        parser.setContentHandler(handler);
        return parser;
    }

    /**
     * See org.xml.sax.helpers.DefaultHandler javadocs
     */
    private class SheetHandler extends DefaultHandler {

        /**
         * excel 常量資料對象,對應的就是sharedStrings.xml檔案中的内容,類似excel中的常量池
         */
        private SharedStringsTable sst;
        /**
         * 目前處理的文本值
         */

        private String lastContents;
        /**
         * 下一個文本是不是String類型
         */
        private boolean nextIsString;
        /**
         * 目前單元格的索引值,從0開始,0:第一列
         */
        private Short index;
        /**
         * 自定義資料類型,存儲被解析的每一行原始資料
         */
        List<ParsedRow> sheetData = Lists.newArrayList();
        ParsedRow currentRow = new ParsedRow();

        private SheetHandler(SharedStringsTable sst) {
            this.sst = sst;
        }

        @Override
        public void startElement(String uri, String localName, String name, Attributes attributes) throws SAXException {
            //第一行
            if (name.equals("row")) {
                currentRow.setRowNum(new Long(attributes.getValue("r")));
                sheetData.add(currentRow);
            }
            //c => cell 一個單元格,
            if (name.equals("c")) {
                //r屬性表示單元格位置,例如A2,C3
                String coordinate = attributes.getValue("r");
                CellReference cellReference = new CellReference(coordinate);
                //根據r屬性擷取其列下标,從0開始
                index = cellReference.getCol();

                //t:屬性代表單元格類型
                String cellType = attributes.getValue("t");
                if (cellType != null && cellType.equals("s")) {
                    //t="s"表示是改單元格是字元串,那麼該單元格的實際值值需要去SharedStringsTable中取
                    nextIsString = true;
                } else {
                    nextIsString = false;
                }
            }
            // Clear contents cache
            lastContents = "";
        }

        @Override
        public void endElement(String uri, String localName, String name) throws SAXException {
            if (nextIsString) {
                int idx = Integer.parseInt(lastContents);
                //從SharedStringsTable中取目前單元格的實際值
                lastContents = new XSSFRichTextString(sst.getEntryAt(idx)).toString();
                nextIsString = false;
            }

            // v => contents of a cell
            // Output after we've seen the string contents
            if (name.equals("v")) {
                //不管是不是數字還是文本值
                currentRow.getData().put(index, lastContents);
            }
            if (name.equals("row")) {
                currentRow = new ParsedRow();
            }
        }

        @Override
        public void endDocument() throws SAXException {
            local.set(sheetData);
        }

        /**
         * 通知一個元素中的字元,是否處理由自己決定,比如  <v>1</v>,
         *
         * @param ch     The characters. 整個sheet.xml的char[]數組表示
         * @param start  The start position in the character array. 本次處理的元素值的的開始位置
         * @param length The number of characters to use from the ,元素長度
         *               character array.
         * @throws SAXException Any SAX exception, possibly
         * wrapping another exception.
         * @see ContentHandler#characters
         */
        @Override
        public void characters(char[] ch, int start, int length) throws SAXException {
            //對于lastContents是String類型來說,lastContent存放的是其在SharedStringsTable中的索引,
            // 對于是數字類型來說,lastContents存放就是該數字的字元串表示
            lastContents += new String(ch, start, length);
        }
    }

    public static void main(String[] args) throws Exception {
        String fileName = "/Users/thinerzq/alltest/excel/test_big_3300_diffrent_row.xlsx";
        MyEventUserModel example = new MyEventUserModel();
        Stopwatch stopwatch = new Stopwatch();

        stopwatch.start();
        example.processOneSheet(fileName);
        System.out.println("-----------------finish, " + stopwatch.toString());

        System.out.println(local.get());
        Thread.sleep(100000 * 1000);
    }
}           

性能對比

dom 3455行

解析時間

-----------------finish, 6.987 s

記憶體消耗

jmap -histo:live 2646
thinerzq@thinerzq-2:~$ jmap -histo:live 2646

 num     #instances         #bytes  class name
----------------------------------------------
   1:       2574454      247147584  org.apache.xmlbeans.impl.store.Xobj$AttrXobj
   2:       1332126      127884096  org.apache.xmlbeans.impl.store.Xobj$ElementXobj
   3:       1265264       50610560  java.util.TreeMap$Entry
   4:        778421       48667664  [C
   5:           636       37006672  [B
   6:        611910       29371680  java.util.TreeMap
   7:        611886       24475440  org.apache.xmlbeans.impl.values.XmlUnsignedIntImpl
   8:        653334       20906688  org.apache.poi.xssf.usermodel.XSSFCell
   9:        653334       20906688  org.openxmlformats.schemas.spreadsheetml.x2006.main.impl.STCellRefImpl
  10:        775269       18606456  java.lang.String
  11:        653334       15680016  org.openxmlformats.schemas.spreadsheetml.x2006.main.impl.CTCellImpl
  12:        611866       14684784  org.apache.poi.xssf.usermodel.XSSFRow
  13:        611866       14684784  org.openxmlformats.schemas.spreadsheetml.x2006.main.impl.CTRowImpl
  14:        622210        9955360  java.lang.Integer
  15:         55239        1767648  org.openxmlformats.schemas.spreadsheetml.x2006.main.impl.STXstringImpl
  16:         34552        1105664  org.openxmlformats.schemas.spreadsheetml.x2006.main.impl.STCellTypeImpl
  17:         18776         600832  java.util.HashMap$Node
  18:         10328         247872  org.openxmlformats.schemas.spreadsheetml.x2006.main.impl.CTRstImpl
….
 726:             1             16  sun.util.resources.LocaleData$LocaleDataResourceBundleControl
Total      11909576      686173768=81.8MB           

sax 3455行

-----------------finish, 2.427 s
thinerzq@thinerzq-2:~$ jmap -histo:live 2711

 num     #instances         #bytes  class name
----------------------------------------------
   1:        612060       29378880  java.util.HashMap
   2:        611866       14684784  com.zq.poi.ParsedRow
   3:        611866       14684784  java.lang.Long
   4:          1298        3342120  [Ljava.lang.Object;
   5:         60140        3149488  [C
   6:         51946        1662272  java.util.HashMap$Node
   7:         60096        1442304  java.lang.String
   8:          3610         578024  [Ljava.util.HashMap$Node;
   9:           617         288960  [B
  10:          1626         186544  java.lang.Class
  11:           975         173176  [I
  12:          2911         116440  java.util.LinkedHashMap$Entry
  13:          2648          63552  javax.xml.namespace.QName
  14:          1811          57952  java.util.concurrent.ConcurrentHashMap$Node
  15:          2242          53808  org.apache.xmlbeans.SchemaType$Ref
  16:           423          27072  java.net.URL
  17:          1619          25904  java.lang.Object
  18:           290          20496  [Ljava.lang.String;
 531:             1             16  sun.util.resources.LocaleData$LocaleDataResourceBundleControl
Total       2036715       70309432 =8.4MB           

總覽

解析類型 資料量 記憶體占用
dom 3455行不同資料 6.587 s 81.8MB
sax 2.427 s 8.4MB
10000行資料,2/3重複 6.748s 100.4M
2.827s 9.4MB

可以看到使用sax解析之後記憶體下降了近10倍之多,再也不用擔心full gc了。由于其常量池的緣故,excel檔案大小和行數是否有重複的單元格有關系。

其他

參考連結

  1. poi文檔

檢視excel的xml檔案

将excel檔案的字尾名改為.zip 然後解壓縮裡面就全部都是xml檔案了。

xml檔案快速一覽

POI架構EXCEL解析性能優化背景思考Action其他
POI架構EXCEL解析性能優化背景思考Action其他