最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

java導(dǎo)出Excel實(shí)現(xiàn)八倍效率優(yōu)化的方法詳解

 更新時(shí)間:2025年11月28日 09:58:33   作者:稻草人2222  
這篇文章主要為大家詳細(xì)介紹了java導(dǎo)出Excel實(shí)現(xiàn)八倍效率優(yōu)化的相關(guān)知識,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下

背景

老后臺系統(tǒng)的數(shù)據(jù)記錄導(dǎo)出Excel功能被財(cái)務(wù),運(yùn)營吐槽難用有時(shí)候甚至用不了,我負(fù)責(zé)重構(gòu)老后臺系統(tǒng)代碼,剛好把Excel導(dǎo)出功能重新優(yōu)化設(shè)計(jì)一下。接下來我會(huì)分析當(dāng)前問題,針對優(yōu)化性能,進(jìn)行代碼分層解藕實(shí)現(xiàn)優(yōu)雅封裝 盡可能在提高10秒同步流返回最大的記錄條數(shù)

關(guān)鍵字: ES滾動(dòng)查詢,讀寫分離,阻塞隊(duì)列,生產(chǎn)消費(fèi)模型,工廠模式與策略接口,抽象模板與資源控制

原代碼邏輯

     public ResponseEntity export(PrizeDto param) {
            param.setPageNum(1);
            param.setPageSize(10000);
            ESPageEntity<ESPrizeVo> result = null;
            try {
                result = esUtil2.query(authCode, indexName, fuzzyQueryOrder, fuzzyQueryOrderVersion, param, ESPrizeVo.class);
            } catch (Throwable ex) {
                log.error(ex.getMessage(), ex);
            }
            ExcelUtil<ESPrizeVo> util = new ExcelUtil<>(ESPrizeVo.class);
            List<ESPrizeVo> list = JSON.parseArray(JSON.toJSONString(result.getList()), ESPrizeVo.class);
            return util.exportExcel(list, "數(shù)據(jù)記錄", null);
        }
        
        //util.exportExcel 基本與若以框架自帶的ExcelUtils一致

這一看,代碼確實(shí)簡單粗暴,因?yàn)閿?shù)據(jù)是存在ES庫中的,公司的ES庫 分頁搜索最多支持到第一萬條數(shù)據(jù),所以代碼簡單粗暴的直接一次查1w條

缺點(diǎn)可以說相當(dāng)大,首先導(dǎo)出數(shù)據(jù)不支持一萬條以上的數(shù)量,第二就是整個(gè)效率低下,查詢與寫記錄是同步阻塞。第三就是內(nèi)存還有待優(yōu)化,一個(gè)list 最大情況下會(huì)保存一萬條記錄。

光1W條記錄導(dǎo)出用了15s,效率實(shí)在太低

用戶的訴求是希望能夠?qū)С?W條左右的數(shù)據(jù),接下來開始進(jìn)行優(yōu)化分析

優(yōu)化點(diǎn)

1.首先要突破1W條記錄數(shù)量限制需要使用ES的滾動(dòng)查詢,同時(shí)需要確定好沒次滾動(dòng)的數(shù)量,太長會(huì)導(dǎo)致單次查詢較慢,同時(shí)list元素過多占用堆內(nèi)存過多,滾動(dòng)數(shù)量太少則會(huì)增加滾動(dòng)次數(shù),整體效率慢

2.優(yōu)化整體效率,因?yàn)槭褂昧藵L動(dòng)查詢,那么可以進(jìn)行讀寫分離,每次滾動(dòng)的數(shù)據(jù)交給異步線程池進(jìn)行異步的寫Excel。

3.優(yōu)化內(nèi)存空間占用,這里主要涉及到讀寫分離時(shí)中間的緩沖池的容量大小,這個(gè)容量最好能過做到寫數(shù)據(jù)不需要空等待數(shù)據(jù)進(jìn)來,生產(chǎn)數(shù)據(jù)不會(huì)出現(xiàn)緩存池滿了阻塞的情況。還有一個(gè)寫Excel操作的配置,配置SXSSFWorkbookrowAccessWindowSize 屬性為每次滾動(dòng)查詢的大小,同時(shí)我還優(yōu)化了ES的查詢模板,減少了運(yùn)營與財(cái)務(wù)不需要的數(shù)據(jù)字段,大幅降低了單條數(shù)據(jù)的大小

整體設(shè)計(jì)

先上架構(gòu)圖:

可以看到我通過數(shù)據(jù)范圍的大小分為了兩種處理器,一種是同步流返回處理器,面對數(shù)據(jù)量較小范圍的數(shù)據(jù)進(jìn)行同步導(dǎo)出,而數(shù)據(jù)量較大的則需要完全異步后端處理,前端再輪訓(xùn)或者后端主動(dòng)通知的方法告知其文件下載。(1.5w 其實(shí)可以調(diào)整至5w,能過實(shí)現(xiàn)5秒處理)

這兩種處理器的處理邏輯其實(shí)是類似的,全部都采用了生產(chǎn)消費(fèi)模型,實(shí)現(xiàn)讀寫異步的功能。在代碼上封裝統(tǒng)一提交任務(wù)與消費(fèi)數(shù)據(jù)兩個(gè)主要功能。

代碼細(xì)節(jié)

ES 滾動(dòng)查詢封裝

