场景
X系统需要从数据库读取大量数据存放到List集合中(可能还会做逻辑上的处理),并生成一个Excle文件,下载到客户本地。
问题一:客户体验
如果导出的文件比较大,比如几十万条数据,同步导出页面就会卡主,用户无法进行其他操作。
问题二:服务性能
导出的时候,任务比较耗时就会阻塞主线程。如果导出的服务是暴露给外部(前后端分离),这种大量的数据传输十分消耗性能。
解决方案:使用异常处理导出请求,后台 MQ 通知自己进行处理。MQ 消费之后,多线程处理 excel 文件导出,生成文件后上传到 FTP 等文件服务器。前端直接查询并且展现对应的任务执行列表,去 FTP 等文件服务器下载文件即可。现在准备金系统的数据量在五万条以下,采用的方式是直接查询数据并生成导出Excel文件。这种方式在以后数据量增大以后是有很大问题的。具体问题如下:
问题三:FULL GC
如果一次查询 100W 条数据库,然后把这些信息全部加载到内存中,是不可取的。
建议有2个:
限制每一次分页的数量。比如一次最多查询 1w 条。分成 100 次查询。(必须)
限制查询得总条数。比如限制为最多 10W 条。(根据实际情况选择)
尽量避免 FULL-GC 的情况发生,因为目前的所有方式对于 excel 的输出流都会占用内存,100W 条很容易导致 FULL-GC。
问题四:数据库的压力
去数据库读取的时候一定要记得分页,免得给数据库太大的压力。
一次读取太多,也会导致内存直线上升。比如 100W 条数据,则分成 100 次去数据库读取。
问题五:网络传输
传统的 excel 导出,都是前端一个请求,直接 HTTP 同步返回。导出 100W 条,就在那里傻等。这客户体验不友好,而且网络传输,系统占用多种问题。
建议使用异步处理的方式,将文件上传到文件服务器。前端直接去文件服务器读取。
poi创建Excel文件的三种方式:
1. HSSFWorkbook(excel 2003)
HSSFWorkbook 针对是 EXCEL2003 版本,扩展名为 .xls;所以 此种的局限就是导出的行数 至多为 65535 行,此种 因为行数不够多所以一般不会发生OOM。
2. XSSFWorkbook (excel 2007)
这种形式的出现 是由于 第一种HSSFWorkbook 的局限性而产生的,因为其所导出的行数比较少,所以 XSSFWookbook应运而生 其 对应的是EXCEL2007+(1048576行,16384列)扩展名 .xlsx,最多可以 导出 104 万行,不过 这样 就伴随着一个问题---OOM 内存溢出,原因是你所创建的 book sheet row cell 等此时是存在内存的并没有 持久化,那么随着 数据量增大内存的需求量也就增大,那么很大可能就是要 OOM了。
3. SXSSFWorkbook(excel 2007后,poi使用3.8+版本)
因为数据量过大导致内存吃不消那么可以让内存到量持久化 吗?答案是肯定的,此种的情况 就是 设置 最大内存条数比如设置最大内存量为5000 rows --new SXSSFWookbook(5000),此时当行数 达到 5000 时,把 内存 持久化 写到 文件中,以此 逐步 写入 避免OOM,那么这样 就完美解决了大数据下导出的问题。
我们知道excel2007及以上版本可以轻松实现存储百万级别的数据,但是系统中的大量数据是如何能够快速准确的导入到excel中这好像是个难题,对于一般的web系统,我们为了解决成本,基本都是使用的入门级web服务器tomcat,既然我们不推荐调整JVM的大小(注:调整JVM的配置参数也不是一个好对策,jdk在32位系统中支持的内存不能超过2个G,而在64位中没有限制,但是在64位的系统中,性能并不是太好),那我们就要针对我们的代码来解决我们要解决的问题。
在POI3.8之后新增加了一个类,SXSSFWorkbook,采用当数据加工时不是类似前面版本的对象,它可以控制excel数据占用的内存,他通过控制在内存中的行数来实现资源管理,即当创建对象超过了设定的行数,它会自动刷新内存,将数据写入文件,这样导致打印时,占用的CPU,和内存很少。但有人会说了,我用过这个类啊,他好像并不能完全解决,当数据量超过一定量后还是会内存溢出的,而且时间还很长。对你只是用了这个类,但是你并没有针对你的需求进行相应的设计,仅仅是用了,所以接下来我要说的问题就是,如何通过SXSSFWorkbook以及相应的写入设计来实现百万级别的数据快速写入。
我先举个例子,以前我们数据库中存在大量的数据,我们要查询,怎么办?我们在没有经过设计的时候是这样来处理的,先写一个集合,然后执行jdbc,将返回的结果赋值给list,然后再返回到页面上,但是当数据量大的时候,就会出现数据无法返回,内存溢出的情况,于是我们在有限的时间和空间下,通过分页将数据一页一页的显示出来,这样可以避免了大数据量数据对内存的占用,也提高了用户的体验,在我们要导出的百万数据也是一个道理,内存突发性占用,我们可以限制导出数据所占用的内存,这里我先建立一个list容器,list中开辟10000行的存储空间,每次存储10000行,用完了将内容清空,然后重复利用,这样就可以有效控制内存,所以我们的设计思路就基本形成了,所以分页数据导出共有以下3个步骤:
1、求数据库中待导出数据的行数
2、根据行数求数据提取次数
3、按次数将数据写入文件
代码如下:
我的项目的jar包版本
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>3.9</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>3.9</version>
</dependency>
package com.asd.reserve.utils.poi;
import com.asd.reserve.pojo.bean.Person;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* @author zs
* @date 2019/12/24 15:39
*/
public class Sxss {
public static void main(String[] args) {
try {
Sxss.exportBigDataExcel("E:/xx.xlsx");
} catch (IOException e) {
e.printStackTrace();
}
}
public static void exportBigDataExcel(String path)
throws IOException {
// 最重要的就是使用SXSSFWorkbook,表示流的方式进行操作
// 在内存中保持100行,超过100行将被刷新到磁盘
SXSSFWorkbook wb = new SXSSFWorkbook(100);
Sheet sh = wb.createSheet(); // 建立新的sheet对象
Row row = sh.createRow(0); // 创建第一行对象
// -----------定义表头-----------
Cell cel0 = row.createCell(0);
cel0.setCellValue("1");
Cell cel2 = row.createCell(1);
cel2.setCellValue("2");
Cell cel3 = row.createCell(2);
cel3.setCellValue("3");
Cell cel4 = row.createCell(3);
// ---------------------------
List<Person> list = new ArrayList<Person>();
// 数据库中存储的数据行
int page_size = 2;
// 求数据库中待导出数据的行数
/*int list_count = this.daoUtils.queryListCount(this.valueDataDao
.queryExportSQL(valueDataDto).get("count_sql"));*/
int list_count = 2;//需求计算得到
// 根据行数求数据提取次数
int export_times = list_count % page_size > 0 ? list_count / page_size
+ 1 : list_count / page_size;
// 按次数将数据写入文件
for (int j = 0; j < export_times; j++) {
/*list = this.valueDataDao.queryPageList(this.valueDataDao
.queryExportSQL(valueDataDto).get("list_sql"), j + 1,
page_size);*/
Person person1 = new Person("ZS","21");
Person person2 = new Person("MF","21");
for(int n =0 ; n<500000;n++){
list.add(person1);//需要从数据库分页查询数据,此处写死为两条数据
list.add(person2);
}
int len = list.size();
//int len = list.size() < page_size ? list.size() : page_size;
for (int i = 0; i < len; i++) {
Row row_value = sh.createRow(j * page_size + i + 1);
Cell cel0_value = row_value.createCell(0);
cel0_value.setCellValue(list.get(i).getName());
Cell cel2_value = row_value.createCell(1);
cel2_value.setCellValue(list.get(i).getName());
Cell cel3_value = row_value.createCell(2);
cel3_value.setCellValue(list.get(i).getAge());
}
list.clear(); // 每次存储len行,用完了将内容清空,以便内存可重复利用
}
FileOutputStream fileOut = new FileOutputStream(path);
wb.write(fileOut);
fileOut.close();
wb.dispose();
}
}
注:此处测试的了100万条数据导出,文件大小为9MB;而准备金系统现在的用HSSFWorkbook生成并导出Excel文件,当文件大小为5MB时,就内存溢出了。(本地电脑的物理内存为8G)。
到目前已经可以实现百万数据的导出了,但是当我们的业务数据超过200万,300万了呢?如何解决?
这时,直接打印数据到一个工作簿的一个工作表是实现不了的,必须拆分到多个工作表,或者多个工作簿中才能实现。因为一个sheet最多行数为1048576。代码如下:
package com.asd.reserve.utils.poi;
import com.asd.common.jdbc.ConnectionUtil;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import java.io.FileOutputStream;
import java.io.IOException;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
/**
* @author zs
* @date 2019/12/24 16:55
*/
public class Sxss2 {
public static void main(String[] args) throws Exception {
Sxss2 tm = new Sxss2();
tm.jdbcex(true);
}
public void jdbcex(boolean isClose) throws InstantiationException, IllegalAccessException,
ClassNotFoundException, SQLException, IOException, InterruptedException {
String xlsFile = "E:/xxx.xlsx"; //输出文件
//内存中只创建100个对象,写临时文件,当超过100条,就将内存中不用的对象释放。
Workbook wb = new SXSSFWorkbook(100); //关键语句
Sheet sheet = null; //工作表对象
Row nRow = null; //行对象
Cell nCell = null; //列对象
//使用jdbc链接数据库
/*Class.forName("com.mysql.jdbc.Driver").newInstance();
String url = "jdbc:mysql://localhost:3306/bigdata?characterEncoding=UTF-8";
String user = "root";
String password = "123456";
//获取数据库连接
Connection conn = DriverManager.getConnection(url, user,password);
Statement stmt = conn.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE,ResultSet.CONCUR_UPDATABLE);
String sql = "select * from hpa_normal_tissue limit 1000000"; //100万测试数据
ResultSet rs = stmt.executeQuery(sql);
ResultSetMetaData rsmd = rs.getMetaData();*/
//List<String> list = new ArrayList<String>();
Connection conn = null;
Statement stmt = null;
PreparedStatement statement = null;
ResultSet rs = null;
//try {
//连接数据库
conn = ConnectionUtil.openConnect();
//开启事物
ConnectionUtil.beginTransaction(conn);
String sql = "select * from aaa";
statement = conn.prepareStatement(sql);
rs = statement.executeQuery();
ResultSetMetaData rsmd = rs.getMetaData();
//把数据放入List<String>集合中
/*while (rs.next()) {
String assessdateTemp = rs.getString(1);
list.add(assessdateTemp);
}
}catch (Exception e){
e.printStackTrace();
ConnectionUtil.rollBackTransaction(conn);
}finally{
ConnectionUtil.closeStatements(statement);
ConnectionUtil.closeStatements(stmt);
ConnectionUtil.commitTransaction(conn);
ConnectionUtil.closeConnect(conn);
}*/
long startTime = System.currentTimeMillis(); //开始时间
System.out.println("strat execute time: " + startTime);
int rowNo = 0; //总行号
int pageRowNo = 0; //页行号
while (rs.next()) {
//打印3000条后切换到下个工作表,可根据需要自行拓展,2百万,3百万...数据一样操作,只要不超过1048576就可以
if (rowNo % 3000 == 0) {
System.out.println("Current Sheet:" + rowNo / 3000);
sheet = wb.createSheet("我的第" + (rowNo / 3000) + "个工作簿");//建立新的sheet对象
sheet = wb.getSheetAt(rowNo / 3000); //动态指定当前的工作表
pageRowNo = 0; //每当新建了工作表就将当前工作表的行号重置为0
}
rowNo++;
nRow = sheet.createRow(pageRowNo++); //新建行对象
// 打印每行,每行有6列数据 rsmd.getColumnCount()==6 --- 列属性的个数(由查询sql查询的列数决定)
for (int j = 0; j < rsmd.getColumnCount(); j++) {
nCell = nRow.createCell(j);
nCell.setCellValue(rs.getString(j + 1));
}
if (rowNo % 1000 == 0) {
System.out.println("row no: " + rowNo);
}
// Thread.sleep(1); //休息一下,防止对CPU占用,其实影响不大
}
long finishedTime = System.currentTimeMillis(); //处理完成时间
System.out.println("finished execute time: " + (finishedTime - startTime) / 1000 + "m");
FileOutputStream fOut = new FileOutputStream(xlsFile);
wb.write(fOut);
fOut.flush(); //刷新缓冲区
fOut.close();
long stopTime = System.currentTimeMillis(); //写文件时间
System.out.println("write xlsx file time: " + (stopTime - startTime) / 1000 + "m");
if (isClose) {
this.close(rs, statement, conn);
}
}
//执行关闭流的操作
private void close(ResultSet rs, Statement statement, Connection conn ) throws SQLException{
rs.close();
statement.close();
conn.close();
}
}
上述代码都只是简单的demo,仅仅实现了功能。具体到项目中,还要根据业务场景等,对用SXSSFWookbook导出Excel的工具(参看HSSFWorkbook)进行完善。添加样式,表头设置等。在代码的编写过程中,遇到了很多问题:jdk偏低不支持最新的poi版本等(准备金系统现在是jdk7)。
此外还可以用hutool工具,在使用时,一定要注意版本与jdk,poi的版本是否匹配。要求poi-ooxml版本高于3.17(todo)
没测试,本地是3.9,版本太低,就没有测试且jdk为1.7的就没测试。链接:https://www.bookstack.cn/read/hutool/a6819f05207359bb.md
使用poi导出Excel遇到的问题
1、Excel读写时候内存溢出
虽然POI是目前使用最多的用来做excel解析的框架,但这个框架并不那么完美。大部分使用POI都是使用他的userModel模式。userModel的好处是上手容易使用简单,随便拷贝个代码跑一下,剩下就是写业务转换了,虽然转换也要写上百行代码,相对比较好理解。然而userModel模式最大的问题是在于非常大的内存消耗,一个几兆的文件解析要用掉上百兆的内存。现在很多应用采用这种模式,之所以还正常在跑一定是并发不大,并发上来后一定会OOM或者频繁的full gc。
2、OOM
正常的 poi 在处理比较大的 excel 的时候,会出现内存溢出。网上的解决方案也比较多。
比如官方的 SXSSF (Since POI 3.8 beta3) (http://poi.apache.org/components/spreadsheet/index.html)解决方式。
或者使用封装好的包
easypoi ExcelBatchExportServer(http://easypoi.mydoc.io/#text_202984)
hutool BigExcelWriter(https://www.hutool.cn/docs/#/poi/Excel%E5%A4%A7%E6%95%B0%E6%8D%AE%E7%94%9F%E6%88%90-BigExcelWriter)
原理都是强制使用 xssf 版本的Excel。
hutool在表头的处理方面没法很方便的统一。你可以自己定义类似于 easypoi/easyexcel 中的注解,自己反射解析。然后统一处理表头即可。
你也可以使用 easyexcel(https://github.com/alibaba/easyexcel),当然这个注释文档有些欠缺,而且设计的比较复杂,不是很推荐。
3、关于SXSSFWorkBook
写有大量数据的xlsx文件时,POI为我们提供了SXSSFWorkBook类来处理,这个类的处理机制是当内存中的数据条数达到一个极限数量的时候就flush这部分数据,再依次处理余下的数据,这个在大多数场景能够满足需求。 读有大量数据的文件时,使用WorkBook处理就不行了,因为POI对文件是先将文件中的cell读入内存,生成一个树的结构(针对Excel中的每个sheet,使用TreeMap存储sheet中的行)。如果数据量比较大,则同样会产生java.lang.OutOfMemoryError: Java heap space错误。POI官方推荐使用“XSSF and SAX(event API)”方式来解决。
参考连接:https://houbb.github.io/2018/11/07/excel-export
问题案例
百度的案例(其他人项目遇到的一个问题,自己在写Excel导出工具时可以借鉴,不再出现该问题):链接:https://www.jianshu.com/p/dbb05971ca56
最近公司一个06年统计项目在导出Excel时造成应用服务器内存溢出、假死现象;查看代码发现问题一次查询一整年的数据导致堆内存被撑爆
假死,随后改用批量查询往Excel中写数据,同样的问题又出现了!!!随后在网上查阅了部分资料只是在POI大数据导出API的基础上写的demo示例无任何参考价值...
解决内存溢出常用方法就是打开GC日志: {Heap before GC invocations=29 (full 14): par new generation total 306688K, used 306687K [0x0000000080000000, 0x0000000094cc0000, 0x0000000094cc0000) eden space 272640K, 100% used [0x0000000080000000, 0x0000000090a40000, 0x0000000090a40000) from space 34048K, 99% used [0x0000000090a40000, 0x0000000092b7ffe0, 0x0000000092b80000) to space 34048K, 0% used [0x0000000092b80000, 0x0000000092b80000, 0x0000000094cc0000) concurrent mark-sweep generation total 1756416K, used 1756415K [0x0000000094cc0000, 0x0000000100000000, 0x0000000100000000) Metaspace used 43496K, capacity 44680K, committed 45056K, reserved 1089536K class space used 5254K, capacity 5515K, committed 5632K, reserved 1048576K 2017-09-12T21:55:02.954+0800: 239.209: [Full GC (Allocation Failure) 2017-09-12T21:55:02.954+0800: 239.209: [CMS: 1756415K->1756415K(1756416K), 5.4136680 secs] 2063103K->1971243K(2063104K), [Metaspace: 43496K->43496K(1089536K)], 5.4138690 secs] [Times: user=5.41 sys=0.00, real=5.41 secs] Heap after GC invocations=30 (full 15): par new generation total 306688K, used 214827K [0x0000000080000000, 0x0000000094cc0000, 0x0000000094cc0000) eden space 272640K, 78% used [0x0000000080000000, 0x000000008d1cacb0, 0x0000000090a40000) from space 34048K, 0% used [0x0000000090a40000, 0x0000000090a40000, 0x0000000092b80000) to space 34048K, 0% used [0x0000000092b80000, 0x0000000092b80000, 0x0000000094cc0000) concurrent mark-sweep generation total 1756416K, used 1756415K [0x0000000094cc0000, 0x0000000100000000, 0x0000000100000000) Metaspace used 43238K, capacity 44256K, committed 45056K, reserved 1089536K class space used 5213K, capacity 5441K, committed 5632K, reserved 1048576K }
主要信息: 2017-09-12T21:55:02.954+0800: 239.209: [Full GC (Allocation Failure) 2017-09-12T21:55:02.954+0800: 239.209: <span style="color:red;">[CMS: 1756415K->1756415K(1756416K), 5.4136680 secs] 2063103K->1971243K(2063104K), [Metaspace: 43496K->43496K(1089536K)], 5.4138690 secs]</span> [Times: user=5.41 sys=0.00, real=5.41 secs] 通过查看GC日志发现堆空间、元空间不能被回收(对象强引用导致) 解决方法: 查看业务代码: SXSSFWorkbook sxssfWorkbook = new SXSSFWorkbook(1000); for(int i=1;i<=pageCount;i++){ int tableNum = i; int pageIndex = i; //分页数据查询 List<Map<String, Object>> maps = dbFactory.getJdbcTemplate().queryForList(finalSql,(pageIndex-1)*pageSize,pageIndex*pageSize); SXSSFSheet sheet = sxssfWorkbook.createSheet("sheet"+tableNum); SXSSFRow sxssfRow = sheet.createRow(0); for(int a=0;a<titles.length;a++){ sxssfRow.createCell(a).setCellValue(titles[a]); } for(int a=1;a<=maps.size();a++){ SXSSFRow sxssfRow = sheet.createRow(a); Map<String,Object> data = maps.get(a-1); Set<String> keySet = data.keySet(); Iterator<String> iterator = keySet.iterator(); int cell = 0; while(iterator.hasNext()){ String key = iterator.next(); Object valueObject = data.get(key); SXSSFCell sxssfCell = sxssfRow.createCell(cell); sxssfCell.setCellValue(valueObject==null?"":valueObject.toString()); cell++; } } //数据清理 maps.clear(); //设置空引用 maps = null; } FileOutputStream fos = new FileOutputStream(tempPath+fileName); sxssfWorkbook.write(fos); fos.close(); sxssfWorkbook.dispose(); 代码中数据清理、设置空引用都做了,为什么还是不能被回收呢??? 通过JVM自带检测工具jmap查看活跃对象 重大发现原来是
org.apache.poi.xssf.streaming.SXSSFCell、
org.apache.poi.xssf.streaming.SXSSFCell$PlainStringValue、
org.apache.poi.xssf.streaming.SXSSFRow
这三个鬼把内存占完了 优化代码 SXSSFWorkbook sxssfWorkbook = new SXSSFWorkbook(1000); SXSSFCell sxssfCell = null; SXSSFRow sxssfRow = null; for(int i=1;i<=pageCount;i++){ int tableNum = i; int pageIndex = i; List<Map<String, Object>> maps = dbFactory.getJdbcTemplate().queryForList(finalSql,(pageIndex-1)*pageSize,pageIndex*pageSize); SXSSFSheet sheet = sxssfWorkbook.createSheet("sheet"+tableNum); sxssfRow = sheet.createRow(0); for(int a=0;a<titles.length;a++){ sxssfRow.createCell(a).setCellValue(titles[a]); } for(int a=1;a<=maps.size();a++){ sxssfRow = sheet.createRow(a); Map<String,Object> data = maps.get(a-1); Set<String> keySet = data.keySet(); Iterator<String> iterator = keySet.iterator(); int cell = 0; while(iterator.hasNext()){ String key = iterator.next(); Object valueObject = data.get(key); sxssfCell = sxssfRow.createCell(cell); sxssfCell.setCellValue(valueObject==null?"":valueObject.toString()); cell++; } //map数据清理 data.clear(); } //数据清理 maps.clear(); //设置空引用 maps = null; } FileOutputStream fos = new FileOutputStream(tempPath+fileName); sxssfWorkbook.write(fos); fos.close(); sxssfWorkbook.dispose(); 程序SXSSFRow、SXSSFCell这两个对象持有一个引用,每当新创建一个对象时候原来引用失效jvm会自动回收
通过上述案例,自己在编写Excel导出工具时,注意也可以使上述的两个对象都持有一个引用。
自己的工具(附代码):(todo)
XSSFWorkbook方式及SXSSFWorkbook: https://www.cnblogs.com/zhangshuaivole/p/13793392.html
Excel的简单介绍
.xls 是03版Office Microsoft Office Excel 工作表的格式,用03版Office,新建Excel默认保存的Excel文件格式的后缀是.xls;
.xlsx 是07版Office Microsoft Office Excel 工作表的格式,用07版Office,新建Excel默认保存的的Excel文件格式后缀是.xlsx。
07版的Office Excel,能打开编辑07版(后缀.xlsx)的Excel文件,也能打开编辑03版(后缀.xls)的Excel文件,都不会出现乱码或者卡死的情况。
但是,03版的Office Excel,就只能打开编辑03版(后缀.xls)的Excel文件;如果打开编辑07版(后缀.xlsx)的Excel文件,则可能出现乱码或者开始能操作到最后就卡死,以后一打开就卡死。
那么07版.xlsx的Excel文件,怎么才能在03版的Office Excel中打开呢?
也简单,举个例,你家里的电脑用的07版Office Excel,你在家里做好一个Excel的文件,你默认保存的话就是.xlsx格式;如果你要拷到公司电脑上用,公司的电脑是03版Office Excel,要是你直接拷过去的话,是没法用的;你得这样,在家里做Office Excel的时候,保存的时候,点击office按钮,采用另存为“97-2003 Excel 工作簿”,这样保存的格式就是03版的.xls格式,这样就实现.xlsx文件转换成.xls。这样再把.xls格式Excel文件拷到公司的电脑上就能用了,就OK了。
如果你家里的电脑是03版Office Excel,那么默认保存(.xls)就行,不管公司的电脑是03版的还是07版的Office。
EXCEL 的限制
EXCEL 的上限行数为 1048576。2^20。超过这个数量,EXCEL 打开会有问题。