長連接與echarts實現動態數據實時展示

一、需求

項目上提出了一個需求,說是需要做一個簡單大氣的頁面,上方一排方塊顯示各個市區的當日業務數量,下方是一個柱狀圖表,動態的顯示當日的業務數量。所謂動態就是要實時的顯示業務數量,如果有業務的增加,數字會跳動,而且柱狀圖也會增長。

二、解決方案

1、不太好的方案

按照正常的想法,可以通過異步加載不斷的向後臺發起請求,對業務數量進行查詢,將查詢結果返回到前臺後刷新數據。但是如果採用這種方式,就會有以下幾個問題:
a、間斷的持續異步訪問後臺會對服務器造成壓力,佔用過多的資源。
b、結合業務情況,數據量並不是每分每秒都在變化。如果在一段時間內業務量並沒有變化,前臺卻做了多次查詢請求,那麼這些請求相當於是無意義的請求,進而也浪費了服務器資源。
其實這個方案的優點在於,實現方式簡單。缺點是會對服務器造成壓力,消耗不必要的資源。

2、改進後的方案

經過討論之後,部門的大神提出這樣的方案。在後臺定時查詢數據庫得到業務數量的統計結果,並將結果保存在一個變量裏。前臺通過ajax向後臺輪詢請求,保持一個長連接。如果變量中的數據有了變化,則向前臺返回結果,前臺刷新數據,之後發起下一個長連接請求。這樣,對於後臺的訪問次數會減小,幾乎是只有數據變動的時候纔會發起新的請求(之所以說是幾乎是因爲長連接儘量不要真的一直保持下去,所以在業務量沒有變化的情況下,也會定時的關閉長連接併發起下一次鏈接)。我們將查詢業務量的訪問壓力放在後臺與數據庫之間。而這部分壓力對於服務器來說要比前臺多次發起請求要小得多。

3、再一次改進?

就算是查詢數據庫的壓力完全放到了後臺和數據庫之間的交互,但是,還是會存在數據量沒有變化的多餘查詢。如果是在業務辦理的同時向後臺的變量寫入記錄,就可以讓數據從之前的主動查詢變成被動接受,由拉變推,這樣可以真正的保證數據是實時變化的。但是問題又來了,我們要改造業務生成的方法,同時要做一個整體的變量以便於獲取。但是這樣實現的工作量太大,而且這個頁面的數據展示只不過是一個簡單的功能而已,因此我們沒有考慮這種方法。所以最終選擇使用第二種方案來實現這個功能。

三、代碼實現

1、前臺頁面實現

a、數據展示
數據展示使用了echarts的柱狀圖顯示。echarts是個很常用的前端圖表插件。不過作爲一個非專業前端碼農,會用就行了。具體怎麼用就直接看官方的文檔吧,官方文檔就已經很詳細了。
b、長連接發起
我們通過ajax向後臺發起異步請求,並保持長連接。代碼如下:
//區劃統計長連接查詢
        function longPolling() {
            $.ajax({
                url: "${root!}/accept/statistics/wisdomdata/regionQueryLongPolling?regionCode=" + regionCode,
                data: {"timed": new Date().getTime()},
                dataType: "json",
                type: "POST",
                timeout: 20000,//設置爲20s後斷開連接
                error: function (XMLHttpRequest, textStatus, errorThrown) {//請求失敗
                    //如果返回錯誤,根據錯誤信息進行相應的處理
                    //再次發起長連接
                    longPolling();
                },
                success: function (data) {//請求成功
                    //根據後臺返回的數據對頁面數據進行刷新
                    refresh(data)
                    longPolling();//刷新成功後發起新的長連接請求
                }
            });
        }
長連接成功返回數據後,我們根據獲取的數據對頁面數據進行刷新。因爲使用的是echarts的柱狀圖,所以只需要重新封裝echarts的數據,然後調用myChart的setOption方法將數據重新裝載,就可以完成圖表的刷新了。其實因爲只是數據在變化,我們只需要將後臺返回的數據放到一個數組裏,然後修改series的data值即可。代碼如下:
function refresh(data) {
            //後臺是將各列的數據用【,】隔開返回到前臺,所以可以通過split(",")來獲取series中的data所需要的數組
            var regionTotal = data.regionTotal.split(",");
            var regionCodeArray = data.regionCodeArray.split(",");
            var regionNameArray = data.regionNameArray.split(",");
            //這裏的series,option和myChart都是頁面初始化時創建的變量,在此不表
            series[0]["data"] = regionTotal;//動態刷新
            option.series = series;
            myChart.setOption(option);
        }

2、後臺邏輯實現