es滾動(dòng)主要分為三個(gè)步驟,第一個(gè)步驟是 發(fā)送第一次滾動(dòng)查詢請求,ES內(nèi)部會(huì)生成一份查詢數(shù)據(jù)快照。API: POST /<index_name>/_search?scroll=<timeout> timeout 是這個(gè)快照保存的時(shí)間,可以設(shè)置為1m 即一分鐘,請求體需要有一個(gè)size,即每次滾動(dòng)的大小。請求返回一個(gè)scrollID,以及第一次滾動(dòng)查詢的數(shù)據(jù)!

{
  "size": 100,       // 每次滾動(dòng)返回的文檔數(shù)量
  "query": {
    "match_all": {}  // 你的查詢條件,可以是任何復(fù)雜的查詢
  },
  "sort": ["_doc"]   // 【最佳實(shí)踐】使用 `_doc` 排序以獲得最高效的檢索性能
}

第二個(gè)步驟是開始滾動(dòng)查詢數(shù)據(jù),API: POST /_search/scroll 你需要使用第一步返回的scrollId 作為請求體去請求,不需要攜帶查詢模板。這個(gè)步驟會(huì)再返回一個(gè)新的scrollId,然后在用新的scrollID 重復(fù)步驟二即開始滾動(dòng)數(shù)據(jù),需要注意此步驟不可并發(fā),即使有些獲取的scrollId是相同的。

{
  "scroll": "1m",    // 【重要】可以重新設(shè)置 scroll 的超時(shí)時(shí)間,重置倒計(jì)時(shí)
  "scroll_id": "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WYm9laVYtZndUQlNsdDcwakFMNjU1QQ=="
}

刪除查詢快照,在滾動(dòng)獲取數(shù)據(jù)為空的時(shí)候需要去請求ES刪除這次的查詢快照,如果不請求,那么會(huì)在設(shè)置的timeout超時(shí)時(shí)間后自動(dòng)刪除。API: DELETE /_search/scroll

封裝初始化滾動(dòng)查詢,以及后續(xù)的滾動(dòng),使用TransmittableThreadLocal存儲scrollId

public class ESScrollUtil extends ESUtil implements EsSearch {
?
    @Value("${es.xxx.order.authCode}")
    private String authCode;
?
    @Value("${es.xxx.order.indexName}")
    private String indexName;
?
    public static final TransmittableThreadLocal<String> localScrollId = new TransmittableThreadLocal<>();
  
    // 每次滾動(dòng)完進(jìn)行TL清除
    @Override
    public void cleanLocalScrollId() {
        localScrollId.remove();
    }
?
    public <R extends BaseESResult> ScrollRes<R> getFirstScrollId(Class<R> clazz, String templateName, String templateVersion, Object body) {
        ParameterizedTypeImpl type = new ParameterizedTypeImpl(new Type[]{clazz}, null, ESPageEntity.class);
        ScrollRes<R> baseESResultScrollRes = super.queryForScroll(authCode, indexName, templateName, templateVersion, body, type);
        localScrollId.set(baseESResultScrollRes.getScrollId());
        return baseESResultScrollRes;
    }
?
   
    public <R extends BaseESResult> ScrollRes<R> nextScrollPage(Class<R> clazz){
        if (StringUtilsExt.isEmpty(localScrollId.get())) {
            log.error("請先調(diào)用getFirstScrollId方法獲取scrollId");
            throw new CustomException("請先調(diào)用getFirstScrollId方法獲取scrollId");
        }
        ParameterizedTypeImpl type = new ParameterizedTypeImpl(new Type[]{clazz}, null, ESPageEntity.class);
        ScrollRes<R> res = super.queryForScrollNextPage(authCode, indexName, localScrollId.get(), type);
        localScrollId.set(res.getScrollId());
        return res;
    }
}

這樣在業(yè)務(wù)處理代碼只需要執(zhí)行 getFirstScrollId 然后 while 執(zhí)行 nextScrollPage 不需要處理scrollId賦值的問題

基本Excel導(dǎo)出處理夫類,封裝滾動(dòng)任務(wù)執(zhí)行以及Excel寫入任務(wù),內(nèi)部使用阻塞隊(duì)列實(shí)現(xiàn)生產(chǎn)消費(fèi)模型

