我是如何将一个加载90s的接口优化到不足2s的

 

一、提出问题

  由于公司是做医疗级智能可穿戴设备的,所以数据(二进制数据)的存储方面有点特殊,数据没有存储于数据库里面,而是存储于磁盘上。可能有同学质疑,mysql的Blob类型也可以存储二进制数据啊,是啊,的确可以。但是我们的数据太大了,用户可能连续测量30-40个小时,这时这个数据量有50M左右,而一般的接口传输的数据量也就5k左右,大数据在mysql里面的传输也要花大量的传输时间啊,而且数据量一大,就可能在传输中出现问题,比如传输中断、这种大流量接口对服务器、网络的冲击等等。所以就把这个数据存储于磁盘上了。

   这样每次前端需要数据的时候就发送一个请求,后端都需要读文件,之后将文件数据经过特殊处理后发送到前端(可以理解为一个很大很大的数组)。最开始的时候,我测了一下,对于一个用户测量了40小时的数据,文件大概在60M,这样一个请求从发起请求到前端接受完数据要90多秒,这肯定是受不了的,那我是怎么优化的呢(当然这个过程是比较曲折的)?

二、分析问题

通过对整个请求的分析(可以借助谷歌浏览器的开发者控制台),耗时无非花费在两方面:

    • 后端处理时间,即Waiting(TTFB);
    • 数据下载的时间,即Content Download;

  所以我的优化集中在这两个方面。

 

三、解决问题

1、Tomcat开启GZIP压缩

    详见博文:Tomcat启用GZIP压缩,提升web性能

2、使用逐差压缩法压缩原始数据

    首先这里我引用一下百度百科里面的解释:

1、解释:
    逐差压缩法是记录某些实数的数列时,为了节约记录空间和查找时间而采用的一种常用方法。具体通过逐差、压缩和记录三步。

2、具体做法:
    1)逐差——依次计算每项与后一项的差。
    2)压缩——将每一连续的相同差值记为一个块。
    3)记录——最终记录每个块的值(即相同的差值)和块长,另外记录原数列的首位。

3、适用对象:
    具有某些特征的数列,如有条件等差数列、函数取整数列、排序后数列等,使用很灵活。

3、应用举例:
    记录数列:1 2 2 3 3 3 4 4 4 4 5 5 5 5 5……
    逐差为:1 0 1 0 0 1 0 0 0 1 0 0 0 0……
    记录为:(1,1)(0,1)(1,1)(0,2)(1,1)(0,3)(1,1)(0,4)……

  当然这个方法是有应用限制的,因为我们的数据是一个大数组,数组的相邻两个点之间的差值特别的小,即如果作图的话,曲线是连续平滑的,不会出现跳动,所以这样的话,数组的点与点之间的差值特别的小,相应的传输的体积特别小;如果数组的点与点之间的差值比较大,那这个方法是没啥作用的。

  3、后端使用NIO读取文件,代替原有的IO读取文件

    Java NIO我这里不详细介绍了,请见博文:Java NIO 系列教程

