业务场景是主要是查询或者导出某家分销商一个月内产生的退票退款订单数据。由于涉及到机密数据,因此不便展示效果图。只记录一下遇到的两个经典的问题以及解决思路以供参考。
出现的问题:
因为测试环境测试不出生产环境的问题,生产环境订单量较大,我看了一下订单加在一起有三千多万条,因此部署到生产环境之后导出功能出现了两个问题,一个是内存溢出,另一个是导出超时。
思路:
因为导出的数据格式和查询的数据格式相同,因此开发时,导出的逻辑是在线程池中分页查询每一页数据然后组装完成Excel,因此我优化导出功能时其实是包含着优化查询功能,查询的逻辑是线程池按时间分段查询数据。
解决步骤如下:
第一阶段:上生产之后报内存溢出(OutOfMemory:Heap Space)我首先检查了一下代码,主要考虑堆溢出的原因,发现了一些没有必要的或者肉眼可见的问题进行了第一次优化,具体内容如下:
一、 数据库方面:从查询sql字段由“*”减少到只查询需要展示的几个字段,提高查询速度,降低内存容量。
二、 Excel生成工具:因为Excel生成工具我用的是HSSFWorkbook,这个生成的数据流是往内存中写的,当数据量过大时,肯定会超出内存承受能力。从HSSFWorkbook改成SXSSFWorkbook,SXSSFWorkbook是专门用于大数据量时使用,SXSSFWorkbook是streaming版本的XSSFWorkbook,它只会保存最新的rows在内存里供查看,在此之前的rows都会被写入到硬盘里,因此可以降低内存的消耗。
三、 减少创建对象的数量:在每页生成Excel行数时,每个SXSSFCell我都创建了一个cell对象来设置换行样式和数据信息,因为是堆溢出,说明对象创建的太多了。而且我生成Excel数据流时使用的是线程池,需要内存同时存放多个线程的cell对象,这样太费内存了,因此我去掉了用对象接收cell,而是直接createCell,然后设置有样式使用getCell。降低了内存的压力。
四、 使用Redis缓存:针对用户相同的查询条件,将查询条件和查询结果缓存到Redis中,这样,下次用户进行分页查询时就不需要再去访问数据库重新组装数据了,降低数据库访问压力。
经过上面第一阶段的优化,我又重新上了生产,再次测试一万以上的数据导出,好吧,超时(一分钟为最大访问时间)。
于是,我进行了第二阶段的优化:
一、 逻辑上:之前因为导出的数据格式和查询的数据格式相同,因此开发时,导出的逻辑是在线程池中分页查询每一页数据然后组装完成Excel。但是直接调用查询方法会重复调用相同逻辑,比如查询时,每次都会检查Redis中是否已存在相同的查询条件,这个检查也是比较耗时的。于是我改成了不直接调用查询方法,在组装Excel数据前,先检查Redis中数据是否已存在,若存在,则直接使用数据去循环组装每页数据,这样节省了查询Redis的时间。
二、 查询方式修改:由于主表数据量太大,表的总数据量有3千多万,字段数50个,现在已经分表了,数据量太大,查询起来确实比较耗时。建议我改一下查询逻辑,不要从主表下手查询,改成其他查询方式。因为我这个功能主要是查询某种符合条件的数据,正常查询逻辑的话,是应该从主表查询出数据,然后再进行筛选。但是查询主表的话,需要将多种状态的数据查询出来然后再根据逻辑筛选,主表数据量大的一个月的数据量有十几万订单,我改成了直接查询特定表的状态符合当前查询条件的数据,这样就直接可以根据特定表与主表的关联查询出符合条件的主表数据,降低了主表的查询压力,节省了查询时间。
三、 导出文件方式:之前是分页生成数据流,然后将生成的二进制流传给前端。这样的弊端是,在大数据量比如一个月有一万三四的数据时,那么后端将这么大的二进制流传给前端是需要时间传送的,前端再将二进制流生成文件给用户,整个过程也是比较耗时的。因此我改成了将数据流直接上传压缩成zip包上传到云上,然后给前端一个文件的链接,前端再从云上下载压缩包给用户。这个过程省去了后端将二进制流传给前端的时间。
经过第二阶段的优化,再次上生产,尝试导出最大数据量的数据,只需要17秒即可获取压缩文件。解决了超时的问题。
这个优化前后大概花了两三天时间,晚上睡觉都梦见如何解决这个问题,我都在想这个问题如果解决不了我咋整了,想的有点儿远了···这个问题我反思了一下,一是对代码开发环境大数据量没有一个深刻的认识,二是没有之前没有碰到过这种性能问题,经验不足。通过这个问题的教训,我觉得还是有必要多来几个这样的功能锻炼一下,除了JVM之外,性能优化还是有很多方面是存在优化点的,在优化过程中可以打印一下耗时或者用一些监控工具看一下内存等等,来定位具体的优化点。