@Slf4j
public class BaseExcelExporter {
    // clazz緩存, key為類名, value為字段名和注解的map
    protected static final HashMap<Class<?>, List<Excel>> cacheExcelInfo = new HashMap<>();
?
    // 解析Excel緩存, key為 className + "_" + 注解name, value為字段
    protected static final HashMap<String,Field> cacheField = new HashMap<>();
?
    protected static ExecutorService executorService;
?
    static {
        ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
        // 兩倍最大并發(fā)數(shù)滿足了最大處理情況(單次oss處理器需要兩個(gè)線程),需要使用包裝Ttl線程池,繼承ScrollID
        threadPoolTaskExecutor.setCorePoolSize(MAX_CONCURRENCY*2);
        threadPoolTaskExecutor.setMaxPoolSize(MAX_CONCURRENCY*2);
        threadPoolTaskExecutor.setQueueCapacity(0);
        threadPoolTaskExecutor.setThreadNamePrefix("excel-exporterThread-");
        threadPoolTaskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
        threadPoolTaskExecutor.initialize();
        executorService = TtlExecutors.getTtlExecutorService(threadPoolTaskExecutor.getThreadPoolExecutor());
    }
    /**
     * 提交ES查詢?nèi)蝿?wù),將查詢結(jié)果放入阻塞隊(duì)列,異?;虿樵償?shù)據(jù)為空時(shí)將空數(shù)據(jù)放入隊(duì)列
     * @param task 滾動(dòng)查詢?nèi)蝿?wù)
     * @param taskResQueue 結(jié)果阻塞隊(duì)列
     * @param <R> 結(jié)果類型
     */
    protected <R extends BaseESResult> void commitEsQueryTask(Supplier<ScrollRes<R>> task,
                                                              BlockingQueue<List<R>> taskResQueue){
        try {
            executorService.submit(() -> {
                try {
                    for (;;){
                        ScrollRes<R> taskResult = task.get();
                        List<R> res = taskResult.getRes();
                        if(res.isEmpty()){
                            log.debug("任務(wù)已完成,退出循環(huán)");
                            taskResQueue.put(new ArrayList<>());
                            return;
                        }
                        taskResQueue.put(res);
                    }
                }catch (Exception e) {
                    log.error("ES滾動(dòng)查詢出現(xiàn)異常", e);
                }
            });
        } catch (RejectedExecutionException e) {
            log.error("Excel導(dǎo)出線程池已滿,無法提交任務(wù)", e);
            throw e;
        }
    }
?
    protected <R extends BaseESResult> void syncWriteTaskResult(String handlerName, Class<R> clazz,
                                                                BlockingQueue<List<R>> dataQueue,
                                                                Sheet sheet, CellStyle dateCellStyle){
        long start;
        try {
            for (;;){
                List<R> taskRes = dataQueue.poll(9, TimeUnit.SECONDS);
                if(taskRes == null){
                    log.error("【{}】獲取阻塞隊(duì)列數(shù)據(jù)超時(shí)! 線程退出阻塞等待", handlerName);
                    break;
                }
                log.info("主線程獲取到阻塞隊(duì)列數(shù)據(jù),數(shù)據(jù)大小:{} 當(dāng)前隊(duì)列剩余數(shù)據(jù)批數(shù):{}",taskRes.size(), dataQueue.size());
                // 獲取的list為空則結(jié)束了(可能任務(wù)執(zhí)行異常了)
                if(taskRes.isEmpty()){
                    log.info("Excel導(dǎo)出任務(wù)已完成,主線程退出循環(huán)");
                    break;
                }
                // 寫入數(shù)據(jù)
                start = System.currentTimeMillis();
                writeRows(sheet, taskRes, clazz, dateCellStyle);
                log.info("主線程寫入數(shù)據(jù)耗時(shí):{} ms", System.currentTimeMillis() - start);
            }
        } catch (InterruptedException e) {
            log.error("【{}】 消費(fèi)線程被中斷,線程池繁忙", handlerName, e);
            throw new CustomException("系統(tǒng)導(dǎo)出繁忙中,請稍后重試");
        }
    }
?
    public void analysisExcelAnno(Class<?> clazz){
        List<Excel> excels = cacheExcelInfo.computeIfAbsent(clazz, k -> new ArrayList<>());
        //獲取字段上的注解
        Field[] declaredFields = clazz.getDeclaredFields();
        for (Field declaredField : declaredFields) {
            declaredField.setAccessible(true);
            Excel excel = declaredField.getAnnotation(Excel.class);
            if (excel != null) {
                excels.add(excel);
                cacheField.put(clazz.getSimpleName()+"_"+excel.name(), declaredField);
            }
        }
    }
?
    /**
     * 獲取excel表頭, 值為 {@link Excel} 注解的 name 屬性, 順序與字段順序一致
     * 如果是第一次導(dǎo)出該類則會(huì)進(jìn)行 {@link #analysisExcelAnno(Class)} 解析
     * @param clazz 待導(dǎo)出的類
     * @return 表頭
     */
    public String[] getExcelHeader(Class<?> clazz){
        List<Excel> excels = cacheExcelInfo.getOrDefault(clazz, null);
        if (excels == null) {
            analysisExcelAnno(clazz);
            excels = cacheExcelInfo.get(clazz);
        }
        return excels.stream().map(Excel::name).toArray(String[]::new);
    }
?
    public Field getFieldByExcelName(Class<?> clazz, String name){
        Field field = cacheField.get(clazz.getSimpleName()+"_"+name);
        if(field == null){
            throw new CustomException("未找到對應(yīng)的字段,請檢查是否執(zhí)行analysisExcelAnno(clazz) 過該類");
        }
        return field;
    }
?
    /**
     * 寫入數(shù)據(jù)行,需要注意寫入后list數(shù)據(jù)已被清空,無法再次使用
     * @param sheet 工作表
     * @param data 待寫入數(shù)據(jù)
     * @param clazz 數(shù)據(jù)類型
     * @param dateCellStyle 單元格樣式
     */
    public void writeRows(Sheet sheet, List<?> data, Class<?> clazz, CellStyle dateCellStyle){
        String[] headers = getExcelHeader(clazz);
        // 獲取當(dāng)前行數(shù),判斷是否需要?jiǎng)?chuàng)建表頭
        int currentRowNum = sheet.getPhysicalNumberOfRows();
        // 如果當(dāng)前行數(shù)為 0,說明表頭還未創(chuàng)建,需要?jiǎng)?chuàng)建表頭
        if (currentRowNum == 0) {
            Row headerRow = sheet.createRow(0); // 第 0 行作為表頭行
            for (int i = 0; i < headers.length; i++) {
                // 創(chuàng)建表頭單元格,值為注解的 name 屬性
                Cell cell = headerRow.createCell(i);
                cell.setCellValue(headers[i]);
                // 設(shè)置每個(gè)列的寬度
                Excel excel = cacheExcelInfo.get(clazz).get(i);
                sheet.setColumnWidth(i, (int) ((excel.width() + 0.72) * 256));
            }
            currentRowNum++; // 表頭占用第 0 行,所以數(shù)據(jù)從第 1 行開始
        }
?
        // 寫入數(shù)據(jù)行
        for (Object item : data) {
            Row dataRow = sheet.createRow(currentRowNum++); // 從當(dāng)前行開始寫入數(shù)據(jù)
            for (int j = 0; j < headers.length; j++) {
                Cell cell = dataRow.createCell(j); // 創(chuàng)建每列單元格
                Field field = getFieldByExcelName(clazz,headers[j]); // 根據(jù)表頭名稱獲取字段
                try {
                    Object value = field.get(item); // 獲取字段值
                    if (value instanceof Date) {
                        // 如果是日期類型,設(shè)置日期值和樣式
                        cell.setCellValue((Date) value);
                        cell.setCellStyle(dateCellStyle);
                    } else {
                        // 其他類型直接轉(zhuǎn)換為字符串
                        cell.setCellValue(value == null ? "" : value.toString());
                    }
                } catch (IllegalAccessException e) {
                    log.error("獲取字段值異常", e);
                    throw new CustomException("獲取字段值異常");
                }
            }
        }
        // 顯式清空輔助jvm回收
        data.clear();
    }
}