public List<Byte> readByNIO(String filePath) {
        List<Byte> byteLists = new ArrayList<>();

        try (FileInputStream fin = new FileInputStream(filePath);
             FileChannel inChannel = fin.getChannel()) {
            
            ByteBuffer byteBuffer = ByteBuffer.allocate(4096);
            int bytesRead = inChannel.read(byteBuffer);

            while (bytesRead != -1) {
                byteBuffer.flip();
                while (byteBuffer.hasRemaining()) {
                    byteLists.add(byteBuffer.get());
                }
                byteBuffer.clear();
                bytesRead = inChannel.read(byteBuffer);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return byteLists;
    }

4、加大服务器的下行带宽

  网络带宽是指在单位时间(一般指的是1秒钟)内能传输的数据量。带宽是一个非常有用的概念,在网络通信中的地位十分重要。

  带宽的实际含义是在给定时间等条件下流过特定区域的最大数据位数。虽然它的概念有点抽象,但是可以用比喻来帮助理解带宽的含义。把城市的道路看成网络,道路有双车道、四车道也许是八车道,人们驾车从出发点到目的地,途中可能经过双车道、四车道也许是单车道。在这里,车道的数量好比是带宽,车辆的数目就好比是网络中传输的信息量。我们再用城市的供水网来比喻,供水管道的直径可以衡量运水的能力,主水管直径可能有2m,而到家庭的可能只有2cm。在这个比喻中,水管的直径好比是带宽,水就好比是信息量。使用粗管子就意味着拥有更宽的带宽,也就是有更大的信息运送能力。

  5、使用Http范围请求+前端多线程请求数据

    请见博文:图解:HTTP 范围请求,助力断点续传、多线程下载的核心原理

    后端代码(网上找的):

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.util.Enumeration;

public class DownloadFile {

    private static Logger logger = LoggerFactory.getLogger(DownloadFile.class);

    public static void downRangeFile(File downloadFile, HttpServletResponse response, HttpServletRequest request) throws Exception {
        // 文件不存在
        if (!downloadFile.exists()) {
            // 404
            response.sendError(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        // 记录文件大小
        long fileLength = downloadFile.length();
        // 记录已下载文件大小
        long pastLength = 0;
        // 0:从头开始的全文下载;1:从某字节开始的下载(bytes=27000-);2:从某字节开始到某字节结束的下载(bytes=27000-39000)
        int rangeSwitch = 0;
        // 记录客户端需要下载的字节段的最后一个字节偏移量(比如bytes=27000-39000,则这个值是为39000)
        long toLength = 0;
        // 客户端请求的字节总量
        long contentLength = 0;
        // 记录客户端传来的形如“bytes=27000-”或者“bytes=27000-39000”的内容
        String rangeBytes = "";

        // 负责读取数据
        RandomAccessFile raf = null;
        // 写出数据
        OutputStream os = null;
        // 缓冲
        OutputStream out = null;
        // 缓冲区大小
        int bsize = 1024;
        // 暂存容器
        byte[] b = new byte[bsize];

        if (request.getParameter("showheader") != null) {
            Enumeration paramNames = request.getHeaderNames();

            while (paramNames.hasMoreElements()) {
                String name = paramNames.nextElement().toString();
                if (name != null && name.length() > 0) {
                    String value = request.getHeader(name);
                    logger.info("************" + name + ":" + value);
                }
            }
        }

        String range = request.getHeader("Range");
        // if(range == null)
        // range = "bytes=0-";
        int responseStatus = 206;
        if (range != null && range.trim().length() > 0 && !"null".equals(range)) {
            // 客户端请求的下载的文件块的开始字节
            responseStatus = javax.servlet.http.HttpServletResponse.SC_PARTIAL_CONTENT;
            logger.info("request.getHeader(\"Range\")=" + range);
            rangeBytes = range.replaceAll("bytes=", "");

            if (rangeBytes.endsWith("-")) {
                // 行如:bytes=969998336-
                rangeSwitch = 1;
                rangeBytes = rangeBytes.substring(0, rangeBytes.indexOf('-'));
                pastLength = Long.parseLong(rangeBytes.trim());
                // 客户端请求的是969998336之后的字节(包括bytes下标索引为969998336的字节)
                contentLength = fileLength - pastLength;
            } else {
                // 行如:bytes=1275856879-1275877358
                rangeSwitch = 2;
                String temp0 = rangeBytes.substring(0, rangeBytes.indexOf('-'));
                String temp2 = rangeBytes.substring(rangeBytes.indexOf('-') + 1, rangeBytes.length());
                // bytes=1275856879-1275877358,从第1275856879个字节开始下载
                pastLength = Long.parseLong(temp0.trim());
                // bytes=1275856879-1275877358,到第1275877358 个字节结束
                toLength = Long.parseLong(temp2);
                // 客户端请求的是1275856879-1275877358之间的字节
                contentLength = toLength - pastLength + 1;
            }
        } else {
            // 从开始进行下载
            contentLength = fileLength;// 客户端要求全文下载
        }

        /**
         * 如果设设置了Content-Length,则客户端会自动进行多线程下载。如果不希望支持多线程,则不要设置这个参数。 响应的格式是:
         * Content-Length: [文件的总大小] - [客户端请求的下载的文件块的开始字节]
         * ServletActionContext.getResponse().setHeader("Content-Length", new Long(file.length() - p).toString());
         */

        // 来清除首部的空白行
        response.reset();
        // 告诉客户端允许断点续传多线程连接下载,响应的格式是:Accept-Ranges: bytes
        response.setHeader("Accept-Ranges", "bytes");
        // 如果是第一次下,还没有断点续传,状态是默认的 200,无需显式设置;响应的格式是:HTTP/1.1
        // response.addHeader("Cache-Control", "max-age=1296000");
        // response.addHeader("Expires", "Fri, 12 Oct 2012 03:43:01 GMT");
        // response.addHeader("Last-Modified", "Tue, 31 Jul 2012 03:58:36 GMT");
        // response.addHeader("Connection", "keep-alive");
        // response.addHeader("ETag", downloadFile.getName() + "-" +
        // downloadFile.lastModified());
        // response.addHeader("Last-Modified", "Thu, 27 Sep 2012 05:24:44 GMT");

        /**
         * 设置response的Content-Range。响应的格式是:Content-Range: bytes [文件块的开始字节]-[文件的总大小 - 1]/[文件的总大小]
         */
        if (rangeSwitch != 0) {
            // 不是从最开始下载,断点下载响应号为206
            response.setStatus(responseStatus);
            logger.info("----------------------------片段下载,服务器即将开始断点续传...");
            switch (rangeSwitch) {
                case 1: {
                    // 针对 bytes=27000- 的请求
                    String contentRange = new StringBuffer("bytes ")
                            .append(new Long(pastLength).toString()).append("-")
                            .append(new Long(fileLength - 1).toString())
                            .append("/").append(new Long(fileLength).toString())
                            .toString();
                    response.setHeader("Content-Range", contentRange);
                    break;
                }
                case 2: {
                    // 针对 bytes=27000-39000 的请求
                    String contentRange = range.replace("=", " ") + "/"
                            + new Long(fileLength).toString();
                    response.setHeader("Content-Range", contentRange);
                    break;
                }
                default: {
                    break;
                }
            }
        } else {
            // 是从开始下载
            String contentRange = new StringBuffer("bytes ").append("0-")
                    .append(fileLength - 1).append("/").append(fileLength)
                    .toString();
            response.setHeader("Content-Range", contentRange);
            logger.info("----------------------------是从开始到最后完整下载!");
        }

        try {
            /**
             * 设置response的Content-Type,set the MIME type
             */
            String contentType = null;

            if (contentType != null) {
                // /logger.debug("设置内容类型:" + contentType);
                response.setContentType(contentType);
            } else {
                //response.setContentType("audio/mpeg");
                //response.setContentType("application/octet-stream");
                response.setContentType("text/plain");
            }

            // /////////////////////////设置文件下载名称Content-Disposition///////////////////////////
//            if("bytes=0-1".equals(range)){
//                response.reset();
//                304
//                response.setStatus(javax.servlet.http.HttpServletResponse.SC_NOT_MODIFIED);
//            }
            response.setHeader("Content-Length", String.valueOf(contentLength));

            os = response.getOutputStream();
            out = new BufferedOutputStream(os);
            raf = new RandomAccessFile(downloadFile, "r");
            try {
                // 实际输出字节数
                long outLength = 0;
                switch (rangeSwitch) {
                    case 0: {
                        // 普通下载,或者从头开始的下载
                        // 同1,没有break
                    }
                    case 1: {
                        // 针对 bytes=27000- 的请求
                        raf.seek(pastLength);// 形如 bytes=969998336- 的客户端请求,跳过969998336 个字节
                        int n = 0;
                        while ((n = raf.read(b)) != -1) {
                            out.write(b, 0, n);
                            outLength += n;
                        }
                        // while ((n = raf.read(b, 0, 1024)) != -1) {
                        // out.write(b, 0, n);
                        // }
                        break;
                    }
                    case 2: {
                        // 针对 bytes=27000-39000 的请求,从27000开始写数据
                        raf.seek(pastLength);
                        int n = 0;
                        // 记录已读字节数
                        long readLength = 0;
                        while (readLength <= contentLength - bsize) {
                            // 大部分字节在这里读取
                            n = raf.read(b);
                            readLength += n;
                            out.write(b, 0, n);
                            outLength += n;
                        }
                        if (readLength <= contentLength) {
                            // 余下的不足 1024 个字节在这里读取
                            n = raf.read(b, 0, (int) (contentLength - readLength));
                            out.write(b, 0, n);
                            outLength += n;
                        }
                        break;
                    }
                    default: {
                        break;
                    }
                }
                logger.info("Content-Length为:" + contentLength + ";实际输出字节数:" + outLength);
                out.flush();
            } catch (IOException ie) {
                /**
                 * 在写数据的时候, 对于 ClientAbortException 之类的异常,
                 * 是因为客户端取消了下载,而服务器端继续向浏览器写入数据时, 抛出这个异常,这个是正常的。
                 * 尤其是对于迅雷这种吸血的客户端软件, 明明已经有一个线程在读取 bytes=1275856879-1275877358,
                 * 如果短时间内没有读取完毕,迅雷会再启第二个、第三个。。。线程来读取相同的字节段, 直到有一个线程读取完毕,迅雷会 KILL
                 * 掉其他正在下载同一字节段的线程, 强行中止字节读出,造成服务器抛 ClientAbortException。
                 * 所以,我们忽略这种异常
                 */
                // ignore
            }
        } catch (Exception e) {
            //logger.log(Level.SEVERE, e.getMessage());
        } finally {
            if (out != null) {
                try {
                    out.close();
                } catch (IOException e) {
                    //logger.log(Level.SEVERE, e.getMessage());
                }
            }
            if (raf != null) {
                try {
                    raf.close();
                } catch (IOException e) {
                    //logger.log(Level.SEVERE, e.getMessage());
                }
            }
        }
    }
}

 

猜你喜欢

转载自www.cnblogs.com/DDgougou/p/11371911.html