小弟在修改一位同事的代码,主要功能是将数据库中查询的数据导出成excel并发送邮件,整个过程要55min,有点长,数据不到20W。怎么回事呢?
在排查过程中,发现其他发送邮件与io流写入都耗时很少。那唯一的问题就是在生成excel数据时了,代码如下:
HSSFWorkbook hwb = new HSSFWorkbook();
HSSFFont font = hwb.createFont();// 创建字体样式
font.setFontName("宋体");// 使用宋体
font.setFontHeightInPoints((short) 10);// 字体大小
// 设置单元格格式
HSSFCellStyle style1 = hwb.createCellStyle();
style1.setFont(font);// 将字体注入
style1.setWrapText(true);// 自动换行
style1.setAlignment(HSSFCellStyle.ALIGN_CENTER);// 左右居中
style1.setVerticalAlignment(HSSFCellStyle.VERTICAL_CENTER);// 上下居中
style1.setFillForegroundColor(IndexedColors.LIGHT_YELLOW.getIndex());// 设置单元格的背景颜色
style1.setFillPattern(CellStyle.SOLID_FOREGROUND);
style1.setBorderTop((short) 1);// 边框的大小
style1.setBorderBottom((short) 1);
style1.setBorderLeft((short) 1);
style1.setBorderRight((short) 1);
// 创建sheet对象(表单对象)
HSSFSheet sheet1 = hwb.createSheet("随心转自由转持有金额");
// 设置每列的宽度
sheet1.setColumnWidth(0, 20 * 256);
sheet1.setColumnWidth(1, 20 * 256);
sheet1.setColumnWidth(2, 20 * 256);
sheet1.setColumnWidth(3, 20 * 256);
sheet1.setColumnWidth(4, 20 * 256);
// 创建sheet的列名
HSSFRow row1 = sheet1.createRow(0);
row1.createCell(0).setCellValue("用户id");
row1.createCell(1).setCellValue("会员等级");
row1.createCell(2).setCellValue("天才值");
row1.createCell(3).setCellValue("随心转持有金额");
row1.createCell(4).setCellValue("自由转持有金额");
// Date lastDay = DateUtil.afterNDay(DateUtil.todayDate(), -1);
long pageSize = 500;// 每次查询500条数据
// 总数
long sum = idebtcurrentuserholdingservice.countUserFreeCurrentNum();
logger.info("======sum= 总数========================"+sum);
long totalPage = sum % pageSize > 0 ? sum / pageSize + 1 : sum / pageSize;// 分页公式 总页数
logger.info("======totalPage===总页数======================"+totalPage);
if (totalPage > 0) {
for (int i = 1; i <= totalPage; i++) {
List<DebtHoldingVo> debtHoldvoList = idebtcurrentuserholdingservice
.selectUserfreecruentamount(pageSize * (i - 1), pageSize);// 分页去查询
for (int j = 0; j < debtHoldvoList.size(); j++) {
HSSFRow row = sheet1.createRow((int) ((j+1)+pageSize *(i - 1)));
row.createCell(0).setCellValue(debtHoldvoList.get(j).getUserId());
row.createCell(1).setCellValue(debtHoldvoList.get(j).title());
row.createCell(2).setCellValue(debtHoldvoList.get(j).getTalentValue());
row.createCell(3).setCellValue(NumberFormat.doubleUpTwoDecimal(NumberFormat.outDataMoney(debtHoldvoList.get(j).getCurrentamount()))); //四舍五入保留2位
row.createCell(4).setCellValue(NumberFormat.doubleUpTwoDecimal(NumberFormat.outDataMoney(debtHoldvoList.get(j).getFreeamount())));
}
}
}
以上 查看后发现:
1、使用的是03版本的excel ,根据poi的api可知,03版本的excel一个sheet最大才能存6W+的数据量。而目前数据量是20W左右,虽然生成的数据,但领导也没说正确与否,我估计也不可能正确。所以我修改了poi的版本 支持07版本的excel
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>3.10-FINAL</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>3.10-FINAL</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml-schemas</artifactId>
<version>3.10-FINAL</version>
</dependency>
此api 支持一次性写入大量数据(104W+)。代码修改为:
long startTimes = System.currentTimeMillis();
SXSSFWorkbook sxssfWorkbook = new SXSSFWorkbook(1000);
Font font = sxssfWorkbook.createFont();
font.setFontName("宋体");// 使用宋体
font.setFontHeightInPoints((short) 10);
// 设置单元格格式
CellStyle cellStyle = sxssfWorkbook.createCellStyle();
cellStyle.setFont(font);// 将字体注入
cellStyle.setWrapText(true);// 自动换行
cellStyle.setAlignment(HSSFCellStyle.ALIGN_CENTER);// 左右居中
cellStyle.setVerticalAlignment(HSSFCellStyle.VERTICAL_CENTER);// 上下居中
cellStyle.setFillForegroundColor(IndexedColors.LIGHT_YELLOW.getIndex());// 设置单元格的背景颜色
cellStyle.setFillPattern(CellStyle.SOLID_FOREGROUND);
cellStyle.setBorderTop((short) 1);// 边框的大小
cellStyle.setBorderBottom((short) 1);
cellStyle.setBorderLeft((short) 1);
cellStyle.setBorderRight((short) 1);
Sheet firstSheet = sxssfWorkbook.createSheet("随心转自由转持有金额");
// 设置每列的宽度
firstSheet.setColumnWidth(0, 20 * 256);
firstSheet.setColumnWidth(1, 20 * 256);
firstSheet.setColumnWidth(2, 20 * 256);
firstSheet.setColumnWidth(3, 20 * 256);
firstSheet.setColumnWidth(4, 20 * 256);
Row row0 = firstSheet.createRow(0);
row0.createCell(0).setCellValue("用户id");
row0.createCell(1).setCellValue("会员等级");
row0.createCell(2).setCellValue("天才值");
row0.createCell(3).setCellValue("随心转持有金额");
row0.createCell(4).setCellValue("自由转持有金额");
long pageSize = 500;// 每次查询500条数据最快
// 总数
long sum = idebtcurrentuserholdingservice.countUserFreeCurrentNum();
logger.info("======sum= 总数========================"+sum);
long totalPage = sum % pageSize > 0 ? sum / pageSize + 1 : sum / pageSize;// 分页公式 总页数
logger.info("======totalPage===总页数======================"+totalPage);
List<DebtHoldingVo> debtHoldvoList = null;
if (totalPage > 0) {
for (int i = 1; i <= totalPage; i++) {
if(i == 1){
debtHoldvoList = idebtcurrentuserholdingservice
.selectUserfreecruentamount(pageSize * (i - 1), pageSize);
}else{
//获取最后一个hid
DebtHoldingVo vo = debtHoldvoList.get(debtHoldvoList.size() - 1);
Long hId = vo.gethId();
debtHoldvoList = idebtcurrentuserholdingservice
.selectUserfreecruentamount(hId, pageSize);
}
for (int j = 0; j < debtHoldvoList.size(); j++) {
Row row = firstSheet.createRow((int) ((j+1)+pageSize *(i - 1)));
row.createCell(0).setCellValue(debtHoldvoList.get(j).getUserId());
row.createCell(1).setCellValue(debtHoldvoList.get(j).title());
row.createCell(2).setCellValue(debtHoldvoList.get(j).getTalentValue());
row.createCell(3).setCellValue(NumberFormat.doubleUpTwoDecimal(NumberFormat.outDataMoney(debtHoldvoList.get(j).getCurrentamount()))); //四舍五入保留2位
row.createCell(4).setCellValue(NumberFormat.doubleUpTwoDecimal(NumberFormat.outDataMoney(debtHoldvoList.get(j).getFreeamount())));
}
}
}
修改了上述方案测试了一下 为37min;
2、最耗时的地方是
List<DebtHoldingVo> debtHoldvoList = idebtcurrentuserholdingservice
.selectUserfreecruentamount(pageSize * (i - 1), pageSize);// 分页去查询
逻辑上肯定是没错的,但分页的sql写法有问题,如下:
SELECT DISTINCT
h.id AS hid,
u.id,
u.talent_value,
(
SELECT
IFNULL(sum(amount), 0)
FROM
debt_current_user_holding
WHERE
asset_type = 'FREE_CURRENT'
AND debt_repayment_status != 1
AND user_id = u.id
) currentamount,
(
SELECT
IFNULL(sum(amount), 0)
FROM
debt_current_user_holding
WHERE
asset_type = 'FREE_PRODUCT'
AND debt_repayment_status != 1
AND user_id = u.id
) freeamount
FROM
debt_current_user_holding h
LEFT JOIN users u ON h.user_id = u.id
WHERE
u.deleted_at IS NULL
AND h.deleted_at IS NULL
AND h.asset_type IN (
'FREE_PRODUCT',
'FREE_CURRENT'
)
LIMIT #{startPages}, #{countPage}
打眼一看没啥事。但仔细想想 就不是那么回事了。这种写法在小数据量前提下肯定没问题。一旦数据量过万,就会出现性能瓶颈。
随着startPages的增加,查询速度也会越来越慢,原因就是 每次查询时,mysql都是全表查询,然后从指定位置向后取countPage数量。这种做法是不可取的。
修改后的sql为:
SELECT DISTINCT
h.id AS hid,
u.id,
u.talent_value,
(
SELECT
IFNULL(sum(amount), 0)
FROM
debt_current_user_holding
WHERE
asset_type = 'FREE_CURRENT'
AND debt_repayment_status != 1
AND user_id = u.id
) currentamount,
(
SELECT
IFNULL(sum(amount), 0)
FROM
debt_current_user_holding
WHERE
asset_type = 'FREE_PRODUCT'
AND debt_repayment_status != 1
AND user_id = u.id
) freeamount
FROM
debt_current_user_holding h
LEFT JOIN users u ON h.user_id = u.id
WHERE
h.id > #{startPages}
AND u.deleted_at IS NULL
AND h.deleted_at IS NULL
AND h.asset_type IN (
'FREE_PRODUCT',
'FREE_CURRENT'
)
LIMIT #{countPage}
这种查询的好处就是每次查询的时候都会从指定的id后进行查询,使用主键唯一索引每次不会全表查询,前提是主键最好是数字类型,且自增的。 查询后发现数据是按照主键进行升序排列(查询的默认机制asc)。但网上的说法是可能会丢失一部分数据(这种是因为有人为操作数据到导致主键不连续。或者断层很大。或者主键不是有序的。)
这种查询方式适用范围较小,必须要主键是数字类型,没有断层。最好是自增的。
如果不满足上述条件,如果增加了order by 在大数据量的前提下 效率很低。
其他方案还在验证中;
以上方案修改完成后 测试结果为131s 。