Excel導(dǎo)出接口,對Excel handler 的抽象接口

    public interface ExcelExportHandler {
        // 導(dǎo)出接口,執(zhí)行此方法即完成了excel導(dǎo)出
        public <R extends BaseESResult> ExcelHandleRes export(Class<R> clazz, ScrollRes<R> firstScroll, Supplier<ScrollRes<R>> nextScroll, SXSSFWorkbook workbook, CellStyle dateCellStyle);
        // 判斷該處理器是否支持處理該滾動(dòng)查詢,這里通過res的count即文檔數(shù)量來選擇同步返回流處理器還是Oss異步處理器
        public Boolean support(ScrollRes<?> scrollRes);
    }
    ?

同步流Excel導(dǎo)出處理器-簡單導(dǎo)出處理器

    ?
    /**
     * 簡單Excel導(dǎo)出處理器,支持導(dǎo)出數(shù)據(jù)量小于40000的數(shù)據(jù)。
     * 主線程寫入數(shù)據(jù),子線程請求ES進(jìn)行滾動(dòng)查詢,提高導(dǎo)出效率。
     */
    @Service
    @Slf4j
    public class SampleExportHandler extends BaseExcelExporter implements ExcelExportHandler {
    ?
        @Override
        public <R extends BaseESResult> ExcelHandleRes export(Class<R> clazz, ScrollRes<R> firstScroll,
                                                              Supplier<ScrollRes<R>> nextScroll, SXSSFWorkbook workbook,
                                                              CellStyle dateCellStyle) {
            ServletOutputStream outputStream = null;
            try {
                Sheet sheet = workbook.createSheet("Sheet1");
                // 設(shè)置一個(gè)較小值,size 即為緩沖池的滾動(dòng)批次大小,這里最多允許兩個(gè)滾動(dòng)批次結(jié)果存在隊(duì)列中
                BlockingQueue<List<R>> dataQueue = new LinkedBlockingQueue<>(2);
                // 提交Es滾動(dòng)查詢?nèi)蝿?wù)至線程池中
                commitEsQueryTask(nextScroll, dataQueue);
                // 后寫入第一次滾動(dòng)的數(shù)據(jù)
                writeRows(sheet, firstScroll.getRes(), clazz, dateCellStyle);
                // 主線程消費(fèi)阻塞隊(duì)列的寫入數(shù)據(jù)
                syncWriteTaskResult("SampleExcel導(dǎo)出處理器",clazz, dataQueue, sheet, dateCellStyle);
                HttpServletResponse response = getResponse();
                // 獲取輸出流
                outputStream = response.getOutputStream();
                // 將工作簿寫入輸出流
                workbook.write(outputStream);
                // 刷新輸出流
                outputStream.flush();
                return new ExcelHandleRes(true);
            } catch (Exception e){
                log.error("導(dǎo)出 Excel 時(shí)發(fā)生異常",e);
                return new ExcelHandleRes(false);
            }
            finally {
                if(outputStream != null){
                    try {
                        outputStream.close();
                    } catch (Exception e) {
                        log.error("outputStream 關(guān)閉時(shí)發(fā)生異常, ", e);
                    }
                }
            }
        }
    ?
        private HttpServletResponse getResponse() {
            HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
            response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
            try {
                response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode("export.xlsx", "UTF-8"));
            } catch (UnsupportedEncodingException e) {
                throw new RuntimeException(e);
            }
            return response;
        }
    ?
        @Override
        public Boolean support(ScrollRes<?> scrollRes) {
            return scrollRes.getCount() <= 40000;
        }
    ?
    }
    ?

