前后端交互接口性能速度优化

目录

  • 1 接口上传返回数据量过大
  • 2 返回相应慢查询解决方案
    • 2.1 深度分页
    • 2.2 索引问题
    • 2.3 join 过多 or 子查询过多
    • 2.4 in 的元素过多
    • 2.5 数据量过大
  • 3 业务逻辑复杂线程池优化
    • 3.1 循环调用
    • 3.2 顺序调用
  • 4 线程池设计不合理
  • 5 锁设计不合理
  • 6 机器问题
  • 7 缓存和回调或者反查


1 接口上传返回数据量过大

造成请求非常慢

传输数据量过大解决方案 ,使用gzip流压缩,减少上传速度,接口性能瓶颈就在上传速度,按照运营商来讲,上传带宽是下载带宽的百分之十,上传速度是要比下载少很多的。我们平时感到上网卡其实有很多原因的。比如你这里说,看小众网站会慢,其实不是宽带问题,是这些小众网站服务能力有限。我们看视频,其实用不到上传,都是把视频资源下载到本地播放的。

后端工具类:

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

public class GZIPUtils {
   /**  
     * 字符串的压缩  
     *   
     * @param str
     *            待压缩的字符串
     * @return    返回压缩后的字符串
     * @throws IOException  
     */  
    public static String compress(String str) throws IOException {
        if (null == str || str.length() <= 0) {  
            return null;
        }  
        // 创建一个新的 byte 数组输出流  
        ByteArrayOutputStream out = new ByteArrayOutputStream();  
        // 使用默认缓冲区大小创建新的输出流  
        GZIPOutputStream gzip = new GZIPOutputStream(out);  
        // 将 b.length 个字节写入此输出流  
        gzip.write(str.getBytes("UTF-8"));
        gzip.close();  
        // 使用指定的 charsetName,通过解码字节将缓冲区内容转换为字符串  
        return out.toString("ISO-8859-1");
    }  
    

后台解压代码

  /**  
   * 字符串的解压  
   *   
   * @param b
   *            对字符串解压  
   * @return    返回解压缩后的字符串  
   * @throws IOException  
   */  
  public static String unCompress(byte[] b) {
     try {
         if (null == b || b.length <= 0) {
             return null;
         }  
         // 创建一个新的 byte 数组输出流  
         ByteArrayOutputStream out = new ByteArrayOutputStream();  
         // 创建一个 ByteArrayInputStream,使用 buf 作为其缓冲区数组  
         ByteArrayInputStream in;
       in = new ByteArrayInputStream(b);
       
         // 使用默认缓冲区大小创建新的输入流  
         GZIPInputStream gzip = new GZIPInputStream(in);  
         byte[] buffer = new byte[256];  
         int n = 0;  
         while ((n = gzip.read(buffer)) >= 0) {// 将未压缩数据读入字节数组  
             // 将指定 byte 数组中从偏移量 off 开始的 len 个字节写入此 byte数组输出流  
             out.write(buffer, 0, n);  
         }  
         // 使用指定的 charsetName,通过解码字节将缓冲区内容转换为字符串  
         return out.toString("UTF-8");

     } catch (Exception e) {
       e.printStackTrace();
    }
    return null;  
  }

}
String encodeStr = URLEncoder.encode(JSON.toJSONString(buildMenuTree(menus)), “UTF-8);
encodeStr = Base64.encodeBase64String(encodeStr.getBytes(“UTF-8));

String menuCompressStr = GZIPUtils.compress(encodeStr);

前端js

//data为后台返回的值
JSON.parse(unzip(data));
// 解压
 function unzip(key) {
     var charData = [];
     var keyArray = key.split('');
     for(var i = 0; i < keyArray.length; i++){
         var item = keyArray[i];
         charData.push(item.charCodeAt(0));
     }

     // var binData = new Uint8Array(charData);
     // console.log('Uint8Array:' + binData);
     // 解压
     // var data = pako.inflate(binData);
     var data = pako.inflate(charData);

     // 将GunZip ByTAREAR转换回ASCII字符串
     // let   res= String.fromCharCode.apply(null, new Uint16Array(data));

 	let    res= String.fromCharCode.apply(null, data);
     return decodeURIComponent(Base64.decode(res));
 }


// 压缩
 function zip(str) {
     //escape(str)  --->压缩前编码,防止中午乱码
     var binaryString = pako.gzip(escape(str), { to: 'string' });
     return binaryString;
 }

