偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

Easy Excel 高效導(dǎo)出 30w 數(shù)據(jù)至 Excel 文件的實(shí)戰(zhàn)指南

開(kāi)發(fā)
本文的需求是將一個(gè)百萬(wàn)數(shù)據(jù)量MySQL8的數(shù)據(jù)導(dǎo)出到excel的功能,經(jīng)查閱資料并結(jié)合實(shí)際場(chǎng)景需求整理出這樣一套比較精簡(jiǎn)且使用的導(dǎo)出方案。

最近easy excel停止維護(hù),筆者也看到自己早期寫(xiě)的關(guān)于基于easy excel導(dǎo)出的文章,于是打算重新整理梳理一下,當(dāng)初對(duì)于這個(gè)工具類(lèi)的使用技巧。

本文的需求是將一個(gè)百萬(wàn)數(shù)據(jù)量MySQL8的數(shù)據(jù)導(dǎo)出到excel的功能,經(jīng)查閱資料并結(jié)合實(shí)際場(chǎng)景需求整理出這樣一套比較精簡(jiǎn)且使用的導(dǎo)出方案。

文章主脈絡(luò)如下,筆者會(huì)通過(guò)一個(gè)簡(jiǎn)單的需求案例提出設(shè)計(jì)思路和需要解決的問(wèn)題點(diǎn),然后結(jié)合一段完整的代碼示例供讀者參考,從而幫助讀者更好的解決這類(lèi)問(wèn)題。

一、簡(jiǎn)述需求

為了更好演示筆者的方案,這里給出一個(gè)演示的需求,該需求是要求導(dǎo)出一個(gè)用戶(hù)表的數(shù)據(jù),該數(shù)據(jù)表是一張用戶(hù)表,包含id和name,該用戶(hù)表數(shù)據(jù)量在300w左右,以自增id作為主鍵,而功能要求我們?cè)谝环昼娭畠?nèi)完成百萬(wàn)數(shù)據(jù)導(dǎo)出到excel。需要注意的是,我們導(dǎo)出的excel格式為xlsx,它的每一個(gè)sheet只能容納100w的數(shù)據(jù),這也就意味著我們的數(shù)據(jù)必須以100w作為批次寫(xiě)到不同的sheet中。

CREATE TABLE `t_user` (
  `id` bigint NOT NULL,
  `name` varchar(100) DEFAULT NULL,
  `count` int DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;

二、設(shè)計(jì)思路

1. 三個(gè)問(wèn)題

我們先來(lái)說(shuō)說(shuō)需要解決的問(wèn)題:

  • 如果一次性查詢(xún)300w左右的數(shù)據(jù)可能會(huì)占據(jù)大量的內(nèi)存,如果對(duì)象字段很多的情況下,很可能出現(xiàn)內(nèi)存溢出,我們要如何解決?
  • 每個(gè)excel文件都有sheet,并且每個(gè)sheet只能容納100w左右的數(shù)據(jù),對(duì)于這個(gè)問(wèn)題我們要如何解決?
  • 數(shù)據(jù)寫(xiě)入到excel時(shí),有沒(méi)有合適的工具推薦?

2. 解決方案

(1) 解決大表查詢(xún)問(wèn)題

對(duì)于問(wèn)題1我們兩套解決方案: 方案1是采用分頁(yè)查詢(xún)的方式進(jìn)行查詢(xún),參考自己堆內(nèi)存的配置推算每次分頁(yè)查詢(xún)的數(shù)據(jù)量。因?yàn)閱?wèn)題1采用了分頁(yè)查詢(xún),我們完全可以通過(guò)分頁(yè)查詢(xún)的次數(shù)推算出一個(gè)sheet寫(xiě)入了多少數(shù)據(jù),例如我們每次分頁(yè)查詢(xún)50w的數(shù)據(jù),那么每?jī)纱尉涂梢砸暈橐粋€(gè)sheet寫(xiě)滿(mǎn)了,我們就可以創(chuàng)建一個(gè)新的sheet寫(xiě)入數(shù)據(jù)。

這里需要注意一點(diǎn),因?yàn)槲覀兎猪?yè)查詢(xún)面對(duì)的是百萬(wàn)級(jí)別的數(shù)據(jù),所以隨著分頁(yè)的推進(jìn)勢(shì)必出現(xiàn)深分頁(yè)導(dǎo)致查詢(xún)效率勢(shì)降低,所以為了提高分頁(yè)查詢(xún)的效率,我們可以利用查詢(xún)數(shù)據(jù)有序的特性,通過(guò)id作為偏移進(jìn)行分頁(yè)查詢(xún)。

