放弃使用POI导出Excel

项目中E端有一个订单导出的功能能(导出销售订单或者销售退单,导出列颇多,且必须知足实时数据)。咱们使用POI导出数据,而且后端加了熔断措施,导出限流,大促期间导出开关控制。相对来讲有了这些机制线上应用不会由于导出操做流量过大内存爆掉,也保证了应用安全稳定的运行,可是最近监控发现导出操做性能急剧降低(数据量已经超过3百万),先看看监控。前端

再来看看使用POI导出本地jvisualvm的内存动态变化图。redis

分析问题

  1. 导出请求耗时,排查SQL语句以及数据量。
  2. 应用内存居高不下(内存在新生代未被回收掉进入了老年代,老年代full GC的时候这些对象才能被销毁释放内存)。

这里咱们主要分析第二个点,对于第一点你们都清楚如何解决问题。首先应该思考为何使用POI导出的时候内存飙升的那么快呢?后端

结合Thread Dump能够看出,导出的时候内存增加过快,在数据量大和请求量过大的状况下,内存极速增加,然而这个过程当中大量对象存活在年轻代,在年轻代没法被回收直接进入老年代。整体来讲POI使用XMLBean处理Dom写Excel文件,内存占用过大,耗费资源;而且导出速度满,占用内存资源时间过长,致使一系列恶性循环。安全

如何解决

既然POI导出有这些不足之处,如何解决这样的问题呢?思路很简单,再也不使用POI导出。下降服务端资源占用。后端服务能够只查询JSON数据,导出的工做交给客户端,这样彻底屏蔽掉了使用POI导出的问题,能够想象,这样作就是一个简单的restful列表查询接口。bash

具体实现

思路

  • js使用JSON数据写Excel文件。能够使用SheetJS。使用仍是比较简单的,前端看看demo就实现了。
  • 后端restful接口分批吐数据,如同分页查询,一次性吐1000条数据,后端全部数据吐完以后,前端处理合并数据再写Excel。

后端实现

  • 咱们使用一个uuid来标志一次导出,这个uuid做为key存放在redis,而且设置过时时间,对应的value存放导查询次数,用户在点击导出按钮的时候,先使用一个接口申请这个uuid,服务端存储这个uuid。
String uuid = UUID.randomUUID().toString();
RedisAtomicLong counter = new RedisAtomicLong(ORDER_EXPORT_UUID + uuid, cacheRedisTemplate.getConnectionFactory());
counter.set(0);
counter.expire(60, TimeUnit.SECONDS);
复制代码
  • 导出接口每次请求须要使用uuid做为参数,后端再去redis读取uuid对应value,这也是一个分页设计,这一次查询就只吐对应页的数据就能够了。接口返回的数据,每次返回额外给前端一个字段用来标示,是否还有下一页数据(判断当前查询的list是否小于1000,小于说明数据已经查询完毕)。
public class ExportDTO<T> {
    private Boolean HasNext = true;
    private T data;
}
复制代码
  • 具体实例代码
public boolean existKey(String uuid) {
    return cacheRedisTemplate.getExpire(ORDER_EXPORT_UUID + uuid) > 0;
}

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<SalesOrderExportDTO> getSaleOrderData(String uuid, ExportOrderQueryDTO orderQuery, boolean showPhoneNumFlag) {
    if (!existKey(uuid)) {
        return null;
    }

    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);
}
复制代码

改造后的效果

  • 下图是使用js的导出效果图,能够看出和POI导出的差别有多大。
  • 生产环境服务器内存变化图

通过这么多天的线上应用内存观察,前端导出Excel的有点真的是毋庸置疑,减轻了后端服务的压力,后端服务性能飙升。服务器

相关文章
相关标签/搜索