兩個(gè)處理器的代碼是相似的,這里就不重復(fù)寫了。小細(xì)節(jié)是先去提交滾動(dòng)查詢?nèi)蝿?wù)至線程池中然后再去寫入第一次滾動(dòng)數(shù)據(jù),如果順序相反則第一次寫入數(shù)據(jù)是阻塞讀任務(wù)的。

導(dǎo)出工廠,通過第一次滾動(dòng)查詢的結(jié)果數(shù)量 調(diào)用excelExportHandler.support 找到合適的處理器

    @Component
    public class ExcelExportHandlerFactory {
    ?
        @Autowired
        private List<ExcelExportHandler> handlers;
    ?
        public ExcelExportHandler buildHandler(ScrollRes<?> scrollRes) {
            for (ExcelExportHandler handler : handlers) {
                if (handler.support(scrollRes)) {
                    return handler;
                }
            }
            return null;
        }
    }

Exccel 導(dǎo)出工具, 直接用于service 調(diào)用

    public class LargeExcelUtil implements ApplicationContextAware {
        private static ExcelExportHandlerFactory excelExportHandlerFactory;
    ?
        public static final int MAX_CONCURRENCY = 2;
        // 最大并發(fā)數(shù)量
        private static final AtomicInteger atomicInteger = new AtomicInteger(MAX_CONCURRENCY);
    ?
        /**
         * 一定要設(shè)置分頁大??!
         * @param clazz 返回結(jié)果的類型,必須使用 {@link Excel} 注解
         * @param esSearch es查詢工具類
         * @param templateName es查詢模板名稱
         * @param templateVersion es查詢模板版本
         * @param body es查詢參數(shù)
         * @return
         * @param <R>
         */
        public static <R extends BaseESResult> ExcelHandleRes export(Class<R> clazz, EsSearch esSearch, String templateName, String templateVersion, Object body, Integer pageSize) {
            if(atomicInteger.decrementAndGet() < 0){
                atomicInteger.incrementAndGet();
                throw new CustomException("系統(tǒng)導(dǎo)出繁忙中,請稍后重試");
            }
            // 通過第一次滾動(dòng)獲取scrollID以及滾動(dòng)數(shù)據(jù)量再通過數(shù)據(jù)量獲取處理器來執(zhí)行導(dǎo)出
            ScrollRes<R> firstScrollId = esSearch.<R>getFirstScrollId(clazz, templateName, templateVersion, body);
            ExcelExportHandler excelExportHandler = excelExportHandlerFactory.buildHandler(firstScrollId);
            if (excelExportHandler == null) {
                throw new RuntimeException("待導(dǎo)出數(shù)據(jù)太多了,暫不支持的導(dǎo)出");
            }
            SXSSFWorkbook workbook = null;
            try {
                // 創(chuàng)建 SXSSFWorkbook工作簿,內(nèi)存最大保存pageSize個(gè)數(shù)據(jù), 開啟壓縮臨時(shí)文件減少磁盤空間,但不要開啟使用共享字符會(huì)提高一倍多的寫入時(shí)間
                workbook = new SXSSFWorkbook(null,pageSize,true,false);
                CellStyle dateCellStyle = workbook.createCellStyle();
                // 格式化日期數(shù)據(jù)
                CreationHelper creationHelper = workbook.getCreationHelper();
                dateCellStyle.setDataFormat(creationHelper.createDataFormat().getFormat("yyyy-MM-dd HH:mm:ss"));
                return excelExportHandler.export(clazz, firstScrollId, () -> esSearch.nextScrollPage(clazz), workbook, dateCellStyle);
            }
           finally {
                // 因?yàn)槭褂昧薼ocalStorage存儲scrollID,使用完后必須清除
                esSearch.cleanLocalScrollId();
                atomicInteger.incrementAndGet();
                try {
                    if(workbook != null){
                        // 先嘗試關(guān)閉工作簿同時(shí)釋放了內(nèi)存與臨時(shí)文件
                        workbook.close();
                    }
                } catch (IOException e) {
                    log.error("SXSSFWorkbook close 失敗,錯(cuò)誤信息", e);
                }finally {
                    if(workbook != null){
                        // 如果關(guān)閉工作簿失敗則釋放臨時(shí)文件
                        workbook.dispose();
                    }
                }
            }
        }
    ?
        @Override
        public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
            excelExportHandlerFactory = applicationContext.getBean(ExcelExportHandlerFactory.class);
        }
    }

實(shí)現(xiàn)效果

機(jī)器2C4G 每次滾動(dòng)數(shù)量2000 時(shí) 寫入 與 讀取的耗時(shí)比較接近,下圖中ES滾動(dòng)查詢 平均比寫入多大概50ms

在導(dǎo)出4W條數(shù)據(jù)時(shí),接口總耗時(shí)6911ms

總結(jié) (偷個(gè)懶)

1.突破數(shù)據(jù)量限制 - 引入 ES 滾動(dòng)查詢 (Scroll API)

動(dòng)作:替換原有的簡單分頁查詢,采用 ES 滾動(dòng)查詢機(jī)制。