例如我們第一次分頁(yè)查詢(xún)的sql語(yǔ)句為:

select * from t_user limit 500000 ;

假如我們不以id作為索引,那么第二次的分頁(yè)查詢(xún)sql則是:

select * from t_user limit 500000,500000 ;

查看該查詢(xún)執(zhí)行計(jì)劃,可以看到該查詢(xún)一次性查詢(xún)到幾乎全表的數(shù)據(jù),并且還走了全秒掃描性能可想而知:

id|select_type|table |partitions|type|possible_keys|key|key_len|ref|rows   |filtered|Extra|
--+-----------+------+----------+----+-------------+---+-------+---+-------+--------+-----+
 1|SIMPLE     |t_user|          |ALL |             |   |       |   |2993040|   100.0|     |

因?yàn)槲覀兊臄?shù)據(jù)表是id自增的,所以我們查詢(xún)的時(shí)候完全可以基于該特性通過(guò)上一次查詢(xún)到的id作為篩選條件進(jìn)行分頁(yè)查詢(xún)。

所以我們的分頁(yè)查詢(xún)可直接改為:

select * from t_user where id > 500000 limit 500000 ;

再次查看執(zhí)行計(jì)劃可以發(fā)現(xiàn)該查詢(xún)?yōu)榉秶樵?xún),查詢(xún)到的數(shù)據(jù)量也少了很多,性能顯著提升:

id|select_type|table |partitions|type |possible_keys|key    |key_len|ref|rows   |filtered|Extra      |
--+-----------+------+----------+-----+-------------+-------+-------+---+-------+--------+-----------+
 1|SIMPLE     |t_user|          |range|PRIMARY      |PRIMARY|8      |   |1496520|   100.0|Using where|

另外一種解決方案就是流式查詢(xún),通過(guò)流式查詢(xún)將SQL語(yǔ)句直接提交給MySQL服務(wù)端,讓服務(wù)端按照客戶(hù)端程序接受程度不斷推送數(shù)據(jù),然后我們的java程序每次收集50w的數(shù)據(jù),再寫(xiě)入到對(duì)應(yīng)的excel文件中:

(2) 選擇合適的導(dǎo)出工具

因?yàn)槭忻嫔媳容^多的excel導(dǎo)出工具,常見(jiàn)的就是Apache poi,但是它們的操作對(duì)于內(nèi)存的消耗非常嚴(yán)重,對(duì)于我們這種大數(shù)據(jù)量的寫(xiě)入不是很友好,所以筆者更推薦使用阿里的EasyExcel,它對(duì)poi進(jìn)行一定的封裝和優(yōu)化,同等數(shù)據(jù)量寫(xiě)入使用的內(nèi)存更小。

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>easyexcel</artifactId>
    <version>4.0.3</version>
</dependency>

解決上述問(wèn)題之后,我們就可以說(shuō)說(shuō)代碼實(shí)現(xiàn)思路了,以本文示例來(lái)說(shuō),有一張用戶(hù)表有300w左右的數(shù)據(jù),每次查詢(xún)時(shí)只需查詢(xún)id(4字節(jié))和name(10字節(jié)),按照64位的操作系統(tǒng)來(lái)說(shuō),一個(gè)user對(duì)象所占用的內(nèi)存大小為:

object header +pointer+id字段+name字段大小=8+8+4+10=30字節(jié)

因?yàn)閖ava對(duì)象內(nèi)存大小需要16位對(duì)齊,需要補(bǔ)齊2個(gè)字節(jié),所以實(shí)際大小為32字節(jié),按照筆者對(duì)于堆內(nèi)存的配置,每次查詢(xún)50w條數(shù)據(jù)是允許的,所以每次從數(shù)據(jù)庫(kù)讀取數(shù)據(jù)并轉(zhuǎn)為java對(duì)象,也只需要32*500000/1024即15M內(nèi)存即可。

確定每次分頁(yè)查詢(xún)50w條數(shù)據(jù)之后,我們就需要確定一共需要查詢(xún)幾個(gè)分頁(yè),然后就可以根據(jù)pageSize確定查詢(xún)的頁(yè)數(shù)。 因?yàn)槊看尾樵?xún)50w條數(shù)據(jù),所以每?jī)纱瓮瓿煞猪?yè)查詢(xún)和寫(xiě)入基本上一個(gè)sheet就會(huì)滿(mǎn)了,這時(shí)候我們就需要?jiǎng)?chuàng)建一個(gè)新的sheet進(jìn)行數(shù)據(jù)寫(xiě)入了。

