项目中E端有一个订单导出的功能能(导出销售订单或者销售退单,导出列颇多,且必须满足实时数据)。我们使用POI导出数据,并且后端加了熔断措施,导出限流,大促期间导出开关控制。相对来说有了这些机制线上应用不会因为导出操作流量过大内存爆掉,也保证了应用安全稳定的运行,但是最近监控发现导出操作性能急剧下降(数据量已经超过3百万),先看看监控。
再来看看使用POI导出本地jvisualvm的内存动态变化图。
这里我们主要分析第二个点,对于第一点大家都清楚如何解决问题。首先应该思考为什么使用POI导出的时候内存飙升的那么快呢?
结合Thread Dump可以看出,导出的时候内存增长过快,在数据量大和请求量过大的情况下,内存极速增长,然而这个过程中大量对象存活在年轻代,在年轻代无法被回收直接进入老年代。总体来说POI使用XMLBean处理Dom写Excel文件,内存占用过大,耗费资源;并且导出速度满,占用内存资源时间过长,导致一系列恶性循环。
既然POI导出有这些不足之处,如何解决这样的问题呢?思路很简单,不再使用POI导出。降低服务端资源占用。后端服务可以只查询JSON数据,导出的工作交给客户端,这样完全屏蔽掉了使用POI导出的问题,可以想象,这样做就是一个简单的restful列表查询接口。
public void deleteUUID(String uuid) { cacheRedisTemplate.delete(ORDER_EXPORT_UUID + uuid); }
public long incrementAndGetUUIDValue(String uuid) { RedisAtomicLong counter = new RedisAtomicLong(ORDER_EXPORT_UUID + uuid, cacheRedisTemplate.getConnectionFactory()); return counter.incrementAndGet(); }
public List getSaleOrderData(String uuid, ExportOrderQueryDTO orderQuery, boolean showPhoneNumFlag) { if (!existKey(uuid)) { return null; }
12345678910111213141516171819202122232425 | long exportCount = incrementAndGetUUIDValue(uuid); //导出次数限制(这里一次查询1000条,最多查询30次,导出最大值为3万) if (exportCount > EXPORT_MAX_PAGE) { deleteUUID(uuid); return null; } //验证最大导出值 if (exportCount == 1) { int totalCount = exportMapper.countSaleOrders(orderQuery); Preconditions.checkArgument(totalCount < EXPORT_ONCE * EXPORT_MAX_PAGE, "最多导出%s条数据", EXPORT_ONCE * EXPORT_MAX_PAGE); } orderQuery.setOffset((exportCount - 1) * EXPORT_ONCE); orderQuery.setLimit(EXPORT_ONCE); List<Long> ids = exportMapper.listSaleOrderIds(orderQuery); if (CollectionUtils.isEmpty(ids)) { deleteUUID(uuid); return null; } List<SalesOrderExportDTO> orderExportDTOS = exportMapper.listSaleOrders(ids); } ```` |
---|
经过这么多天的线上应用内存观察,前端导出Excel的有点真的是毋庸置疑,减轻了后端服务的压力,后端服务性能飙升。