關(guān)鍵設(shè)計(jì)

  • 創(chuàng)建查詢快照,保證數(shù)據(jù)一致性。
  • 分批次獲取數(shù)據(jù),通過 size 參數(shù)控制每批數(shù)據(jù)量(如 2000 條),平衡單次查詢速度和網(wǎng)絡(luò)請求次數(shù)。
  • 使用 TransmittableThreadLocal 在線程池間安全地傳遞滾動(dòng) ID (scroll_id),優(yōu)雅地封裝了查詢細(xì)節(jié)。

2.提升處理效率 - 生產(chǎn)消費(fèi)模型 & 異步化

動(dòng)作:將 數(shù)據(jù)查詢(生產(chǎn))Excel 寫入(消費(fèi)) 分離。

關(guān)鍵設(shè)計(jì)

  • 生產(chǎn)者:一個(gè)后臺線程專門負(fù)責(zé)執(zhí)行滾動(dòng)查詢,將獲取到的數(shù)據(jù)批次放入一個(gè)阻塞隊(duì)列 (BlockingQueue)。
  • 消費(fèi)者:主線程(或另一個(gè)消費(fèi)者線程)從隊(duì)列中取出數(shù)據(jù)批次,異步寫入 Excel。
  • 效益:讀寫操作并行,消除了查詢等待寫入的阻塞時(shí)間,極大提升了 CPU 和 I/O 利用率。

3.優(yōu)化內(nèi)存占用 - 流式處理與緩存

動(dòng)作:避免大數(shù)據(jù)量的整體駐留。

關(guān)鍵設(shè)計(jì)

  • 使用 SXSSFWorkbook:替換傳統(tǒng)的 XSSFWorkbook,設(shè)置 rowAccessWindowSize(如 2000),實(shí)現(xiàn)了磁盤換內(nèi)存的流式寫入,極大降低了內(nèi)存壓力。
  • 精簡數(shù)據(jù)字段:優(yōu)化 ES 查詢模板,只獲取導(dǎo)出必需的字段,從源頭上減少了單條數(shù)據(jù)大小和網(wǎng)絡(luò)傳輸量。
  • 可控的隊(duì)列容量:設(shè)置合理的阻塞隊(duì)列大?。ㄈ缛萘繛?2),作為生產(chǎn)者和消費(fèi)者之間的緩沖,防止內(nèi)存堆積。

4.架構(gòu)擴(kuò)展性 - 策略模式與工廠模式

動(dòng)作:根據(jù)數(shù)據(jù)量大小智能選擇不同的處理策略。

關(guān)鍵設(shè)計(jì)

定義 ExcelExportHandler 接口:統(tǒng)一導(dǎo)出處理器的行為。

實(shí)現(xiàn)不同處理器

  • SampleExportHandler:處理數(shù)據(jù)量較小(如 ≤ 2W 條)的場景,直接同步響應(yīng)流返回文件。
  • OssExportHandler (文中提及):處理大數(shù)據(jù)量場景,異步導(dǎo)出至 OSS,前端通過輪詢或通知下載。

工廠模式自動(dòng)選擇:通過 ExcelExportHandlerFactory 根據(jù)第一次查詢的結(jié)果總數(shù),自動(dòng)選擇最合適的處理器。

5.資源保護(hù) - 限流與清理

動(dòng)作:防止系統(tǒng)過載和資源泄漏。

關(guān)鍵設(shè)計(jì)

  • 并發(fā)控制:使用原子計(jì)數(shù)器 (AtomicInteger) 限制最大同時(shí)導(dǎo)出任務(wù)數(shù),超出則友好拒絕,保護(hù)系統(tǒng)穩(wěn)定性。
  • 資源清理: finally 塊中確保關(guān)閉 SXSSFWorkbook(釋放臨時(shí)文件)和清除 ThreadLocal 中的 scroll_id(釋放 ES 服務(wù)端資源)。

優(yōu)化成果

  • 功能上:徹底打破了 1W 條的數(shù)據(jù)限制,可支持海量數(shù)據(jù)導(dǎo)出。
  • 性能上耗時(shí)從 15 秒(1W條)降低到約 7 秒(4W條) ,性能提升超過 8 倍,且吞吐量大幅增加。
  • 穩(wěn)定性上:內(nèi)存占用可控,無 OOM 風(fēng)險(xiǎn);通過線程池和隊(duì)列管理,系統(tǒng)負(fù)載更加平穩(wěn)。
  • 架構(gòu)上:代碼清晰,模塊解耦,擴(kuò)展性強(qiáng),為未來處理更復(fù)雜的導(dǎo)出需求打下了良好基礎(chǔ)。

代碼設(shè)計(jì)模式與架構(gòu)總結(jié)

1.BaseExcelExporter(公共父類) - 模板方法模式 & 資源復(fù)用

設(shè)計(jì)意圖抽取共性,封裝流程。將導(dǎo)出過程中不變的核心算法骨架(生產(chǎn)-消費(fèi)模型)與可變的實(shí)現(xiàn)細(xì)節(jié)(具體如何查詢、如何寫入)分離。

實(shí)現(xiàn)要點(diǎn)

模板方法commitEsQueryTasksyncWriteTaskResult 這兩個(gè) protected 方法定義了“提交查詢?nèi)蝿?wù)”和“消費(fèi)寫入數(shù)據(jù)”的標(biāo)準(zhǔn)流程。子類只需調(diào)用它們即可完成核心邏輯,無需關(guān)心多線程同步和隊(duì)列管理的復(fù)雜細(xì)節(jié)。

