從系統報表頁面導出20w條數據到本地只用了4秒,我是如何作到的

背景

最近有個學弟找到我,跟我描述瞭如下場景:java

他們公司內部管理系統上有不少報表,報表數據都有分頁顯示,瀏覽的時候速度還能夠。可是每一個報表在導出時間窗口稍微大一點的數據時,就異常緩慢,有時候多人一塊兒導出時還會出現堆溢出。mysql

他知道是由於數據所有加載到jvm內存致使的堆溢出。因此只能對時間窗口作了限制。以免因導出過數據過大而引發的堆溢出。最終拍腦殼定下個限制爲:導出的數據時間窗口不能超過1個月。web

雖然問題解決了,可是運營小姐姐不開心了,跑過來和學弟說,我要導出一年的數據,難道要我導出12次再手工合併起來嗎。學弟心想,這也是。系統是爲人服務的,不能爲了解決問題而改變其本質。sql

因此他想問個人問題是:有沒有什麼辦法能夠從根本上解決這個問題。數據庫

所謂從根本上解決這個問題,他提出要達成2個條件服務器

  • 比較快的導出速度
  • 多人能並行下載數據集較大的數據

我聽完他的問題後,我想,他的這個問題估計不少其餘童鞋在作web頁導出數據的時候也確定碰到過。不少人爲了保持系統的穩定性,通常在導出數據時都對導出條數或者時間窗口做了限制。但需求方確定更但願一次性導出任意條件的數據集。多線程

魚和熊掌可否兼得?異步

答案是能夠的。jvm

我堅決的和學弟說,大概7年前我作過一個下載中心的方案,20w數據的導出大概4秒吧。。。支持多人同時在線導出。。。ide

學弟聽完表情有些興奮,可是眉頭又一皺,說,能有這麼快,20w數據4秒?

爲了給他作例子,我翻出了7年前的代碼。。。花了一個晚上把核心代碼抽出來,剝離乾淨,作成了一個下載中心的例子

超快下載方案演示

先不談技術,先看效果,(完整案例代碼文末提供)

數據庫爲mysql(理論上此套方案支持任何結構化數據庫),準備一張測試表t_person。表結構以下:

CREATE TABLE `t_person` (
  `id` bigint(20) NOT NULL auto_increment,
  `name` varchar(20) default NULL,
  `age` int(11) default NULL,
  `address` varchar(50) default NULL,
  `mobile` varchar(20) default NULL,
  `email` varchar(50) default NULL,
  `company` varchar(50) default NULL,
  `title` varchar(50) default NULL,
  `create_time` datetime default NULL,
  PRIMARY KEY  (`id`)
);

一共9個字段。咱們先建立測試數據。

案例代碼提供了一個簡單的頁面,點如下按鈕一次性能夠建立5w條測試數據:

file

這裏我連續點了4下,很快就生成了20w條數據,這裏爲了展現下數據的大體樣子,我直接跳轉到了最後一頁

file

而後點開下載大容量文件,點擊執行執行按鈕,開始下載t_person這張表裏的所有數據

file

點擊執行按鈕以後,點下方刷新按鈕,能夠看到一條異步下載記錄,狀態是P,表示pending狀態,不停刷新刷新按鈕,大概幾秒後,這一條記錄就變成S狀態了,表示Success

file

而後你就能夠下載到本地,文件大小大概31M左右

file

看到這裏,不少童鞋要疑惑了,這下載下來是csv?csv實際上是文本文件,用excel打開會丟失格式和精度。這解決不了問題啊,咱們要excel格式啊!!

其實稍微會一點excel技巧的童鞋,能夠利用excel導入數據這個功能,數據->導入數據,根據提示一步步,當中只要選擇逗號分隔就能夠了,關鍵列能夠定義格式,10秒就能完成數據的導入

file

你只要告訴運營小姐姐,根據這個步驟來完成excel的導入就能夠了。並且下載過的文件,還能夠反覆下。

是否是從本質上解決了下載大容量數據集的問題?

原理和核心代碼

學弟聽到這裏,很興奮的說,這套方案能解決我這裏的痛點。快和我說說原理。

其實這套方案核心很簡單,只源於一個知識點,活用JdbcTemplate的這個接口:

@Override
public void query(String sql, @Nullable Object[] args, RowCallbackHandler rch) throws DataAccessException {
  query(sql, newArgPreparedStatementSetter(args), rch);
}

sql就是select * from t_personRowCallbackHandler這個回調接口是指每一條數據遍歷後要執行的回調函數。如今貼出我本身的RowCallbackHandler的實現

private class CsvRowCallbackHandler implements RowCallbackHandler{

    private PrintWriter pw;

    public CsvRowCallbackHandler(PrintWriter pw){
        this.pw = pw;
    }

    public void processRow(ResultSet rs) throws SQLException {
        if (rs.isFirst()){
            rs.setFetchSize(500);
            for (int i = 0; i < rs.getMetaData().getColumnCount(); i++){
                if (i == rs.getMetaData().getColumnCount() - 1){
                    this.writeToFile(pw, rs.getMetaData().getColumnName(i+1), true);
                }else{
                    this.writeToFile(pw, rs.getMetaData().getColumnName(i+1), false);
                }
            }
        }else{
            for (int i = 0; i < rs.getMetaData().getColumnCount(); i++){
                if (i == rs.getMetaData().getColumnCount() - 1){
                    this.writeToFile(pw, rs.getObject(i+1), true);
                }else{
                    this.writeToFile(pw, rs.getObject(i+1), false);
                }
            }
        }
        pw.println();
    }

    private void writeToFile(PrintWriter pw, Object valueObj, boolean isLineEnd){
        ...
    }
}

這個CsvRowCallbackHandler作的事就是每次從數據庫取出500條,而後寫入服務器上的本地文件中,這樣,不管你這條sql查出來是20w條仍是100w條,內存理論上只佔用500條數據的存儲空間。等文件寫完了,咱們要作的,只是從服務器把這個生成好的文件download到本地就能夠了。

由於內存中不斷刷新的只有500條數據的容量,因此,即使多線程下載的環境下。內存也不會所以而溢出。這樣,完美解決了多人下載的場景。

固然,太多並行下載雖然不會對內存形成溢出,可是會大量佔用IO資源。爲此,咱們仍是要控制下多線程並行的數量,能夠用線程池來提交做業

ExecutorService threadPool = Executors.newFixedThreadPool(5);

threadPool.submit(new Thread(){
    @Override
    public void run() {
    下載大數據集代碼
  }
}

最後測試了下50w這樣子的person數據的下載,大概耗時9秒,100w的person數據,耗時19秒。這樣子的下載效率,應該能夠知足大部分公司的報表導出需求吧。

最後

學弟拿到個人示例代碼後,通過一個禮拜的修改後,上線了頁面導出的新版本,全部的報表提交異步做業,你們統一到下載中心去進行查看和下載文件。完美的解決了以前的2個痛點。

但最後學弟還有個疑問,爲何不能夠直接生成excel呢。也就是說在在RowCallbackHandler中持續往excel裏寫入數據呢?

個人回答是:

1.文本文件流寫入比較快

2.excel文件格式好像不支持流持續寫入,反正我是沒有試成功過。

我把剝離出來的案例整理了下,無償提供給你們,但願幫助到碰到相似場景的童鞋們。

關注做者

關注公衆號「元人部落」回覆」導出案例「得到以上完整的案例代碼,直接能夠運行起來,頁面上輸入http://127.0.0.1:8080就能夠打開文中案例的模擬頁面。

一個只作原創的技術科技分享號

file

相關文章
相關標籤/搜索