长连接与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万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章