公共資源

  • 線程池:集中管理所有導(dǎo)出任務(wù)的線程資源,避免重復(fù)創(chuàng)建,方便參數(shù)調(diào)優(yōu)(如使用 TtlExecutors 解決線程池中 ThreadLocal 傳遞問題)。
  • 緩存機(jī)制cacheExcelInfocacheField 使用 HashMap 緩存了類的注解信息,通過“懶加載”機(jī)制,避免了每次導(dǎo)出都通過反射解析注解的巨大開銷,這是性能優(yōu)化的關(guān)鍵點(diǎn)之一。

好處:極大減少了子類的代碼量,確保了所有導(dǎo)出器行為的一致性,并且將性能優(yōu)化手段(緩存、線程池)集中在父類,便于維護(hù)。

2.ExcelExportHandler(接口類) - 策略模式

設(shè)計(jì)意圖定義標(biāo)準(zhǔn),開放擴(kuò)展。聲明一個(gè)通用的導(dǎo)出契約,將不同的導(dǎo)出算法(如同步流、異步OSS)抽象為不同的策略,使它們可以相互替換。

實(shí)現(xiàn)要點(diǎn)

  • export(...) 方法是策略的核心執(zhí)行方法,接收所有必要的參數(shù)(如首次查詢結(jié)果、工作簿、后續(xù)查詢的Supplier等)。
  • support(...) 方法是策略的選擇依據(jù),根據(jù)數(shù)據(jù)量等條件判斷該處理器是否適用。

好處

  • 符合開閉原則:未來如果需要增加新的導(dǎo)出方式(如導(dǎo)出為CSV、分片ZIP下載),只需實(shí)現(xiàn)新的 ExcelExportHandler 即可,無需修改現(xiàn)有任何代碼。
  • 解耦:使用方(如 LargeExcelUtil)只依賴于接口,不依賴于具體實(shí)現(xiàn),降低了系統(tǒng)各個(gè)部分的耦合度。

3.ExcelExportHandlerFactory(工廠類) - 工廠模式

設(shè)計(jì)意圖對象創(chuàng)建與使用分離。負(fù)責(zé)根據(jù)業(yè)務(wù)規(guī)則(數(shù)據(jù)量)自動(dòng)選擇并創(chuàng)建具體的策略實(shí)現(xiàn)對象。

實(shí)現(xiàn)要點(diǎn)

  • 利用 Spring 的依賴注入(@Autowired private List<ExcelExportHandler> handlers)自動(dòng)收集所有實(shí)現(xiàn)了 ExcelExportHandler 的 Bean。
  • buildHandler 方法遍歷處理器列表,通過調(diào)用每個(gè)處理器的 support 方法來找到最合適的那個(gè)。

好處

  • 簡化客戶端代碼:使用方(LargeExcelUtil.export)無需知道有哪些處理器,也無需寫一堆 if-else 來判斷,只需調(diào)用工廠方法即可獲得合適的處理器。代碼非常簡潔和清晰。
  • 集中管理:所有處理器的選擇邏輯都集中在工廠里,規(guī)則變化只需修改一處。

4.LargeExcelUtil(Utils類) - 外觀模式 & 資源管理

設(shè)計(jì)意圖提供簡潔的高層接口,整合復(fù)雜子系統(tǒng)。它不再是傳統(tǒng)的靜態(tài)工具類,而是一個(gè)集成了復(fù)雜流程和資源管理的門戶類(Facade) 。

實(shí)現(xiàn)要點(diǎn)

外觀模式:對外提供一個(gè)非常簡單的 export 靜態(tài)方法,內(nèi)部卻整合了參數(shù)校驗(yàn)、并發(fā)控制、ES查詢初始化、處理器工廠、工作簿創(chuàng)建、資源清理等一整套復(fù)雜流程。對調(diào)用者而言,導(dǎo)出功能變得非常簡單。

資源管理

  • 并發(fā)控制:使用 AtomicInteger 實(shí)現(xiàn)了一個(gè)簡單的令牌桶限流器,是系統(tǒng)穩(wěn)定性的重要保障。
  • 資源清理:在 finally 塊中嚴(yán)格保證了 workbook.close()/dispose()scrollId 的清理,避免了內(nèi)存泄漏和資源懸掛,體現(xiàn)了良好的編程習(xí)慣。

實(shí)現(xiàn)了 ApplicationContextAware:這是一個(gè)巧妙的設(shè)計(jì),讓這個(gè)靜態(tài)工具類能夠獲取到 Spring 容器中的 Bean(ExcelExportHandlerFactory),解決了靜態(tài)方法無法直接注入 Spring Bean 的問題。

5.ESScrollUtil(Utils類) - 職責(zé)單一 & 封裝

設(shè)計(jì)意圖封裝復(fù)雜細(xì)節(jié),提供友好API。將ES滾動(dòng)查詢的三個(gè)步驟(初始化、滾動(dòng)、清理)及其資源管理(TransmittableThreadLocal)封裝起來。

實(shí)現(xiàn)要點(diǎn)

總結(jié):架構(gòu)圖景

  • 提供了 getFirstScrollIdnextScrollPage 兩個(gè)核心方法,內(nèi)部處理了 scroll_id 的存儲和傳遞,讓業(yè)務(wù)代碼可以像使用迭代器一樣簡單地進(jìn)行滾動(dòng)查詢。
  • 職責(zé)非常單一,就是管理ES滾動(dòng)查詢,符合單一職責(zé)原則。