当出现 Maximum call stack size exceeded 时是因为数据量过大导致需要使用

  function unzip(key) {
        var charData = [];
        var keyArray = key.split('');
        for(var i = 0; i < keyArray.length; i++){
            var item = keyArray[i];
            charData.push(item.charCodeAt(0));
        }

        // var binData = new Uint8Array(charData);
        // console.log('Uint8Array:' + binData);
        // 解压
        // var data = pako.inflate(binData);
        var data = pako.inflate(charData);

        // 将GunZip ByTAREAR转换回ASCII字符串
        // let res= String.fromCharCode.apply(null, new Uint16Array(data));
        //let res = String.fromCharCode.apply(null, data);
         let res=''
		  for (x = 0; x < data.length / chunk; x++) {
		    res+= String.fromCharCode.apply(null, data.slice(x * chunk, (x+ 1) * chunk));
		  }
		  res+= String.fromCharCode.apply(null, data.slice(x * chunk));
        return decodeURIComponent(Base64.decode(res));
    }

前端解压需要用到pako.min.js

2 返回相应慢查询解决方案

2.1 深度分页

所谓的深度分页问题,涉及到 mysql 分页的原理。通常情况下,mysql 的分页是这样写的:

select name,code from student limit 100,20

含义当然就是从 student 表里查 100 到 120 这 20 条数据,mysql 会把前 120 条数据都查出来,抛弃前 100 条,返回 20 条。

当分页所以深度不大的时候当然没问题,随着分页的深入,sql 可能会变成这样:

select name,code from student limit 1000000,20

这个时候,mysql 会查出来 1000020 条数据,抛弃 1000000 条,如此大的数据量,速度一定快不起来。

那如何解决呢?一般情况下,最好的方式是增加一个条件:

select name,code from student where id>1000000  limit 20

这样,mysql 会走主键索引,直接连接到 1000000 处,然后查出来 20 条数据。但是这个方式需要接口的调用方配合改造,把上次查询出来的最大 id 以参数的方式传给接口提供方,会有沟通成本(调用方:老子不改!)。

2.2 索引问题

这个是最容易解决的问题,我们可以通过:

show create table xxxx(表名)

查看某张表的索引。具体加索引的语句网上太多了,不再赘述。不过顺便提一嘴,加索引之前,需要考虑一下这个索引是不是有必要加,如果加索引的字段区分度非常低,那即使加了索引也不会生效。

另外,加索引的 alter 操作,可能引起锁表,执行 sql 的时候一定要在低峰期

这个是慢查询最不好分析的情况,虽然 mysql 提供了 explain 来评估某个 sql 的查询性能,其中就有使用的索引。

2.3 join 过多 or 子查询过多