後臺邏輯實現氛圍兩部分。一部分是響應前臺請求,返回改變後的數據。另一部分是單獨的一個線程,不斷的對數據庫進行查詢獲取最新的數據。
a、數據查詢
我們先來看看後臺與數據庫交互的部分,這部分需要實現下面的功能:
要把實時查詢出來的數據保存在後臺以供前臺隨時獲取。解決方法是使用一個內部類對數據進行保存。
代碼如下:
  class RegionQuery{
        private boolean isOpen = false;//查詢線程是否已經開啓
        public List<String> regionCodelist = new ArrayList<String>();//需要動態查詢的區劃code
        public List<String> regionNamelist = new ArrayList<String>();//需要動態查詢的區劃name
        public List<String> loadingList = new ArrayList<String>();//當前線程已經加入動態查詢的區劃
        public Map<String, JSONObject> regionCountMap = new HashMap<String, JSONObject>();//動態保存最新的區劃業務數量
        public RegionQuery(){
            String[] regionArray = {"00001","00002","00003","00004"};
            String[] regionNameArray = {"北京市","上海市","廣州市","深圳市"};
            regionCodelist = Arrays.asList(regionArray);
            regionNamelist = Arrays.asList(regionNameArray);
            this.isOpen= false;
        }
        public boolean isOpen() {
            return isOpen;
        }
        public void setOpen(boolean open) {
            isOpen = open;
        }
    }
要不間斷的對數據庫中的數據進行查詢。解決方法是在第一次訪問這個頁面時,啓動一個線程對數據庫進行循環查詢,並將數據保存在後臺內部類。
代碼片段如下:
//開啓一個線程來獲取regionQuery類的數據,這個線程是寫在剛進入展示頁面的方法中
            if (!regionQuery.isOpen()) {//如果未開啓線程,就進行開啓
                new Thread(){
                    public void run(){
                        regionQuery.setOpen(true);//標誌進程已開啓
                        while (true) {
                            try {
                                //循環查詢當前loading列表的區劃數據
                                for(int i=0;i<regionQuery.loadingList.size();i++){
                                    String code = regionQuery.loadingList.get(i);//當前區劃
                                    String name = regionQuery.regionNamelist.get(regionQuery.regionCodelist.indexOf(code));//當前區劃名
                                    JSONObject jsonObject = wisdomRegionQuery(code,name,"month");//wisdomRegionQuery是對數據庫進行業務量查詢的方法,在這裏就不放代碼了。返回的是個JSONObject,其中各區的業務量數字是放在key爲regionTotal的鍵值對中
                                    regionQuery.regionCountMap.put(code, jsonObject);
                                }
                                Thread.sleep(5000);//等待5秒鐘繼續進行查詢
                            } catch (ParseException e) {
                                e.printStackTrace();
                                logger.error(e.getMessage());
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                                logger.error(e.getMessage());
                            }
                        }
                    }
                }.start();
            }
b、響應前臺請求返回數據
在數據發生變化時,向保持長連接的前臺請求返回最新的數據。解決方法是,我們將本次查詢的數據保存在session中,在後續的查詢時將新的數據與session中的數據做對比。如果數據發生變化,則將新數據返回前臺,同時更新session中的數據。
代碼如下:
 /**
     * 長連接動態獲取當前區劃統計值
     * @param response
     */
    @RequestMapping(value = "/regionQueryLongPolling", method = RequestMethod.POST)
    public void regionQueryLongPolling(HttpServletResponse response) {
        String regionCode = this.getPara("regionCode");
        //判斷session是否有當前查詢區劃的數據
        HttpSession session = request.getSession();
        if (session.getAttribute(regionCode) == null) {
            JSONObject tempJson = new JSONObject();
            tempJson.put("regionTotal", "0");
            session.setAttribute(regionCode,tempJson);//以json格式保存區劃名稱業務量等相關信息
        }
        JSONObject result = new JSONObject();
        result.put("code", SYSTEM_ERROR);
        try {
            for(int i=0;i<21;i++){//設置21秒後退出循環
                JSONObject sessionJson = (JSONObject) session.getAttribute(regionCode);
                JSONObject regionCountMapJson = (JSONObject) regionQuery.regionCountMap.get(regionCode);
                if(!sessionJson.getString("regionTotal").equals(regionCountMapJson.getString("regionTotal"))){
                    //如果數據有變化則返回值並跳出循環
                    result.put("code", SYSTEM_SUCCESS);
                    result.putAll(regionCountMapJson);
                    session.setAttribute(regionCode,regionCountMapJson);
                    break;
                }
                Thread.sleep(1000);//等待一秒鐘保持連接
            }
        } catch (Exception e) {
            e.printStackTrace();
            logger.error(e.getMessage());
            result.put("code", SYSTEM_ERROR);
        }
        this.renderJson(response, result.toString());// 返回數據
    }

四、總結

這個案例的核心在於兩點,第一點,是使用長連接保持前臺與後臺之間的連接,只有數據改變時再返回數據。第二點,是在後臺拋出一個線程不斷的對數據庫進行查詢,並將結果保存到一個內部類裏。整體實現了查詢數據和展示動態數據的分離,減少前臺頁面對服務器的訪問壓力。畢竟自己的技術不夠成熟,對於方案的實現,依舊會有考慮欠缺的地方,希望有大神能夠指正。
同時,這個案例也有很多需要改進的地方,比如,拋出的用來查詢數據的線程沒有結束的時候,就會一直跑下去,這一點應該會造成隱患。也許我們可以根據,是否還有人保留這個數據展示頁面,來控制這個線程的狀態。這就等以後有時間的時候,再去研究啦。

參考資料:
長連接長輪詢:http://www.cnblogs.com/hoojo/p/longPolling_comet_jquery_iframe_ajax.html

發佈了29 篇原創文章 · 獲贊 70 · 訪問量 21萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章