2. 思路小結(jié)

總結(jié)一下實(shí)現(xiàn)步驟:

  • 查詢(xún)目標(biāo)數(shù)據(jù)量大小。
  • 根據(jù)每次分頁(yè)大小確定查詢(xún)頁(yè)數(shù)(或使用流式查詢(xún))。
  • 根據(jù)頁(yè)數(shù)大小進(jìn)行遍歷,進(jìn)行分頁(yè)查詢(xún),并將數(shù)據(jù)寫(xiě)入到文件中。
  • 基于頁(yè)數(shù)確定sheet切換時(shí)機(jī)。

(1) 分頁(yè)查詢(xún)方案落地

以下便是筆者基于上述思路所實(shí)現(xiàn)的分頁(yè)查詢(xún)和導(dǎo)出的代碼:

long start = System.currentTimeMillis();
        UserMapper userMapper = SpringUtil.getBean(UserMapper.class);
        //計(jì)算總的數(shù)據(jù)量
        int count = Math.toIntExact(userMapper.selectCount(Wrappers.emptyWrapper()));


        //獲取分頁(yè)總數(shù)
        int queryCount = 50_0000;
        int pageCount = count % queryCount == 0 ? count / queryCount : count / queryCount + 1;

        log.info("pageCount: {}", pageCount);

        //設(shè)置導(dǎo)出的文件名
        String fileName = "F://tmp/result.xlsx";
        //設(shè)置excel的sheet號(hào)碼
        int sheetNo = 1;
        //設(shè)置第一個(gè)sheet的名字
        String sheetName = "sheet-" + sheetNo;



        // 創(chuàng)建writeSheet
        WriteSheet writeSheet = EasyExcel.writerSheet(sheetNo, sheetName).build();
        //記錄每次分頁(yè)查詢(xún)的最大值
        Long maxId = null;

        //指定文件
        try (ExcelWriter excelWriter = EasyExcel.write(fileName, User.class).build()) {
            //寫(xiě)入每一頁(yè)分頁(yè)查詢(xún)的數(shù)據(jù)
            for (int i = 1; i <= pageCount; i++) {
                // 分頁(yè)去數(shù)據(jù)庫(kù)查詢(xún)數(shù)據(jù) 這里可以去數(shù)據(jù)庫(kù)查詢(xún)每一頁(yè)的數(shù)據(jù)
                long queryStart = System.currentTimeMillis();

                List<User> userList = null;
                //如果是第一次則直接進(jìn)行分頁(yè)查詢(xún),反之基于上一次分頁(yè)查詢(xún)的分頁(yè)定位實(shí)際偏移量,篩選前n條數(shù)據(jù)以達(dá)到分頁(yè)效果
                PageHelper.startPage(1, queryCount, false);
                if (i == 1) {
                    userList = userMapper.selectList(Wrappers.emptyWrapper());
                } else if (maxId != null) {
                    QueryWrapper wrapper = new QueryWrapper();
                    wrapper.gt("id", maxId);//相當(dāng)于where id=1
                    userList = userMapper.selectList(wrapper);
                    PageHelper.startPage(0, queryCount, false);
                }


                //更新下一次分頁(yè)查詢(xún)用的id
                if (CollUtil.isNotEmpty(userList)) {
                    maxId = userList.get(userList.size() - 1).getId();
                    log.info("maxId: {}", maxId);
                }

                long queryEnd = System.currentTimeMillis();
                log.info("數(shù)據(jù)大小:{},寫(xiě)入sheet位置:{},耗時(shí):{}", userList.size(), sheetName, queryEnd - queryStart);

                long writeStart = System.currentTimeMillis();
                excelWriter.write(userList, writeSheet);

                long writeEnd = System.currentTimeMillis();
                log.info("本次寫(xiě)入耗時(shí):{}", writeEnd - writeStart);

                //如果% 2 == 0,則說(shuō)明一個(gè)sheet寫(xiě)入了50*2即100w的數(shù)據(jù),需要?jiǎng)?chuàng)建新的sheet進(jìn)行寫(xiě)入
                if (i % 2 == 0) {
                    sheetName = "sheet-" + (++sheetNo);
                    writeSheet = EasyExcel.writerSheet(sheetNo, sheetName).build();
                    log.info("寫(xiě)滿(mǎn)一個(gè)sheet,切換到下一個(gè)sheet:{}", sheetName);
                }
            }
        }
        long total = System.currentTimeMillis() - start;
        log.info("導(dǎo)出結(jié)束,總耗時(shí):{}", total);

