放棄使用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的有點真的是毋庸置疑,減輕了後端服務的壓力,後端服務性能飆升。服務器

相關文章
相關標籤/搜索