我把 join 过多[子查询过多放在一起说了。一般来说,不建议使用子查询,可以把子查询改成 join 来优化。同时,join 关联的表也不宜过多,一般来说 2-3 张表还是合适的。

具体关联几张表比较安全是需要具体问题具体分析的,如果各个表的数据量都很少,几百条几千条,那么关联的表的可以适当多一些,反之则需要少一些。

另外需要提到的是,在大多数情况下 join 是在内存里做的,如果匹配的量比较小,或者 join_buffer 设置的比较大,速度也不会很慢。

但是,当 join 的数据量比较大的时候,mysql 会采用在硬盘上创建临时表的方式进行多张表的关联匹配,这种显然效率就极低,本来磁盘的 IO 就不快,还要关联。

一般遇到这种情况的时候就建议从代码层面进行拆分,在业务层先查询一张表的数据,然后以关联字段作为条件查询关联表形成 map,然后在业务层进行数据的拼装。

一般来说,索引建立正确的话,会比 join 快很多,毕竟内存里拼接数据要比网络传输和硬盘 IO 快得多。

减少使用LEFT JOIN查询很简单也很实用的一个方案就是建立中间表, 用空间换时间。
另外当数据量大到一定程度情况下,考虑分库分表,可以看看mycat中间件

2.4 in 的元素过多

这种问题,如果只看代码的话不太容易排查,最好结合监控和数据库日志一起分析。如果一个查询有 in,in 的条件加了合适的索引,这个时候的 sql 还是比较慢就可以高度怀疑是 in 的元素过多。

一旦排查出来是这个问题,解决起来也比较容易,不过是把元素分个组,每组查一次。想再快的话,可以再引入多线程。

进一步的,如果in的元素量大到一定程度还是快不起来,这种最好还是有个限制:

select id from student where id in (1,2,3 ...... 1000) limit 200

当然了,最好是在代码层面做个限制:

if (ids.size() > 200) {
    throw new Exception("单次查询数据量不能超过200");
}

2.5 数据量过大

这种问题,单纯代码的修修补补一般就解决不了了,需要变动整个的数据存储架构。或者是对底层 mysql 分表或分库+分表;或者就是直接变更底层数据库,把 mysql 转换成专门为处理大数据设计的数据库。

这种工作是个系统工程,需要严密的调研、方案设计、方案评审、性能评估、开发、测试、联调,同时需要设计严密的数据迁移方案、回滚方案、降级措施、故障处理预案。

除了以上团队内部的工作,还可能有跨系统沟通的工作,毕竟做了重大变更,下游系统的调用接口的方式有可能会需要变化。

3 业务逻辑复杂线程池优化

3.1 循环调用

这种情况,一般都循环调用同一段代码,每次循环的逻辑一致,前后不关联。

比如说,我们要初始化一个列表,预置 12 个月的数据给前端:

List<Model> list = new ArrayList<>();
for(int i = 0 ; i < 12 ; i ++) {
    Model model = calOneMonthData(i); // 计算某个月的数据,逻辑比较复杂,难以批量计算,效率也无法很高
    list.add(model);
}

这种显然每个月的数据计算相互都是独立的,我们完全可以采用多线程方式进行:

// 建立一个线程池,注意要放在外面,不要每次执行代码就建立一个,具体线程池的使用就不展开了
public static ExecutorService commonThreadPool = new ThreadPoolExecutor(5, 5, 300L,
        TimeUnit.SECONDS, new LinkedBlockingQueue<>(10), commonThreadFactory, new ThreadPoolExecutor.DiscardPolicy());
 
// 开始多线程调用
List<Future<Model>> futures = new ArrayList<>();
for(int i = 0 ; i < 12 ; i ++) {
    Future<Model> future = commonThreadPool.submit(() -> calOneMonthData(i););
    futures.add(future);
}
 
// 获取结果
List<Model> list = new ArrayList<>();
try {
   for (int i = 0 ; i < futures.size() ; i ++) {
      list.add(futures.get(i).get());
   }
} catch (Exception e) {
   LOGGER.error("出现错误:", e);
}

3.2 顺序调用

如果不是类似上面循环调用,而是一次次的顺序调用,而且调用之间没有结果上的依赖,那么也可以用多线程的方式进行:

代码上看:

A a = doA();
B b = doB();
 
C c = doC(a, b);
 
D d = doD(c);
E e = doE(c);
 
return doResult(d, e);

那么可用 CompletableFuture 解决:

CompletableFuture<A> futureA = CompletableFuture.supplyAsync(() -> doA());
CompletableFuture<B> futureB = CompletableFuture.supplyAsync(() -> doB());
CompletableFuture.allOf(futureA,futureB) // 等a b 两个任务都执行完成
 
C c = doC(futureA.join(), futureB.join());
 
CompletableFuture<D> futureD = CompletableFuture.supplyAsync(() -> doD(c));
CompletableFuture<E> futureE = CompletableFuture.supplyAsync(() -> doE(c));
CompletableFuture.allOf(futureD,futureE) // 等d e两个任务都执行完成
 
return doResult(futureD.join(),futureE.join());

这样 A B 两个逻辑可以并行执行,D E 两个逻辑可以并行执行,最大执行时间取决于哪个逻辑更慢。

4 线程池设计不合理

Java线程池七大参数详解和配置:https://blog.csdn.net/ZGL_cyy/article/details/118230264

5 锁设计不合理

锁设计不合理一般有两种:锁类型使用不合理 or 锁过粗。

锁类型使用不合理的典型场景就是读写锁。也就是说,读是可以共享的,但是读的时候不能对共享变量写;而在写的时候,读写都不能进行。

在可以加读写锁的时候,如果我们加成了互斥锁,那么在读远远多于写的场景下,效率会极大降低。

锁过粗则是另一种常见的锁设计不合理的情况,如果我们把锁包裹的范围过大,则加锁时间会过长,例如:

public synchronized void doSome() {
    File f = calData();
    uploadToS3(f);
    sendSuccessMessage();
}

这块逻辑一共处理了三部分,计算、上传结果、发送消息。显然上传结果和发送消息是完全可以不加锁的,因为这个跟共享变量根本不沾边。

因此完全可以改成:

public void doSome() {
    File f = null;
    synchronized(this) {
        f = calData();
    }
    uploadToS3(f);
    sendSuccessMessage();
}

6 机器问题

fullGC,机器重启,线程打满

造成这个问题的原因非常多,笔者就遇到了定时任务过大引起 fullGC,代码存在线程泄露引起 RSS 内存占用过高进而引起机器重启等待诸多原因。

需要结合各种监控和具体场景具体分析,进而进行大事务拆分、重新规划线程池等等工作。

7 缓存和回调或者反查

最后实在优化不下来可以使用缓存第三方中间件,或者某些保存接口较慢无法直接进行全部返回的话进行第三方回调或者反查

你可能感兴趣的:(architect,性能优化,java,前端)