總結(jié):架構(gòu)圖景

  • 調(diào)用層 (Service) -> 門戶層 (LargeExcelUtil):提供簡單接口,負(fù)責(zé)流程編排和資源管理。
  • 門戶層 -> 策略工廠 (ExcelExportHandlerFactory):根據(jù)上下文選擇策略。
  • 策略工廠 -> 具體策略 (SampleExportHandler等):執(zhí)行特定算法。
  • 具體策略 -> 抽象模板 (BaseExcelExporter):復(fù)用基礎(chǔ)流程和組件。
  • 抽象模板 -> 工具層 (ESScrollUtil):調(diào)用更底層的技術(shù)組件。

以上就是java導(dǎo)出Excel實(shí)現(xiàn)八倍效率優(yōu)化的方法詳解的詳細(xì)內(nèi)容,更多關(guān)于java導(dǎo)出Excel的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • Java Spring之@Async原理案例詳解

    Java Spring之@Async原理案例詳解

    這篇文章主要介紹了Java Spring之@Async原理案例詳解,本篇文章通過簡要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下
    2021-09-09
  • Spring Boot Mybatis++ 2025詳解

    Spring Boot Mybatis++ 2025詳解

    文章介紹了三種基于注解SQL和查詢接口的MyBatis使用方式,討論了Entity和Example的區(qū)別,即Entity會(huì)更新所有字段,而Example僅更新非空字段,感興趣的朋友一起看看吧
    2025-02-02
  • 工作中禁止使用Executors快捷創(chuàng)建線程池原理詳解

    工作中禁止使用Executors快捷創(chuàng)建線程池原理詳解

    這篇文章主要為大家介紹了工作中禁止使用Executors快捷創(chuàng)建線程池原理詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-11-11
  • Rabbitmq在死信隊(duì)列中的隊(duì)頭阻塞問題及解決

    Rabbitmq在死信隊(duì)列中的隊(duì)頭阻塞問題及解決

    死信隊(duì)列是RabbitMQ處理無法正常消費(fèi)消息的核心機(jī)制,但隊(duì)頭阻塞(Head-of-LineBlocking)是其高頻踩坑點(diǎn),本文從成因、場景、危害、解決方案全維度解析該問題
    2026-01-01
  • Java并發(fā)編程之StampedLock鎖介紹

    Java并發(fā)編程之StampedLock鎖介紹

    這篇文章主要介紹了Java并發(fā)編程之StampedLock鎖,StampedLock是并發(fā)包里面JDK8版本新增的一個(gè)鎖,下文更多相關(guān)內(nèi)容需要的小伙伴可以參考一下
    2022-04-04
  • MySQL數(shù)據(jù)庫之Purge死鎖問題解析

    MySQL數(shù)據(jù)庫之Purge死鎖問題解析

    這篇文章主要介紹了MySQL數(shù)據(jù)庫之Purge死鎖問題解析的相關(guān)資料,需要的朋友可以參考下
    2017-11-11
  • spring boot 與kafka集成的示例代碼

    spring boot 與kafka集成的示例代碼

    這篇文章主要介紹了spring boot 與kafka集成的示例代碼,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧
    2018-04-04
  • java9的JShell小工具和編譯器兩種自動(dòng)優(yōu)化方法

    java9的JShell小工具和編譯器兩種自動(dòng)優(yōu)化方法

    這篇文章主要介紹了java9的JShell小工具和編譯器兩種自動(dòng)優(yōu)化方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下
    2019-07-07
  • Java學(xué)習(xí)之緩沖流的原理詳解

    Java學(xué)習(xí)之緩沖流的原理詳解

    為了提高其數(shù)據(jù)的讀寫效率,Java中又定義了四種緩沖流,分別是:字節(jié)緩沖輸入流、字節(jié)緩沖輸出流、字符緩沖輸入流和字符緩沖輸出流。本文主要來和大家聊聊這些緩沖流的原理,希望對大家有所幫助
    2023-01-01
  • Java線程池的優(yōu)點(diǎn)及池化技術(shù)的應(yīng)用

    Java線程池的優(yōu)點(diǎn)及池化技術(shù)的應(yīng)用

    這篇文章主要介紹了Java線程池的優(yōu)點(diǎn)及池化技術(shù)的應(yīng)用,Java種提高程序的執(zhí)行效率有兩種實(shí)現(xiàn)方法,一個(gè)是使用線程、另一個(gè)是使用線程池,下文我們就來具體介紹該詳細(xì)內(nèi)容吧,需要的小伙伴可以參考一下
    2022-05-05

最新評論

七台河市| 庄浪县| 拉萨市| 榆中县| 怀柔区| 临海市| 凉山| 大渡口区| 开化县| 肥西县| 皮山县| 宁远县| 松阳县| 磴口县| 彭泽县| 雷波县| 博湖县| 中宁县| 响水县| SHOW| 旬阳县| 安泽县| 海淀区| 德格县| 黔西县| 会泽县| 南平市| 新蔡县| 东阿县| 桑植县| 祁东县| 兴国县| 馆陶县| 和静县| 崇仁县| 调兵山市| 新巴尔虎左旗| 清原| 皮山县| 左云县| 象州县|