可能會(huì)有讀者好奇筆者這個(gè)50w的數(shù)值設(shè)計(jì)思路是什么,除了考慮避免OOM以外,還考慮到每個(gè)sheet只能寫(xiě)入100w條的數(shù)據(jù),為了方便通過(guò)分頁(yè)查詢(xún)的輪次確定當(dāng)前寫(xiě)入的數(shù)據(jù)量大小,筆者嘗試過(guò)20w、50w。 最終在壓測(cè)結(jié)果上看出,50w讀寫(xiě)耗時(shí)雖然是20w的2倍,但是IO次數(shù)卻不到20w查詢(xún)的二分之一,通過(guò)更少的IO操作獲得更好的執(zhí)行性能。

最終300w數(shù)據(jù)導(dǎo)出耗時(shí)大約35s,整體性能表現(xiàn)還是可以的:

(2) 使用流式編程導(dǎo)出(推薦)

對(duì)應(yīng)我們也給出流式編程的導(dǎo)出方案,筆者針對(duì)查詢(xún)語(yǔ)句做了流式編程的配置,通過(guò)這些配置保證MySQL服務(wù)端基于自己的迭代游標(biāo)按照客戶(hù)端處理效率按照順序的數(shù)據(jù)流不斷傳輸給客戶(hù)端:

@Select("select * from user  ")
    @Options(resultSetType = ResultSetType.FORWARD_ONLY, fetchSize = Integer.MIN_VALUE)
    @ResultType(User.class)
    void selectListByStream(ResultHandler<User> handler);

關(guān)于流式查詢(xún)的更多內(nèi)容,建議讀者參考筆者寫(xiě)的這篇文章:《MySQL 流式查詢(xún)的奧秘與應(yīng)用解析

基于上述查詢(xún)語(yǔ)句,我們不斷拿到user對(duì)象,因?yàn)榱魇讲樵?xún)避免的頻繁的IO分頁(yè)請(qǐng)求,所以真正的寫(xiě)入瓶頸點(diǎn)在于寫(xiě)入到excel文件中,所以筆者在流式聚合數(shù)據(jù)時(shí)是通過(guò)每1w條進(jìn)行一次寫(xiě)入,保持每100w切換一次sheet。

需要注意的是因?yàn)楣P者的ResultHandler用的是lambda表達(dá)式,為了讓編譯器通過(guò)編譯所有的計(jì)數(shù)、sheet等修改操作都是通過(guò)原子類(lèi)CAS完成的,具體讀者可以參考筆者的注釋?zhuān)?/p>

long start = System.currentTimeMillis();
        UserMapper userMapper = SpringUtil.getBean(UserMapper.class);

        //設(shè)置導(dǎo)出的文件名
        String fileName = "F://tmp/result.xlsx";
        //設(shè)置excel的sheet號(hào)碼,用原子類(lèi)保證可以在lambda表達(dá)式中通過(guò)編譯
        AtomicInteger sheetNo = new AtomicInteger(1);
        //設(shè)置第一個(gè)sheet的名字,用原子類(lèi)保證可以在lambda表達(dá)式中通過(guò)編譯
        AtomicReference<String> sheetName = new AtomicReference<>("sheet-" + sheetNo.get());


        // 創(chuàng)建writeSheet,用原子類(lèi)保證可以在lambda表達(dá)式中通過(guò)編譯
        WriteSheet writeSheet = EasyExcel.writerSheet(sheetNo.get(), sheetName.get()).build();
        AtomicReference<WriteSheet> writeSheetRef = new AtomicReference<>(writeSheet);


        List<User> userList = new ArrayList<>();
        AtomicReference<List<User>> userListRef = new AtomicReference<>(userList);

        //記錄導(dǎo)出的size
        AtomicInteger atomicCount = new AtomicInteger(0);

        //指定文件
        try (ExcelWriter excelWriter = EasyExcel.write(fileName, User.class).build()) {


            userMapper.selectListByStream(res -> {
                //存入list中
                User user = res.getResultObject();
                userListRef.get().add(user);
                atomicCount.incrementAndGet();

                //50w執(zhí)行一次導(dǎo)出寫(xiě)入
                if (userListRef.get().size() % 1_0000 == 0) {
                    long writeStart = System.currentTimeMillis();
                    //寫(xiě)入到文件
                    excelWriter.write(userListRef.get(), writeSheetRef.get());
                    //清空列表內(nèi)部數(shù)據(jù)
                    userListRef.get().clear();
                    long writeEnd = System.currentTimeMillis();
                    log.info("本次寫(xiě)入耗時(shí):{}", writeEnd - writeStart);
                }

                //寫(xiě)入100w條后,切換sheet
                if (atomicCount.get() % 100_0000 == 0) {
                    //自增sheetNo
                    sheetNo.incrementAndGet();
                    //修改sheetName
                    sheetName.set("sheet-" + sheetNo.get());
                    //寫(xiě)入文件
                    writeSheetRef.set(EasyExcel.writerSheet(sheetNo.get(), sheetName.get()).build());

                    log.info("寫(xiě)滿(mǎn)一個(gè)sheet,切換到下一個(gè)sheet:{}", sheetName);
                }
            });

            //檢查是否還有未寫(xiě)入的文件
            if (CollectionUtil.isNotEmpty(userList)) {
                log.info("存在未寫(xiě)入完成的數(shù)據(jù),size:{}", userList.size());
                excelWriter.write(userList, writeSheetRef.get());
            }
            long total = System.currentTimeMillis() - start;
            log.info("導(dǎo)出結(jié)束,總耗時(shí):{}", total);

        } catch (Exception e) {
            throw new RuntimeException(e);
        }

