最近的项目中碰到这样一个问题,用户需要导入大数据量的数据,要导入的数据文件大于7M,用户在系统使用的高峰期进行导入,结果导致应用服务器的cpu负载过高,其他用户无法使用系统,系统崩溃的情况。
我们之前没有考虑到这么大数据量的导入,我们使用的是poi3.6版本,用的方式是把用户要导入的文件读成流,然后一次性放到poi的WorkBook中,经过测试,当文件的大小大于5M左右,tomcat服务器的内存就会溢出,从而导致服务器崩溃。
为了解决这个问题,我在网上查了一些资料,解决的方式大概如下(此种方式只适用于excel2007):
1. 采用的读取方式是事件模型eventusermodel,因为excel2007采用的存储方式实际上是xml形式,excel2007可以用系统自带的解压缩软件解压之后可以看到数据都是存储成xml格式的数据,所以这里采用sax的方式对excel文件进行解析。
2. 我在网上下载了2个类文件
抽象类:XxlsAbstract ,作用:遍历excel文件,提供行级操作方法 optRows
继承类:XxlsBig,作用:将数据转出到数据库临时表 temp_table
另外,还需要一个属性文件 db.properties,把数据库的链接写到属性文件中。
3. 另外我自己写了一个多线程的程序,把临时表 temp_table 的数据再写到真正需要导入的数据库表中
4. 写完数据把临时表删除。
程序经过测试,50M左右的文件在本地进行导入都比较稳定。
但是有几个问题不知道怎么办?
1. excel2007文件转换成xml格式的文件之后,可以看到,如果某一行的某个单元格没有内容,xml中就不显示这个单元格,也就是说此种方式不能处理空的单元格。
解决方案:我自己是这么处理的,在导入之前,先把左右的空单元格统一替换成1,然后在java后台在把1替换回来,这样做肯定很影响导入的性能,但是暂时只想到这么个办法。
2. 之前的导入传到service层的是一个文件流,现在XxlsBig类中接收的参数仅仅是一个文件路径,所以如果我的程序发布到应用服务器上,肯定是拿不到要导入的文件的。
总之,还有一些细节的问题弄不明白,需要csdn的兄弟姐妹们给予指导,谢谢啦~~
2011年2月22日补充:
在javaeye上看到了原创作者的补充,解决了问题1,
代码如下:
package com.gaosheng.util.xls; import java.io.InputStream; import java.sql.SQLException; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import org.apache.poi.xssf.eventusermodel.XSSFReader; import org.apache.poi.xssf.model.SharedStringsTable; import org.apache.poi.xssf.usermodel.XSSFRichTextString; import org.apache.poi.openxml4j.opc.OPCPackage; import org.xml.sax.Attributes; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.XMLReader; import org.xml.sax.helpers.DefaultHandler; import org.xml.sax.helpers.XMLReaderFactory; /** * XSSF and SAX (Event API) */ public abstract class XxlsAbstract extends DefaultHandler { private SharedStringsTable sst; private String lastContents; private boolean nextIsString; private int sheetIndex = -1; private List<String> rowlist = new ArrayList<String>(); private int curRow = 0; //当前行 private int curCol = 0; //当前列索引 private int preCol = 0; //上一列列索引 private int titleRow = 0; //标题行,一般情况下为0 private int rowsize = 0; //列数 //excel记录行操作方法,以行索引和行元素列表为参数,对一行元素进行操作,元素为String类型 // public abstract void optRows(int curRow, List<String> rowlist) throws SQLException ; //excel记录行操作方法,以sheet索引,行索引和行元素列表为参数,对sheet的一行元素进行操作,元素为String类型 public abstract void optRows(int sheetIndex,int curRow, List<String> rowlist) throws SQLException; //只遍历一个sheet,其中sheetId为要遍历的sheet索引,从1开始,1-3 public void processOneSheet(String filename,int sheetId) throws Exception { OPCPackage pkg = OPCPackage.open(filename); XSSFReader r = new XSSFReader(pkg); SharedStringsTable sst = r.getSharedStringsTable(); XMLReader parser = fetchSheetParser(sst); // rId2 found by processing the Workbook // 根据 rId# 或 rSheet# 查找sheet InputStream sheet2 = r.getSheet("rId"+sheetId); sheetIndex++; InputSource sheetSource = new InputSource(sheet2); parser.parse(sheetSource); sheet2.close(); } /** * 遍历 excel 文件 */ public void process(String filename) throws Exception { OPCPackage pkg = OPCPackage.open(filename); XSSFReader r = new XSSFReader(pkg); SharedStringsTable sst = r.getSharedStringsTable(); XMLReader parser = fetchSheetParser(sst); Iterator<InputStream> sheets = r.getSheetsData(); while (sheets.hasNext()) { curRow = 0; sheetIndex++; InputStream sheet = sheets.next(); InputSource sheetSource = new InputSource(sheet); parser.parse(sheetSource); sheet.close(); } } public XMLReader fetchSheetParser(SharedStringsTable sst) throws SAXException { XMLReader parser = XMLReaderFactory .createXMLReader("org.apache.xerces.parsers.SAXParser"); this.sst = sst; parser.setContentHandler(this); return parser; } public void startElement(String uri, String localName, String name, Attributes attributes) throws SAXException { // c => 单元格 if (name.equals("c")) { // 如果下一个元素是 SST 的索引,则将nextIsString标记为true String cellType = attributes.getValue("t"); String rowStr = attributes.getValue("r"); curCol = this.getRowIndex(rowStr); if (cellType != null && cellType.equals("s")) { nextIsString = true; } else { nextIsString = false; } } // 置空 lastContents = ""; } public void endElement(String uri, String localName, String name) throws SAXException { // 根据SST的索引值的到单元格的真正要存储的字符串 // 这时characters()方法可能会被调用多次 if (nextIsString) { try { int idx = Integer.parseInt(lastContents); lastContents = new XSSFRichTextString(sst.getEntryAt(idx)) .toString(); } catch (Exception e) { } } // v => 单元格的值,如果单元格是字符串则v标签的值为该字符串在SST中的索引 // 将单元格内容加入rowlist中,在这之前先去掉字符串前后的空白符 if (name.equals("v")) { String value = lastContents.trim(); value = value.equals("")?" ":value; int cols = curCol-preCol; if (cols>1){ for (int i = 0;i < cols-1;i++){ rowlist.add(preCol,""); } } preCol = curCol; rowlist.add(curCol-1, value); }else { //如果标签名称为 row ,这说明已到行尾,调用 optRows() 方法 if (name.equals("row")) { int tmpCols = rowlist.size(); if(curRow>this.titleRow && tmpCols<this.rowsize){ for (int i = 0;i < this.rowsize-tmpCols;i++){ rowlist.add(rowlist.size(), ""); } } try { optRows(sheetIndex,curRow,rowlist); } catch (SQLException e) { e.printStackTrace(); } if(curRow==this.titleRow){ this.rowsize = rowlist.size(); } rowlist.clear(); curRow++; curCol = 0; preCol = 0; } } } public void characters(char[] ch, int start, int length) throws SAXException { //得到单元格内容的值 lastContents += new String(ch, start, length); } //得到列索引,每一列c元素的r属性构成为字母加数字的形式,字母组合为列索引,数字组合为行索引, //如AB45,表示为第(A-A+1)*26+(B-A+1)*26列,45行 public int getRowIndex(String rowStr){ rowStr = rowStr.replaceAll("[^A-Z]", ""); byte[] rowAbc = rowStr.getBytes(); int len = rowAbc.length; float num = 0; for (int i=0;i<len;i++){ num += (rowAbc[i]-'A'+1)*Math.pow(26,len-i-1 ); } return (int) num; } public int getTitleRow() { return titleRow; } public void setTitleRow(int titleRow) { this.titleRow = titleRow; } }
问题2有如下解决方案:
我们项目中用到的导入采用了夜间模式,就是普通用户进行导入操作的时候,系统仅仅是把文件上传到文件服务器,并且在数据库表把该文件的路径,上传作者写到特定的数据库表。
我们采用了spring的定时任务,在夜间2点开始扫描指定的数据库表,如果数据库表中存在记录,即有需要进行导入的数据文件,那么这个时候才启动,进行导入,这个时候由于文件是在文件服务器,所以只要指定一个路径,就可以进行文件的读取了。