最終這種方案的執(zhí)行耗時(shí)在最好的情況下差不多30s左右,總的來(lái)說(shuō)流式查詢(xún)天然內(nèi)存友好且游標(biāo)式順序前進(jìn)的特定,對(duì)于這種并發(fā)場(chǎng)景下的數(shù)據(jù)導(dǎo)出是非常友好的,所以這種方案也是筆者比較推薦的方案:

三、小結(jié)

以上便是筆者的百萬(wàn)級(jí)別數(shù)據(jù)導(dǎo)出的落地方案,可以看出筆者針對(duì)分頁(yè)查詢(xún)導(dǎo)出的方案著重在分頁(yè)查詢(xún)大小和分頁(yè)查詢(xún)sql上進(jìn)行重點(diǎn)優(yōu)化,通過(guò)平衡分頁(yè)查詢(xún)的數(shù)據(jù)量和IO次數(shù)找到合適的pageSize,再通過(guò)上一次分頁(yè)查詢(xún)結(jié)果定位下一次查詢(xún)的id作為where條件,避免分頁(yè)查詢(xún)時(shí)的全秒掃描以得到符合業(yè)務(wù)需求的高性能sql。

對(duì)于流式查詢(xún),因?yàn)榱魇讲樵?xún)的特定,筆者在優(yōu)化時(shí)更著重于找到寫(xiě)入到文件這塊的耗時(shí)上,通過(guò)找到IO寫(xiě)入的平衡點(diǎn)找到最佳寫(xiě)入閾值,從而完成百萬(wàn)級(jí)別數(shù)據(jù)的高效導(dǎo)出。

責(zé)任編輯:趙寧寧 來(lái)源: 寫(xiě)代碼的SharkChili
相關(guān)推薦

2019-08-27 08:51:36

計(jì)數(shù)數(shù)據(jù)庫(kù)并發(fā)

2023-02-03 08:21:30

excelMySQL

2025-05-12 00:00:00

2021-03-11 08:24:48

Javapoi數(shù)據(jù)脫敏

2025-05-06 01:00:00

Excel高性能內(nèi)存

2023-02-25 10:04:21

JavaExcel導(dǎo)出功能

2025-05-19 01:00:00

2020-09-22 09:41:09

前端

2022-12-29 08:49:40

SpringBootExcel

2024-07-30 15:56:42

2022-02-09 18:28:46

多線程Excel代碼

2012-04-28 10:50:51

jQuery

2011-08-24 12:35:03

SQL Server按條件批量導(dǎo)出為多個(gè)E

2018-10-22 15:21:52

iBASEAMD EPYC服務(wù)器

2010-07-21 14:17:36

SQL Server數(shù)

2011-07-06 18:07:16

ASP

2020-01-18 10:27:07

簡(jiǎn)書(shū)知乎Python

2024-09-12 17:23:02

2024-10-18 11:39:55

MySQL數(shù)據(jù)檢索
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)