互联网接口性能优化经验

接口性能优化

接口性能优化对于从事后端开发的同学来说,肯定再熟悉不过了,因为它是一个跟开发语言无关的公共问题。

该问题说简单也简单,说复杂也复杂。

有时候,只需加个索引就能解决问题。

有时候,需要做代码重构。

有时候,需要增加缓存。

有时候,需要引入一些中间件,比如mq。

有时候,需要需要分库分表。

有时候,需要拆分服务。

等等。。。

导致接口性能问题的原因多种多样,不同的项目不同的接口,原因可能也不一样。

本文我总结了一些行之有效的,优化接口性能的办法,给有需要的朋友一个参考

1.索引

接口索引优化是第一能想到得,也是优化成本较低的,通过查看线上日志或者监控报告,查到某个接口用到的某条sql语句耗时比较长

你可能会发生疑问:

  1. sql语句加索引了吗?

  2. 加的索引生效了吗?

  3. mysql选错索引了吗?

1.1 检查并添加索引

sql语句中where条件的关键字段,或者order by后面的排序字段,忘了加索引,这个问题在项目中很常见, 前期主要完成业务需求。

项目刚开始的时候,由于表中的数据量小,加不加索引sql查询性能差别不大。

后来,随着业务的发展,表中数据量越来越多,性能就就逐渐显现出来,不得不考虑增加索引了。

ALTER TABLE命令可以添加索引:

ALTER TABLE order ADD INDEX idx_name (name);

也可以通过CREATE INDEX命令添加索引:

CREATE INDEX idx_name ON order (name);

mysql中如果想要修改索引,只能先删除索引,再重新添加新的

ALTER TABLE order DROP INDEX idx_name;

DROP INDEX idx_name ON order;

1.2 检查索引是否生效

1、通过命令方式查看索引生效:explain plan for select * from order where code=‘002’;

2、客户端(DBeaver或者PLSQL)

//补充截图

3、索引是否生效,如果索引已经创建,很大可能索引没有生效;常见索引失效的原因:

在这里插入图片描述

2.sql 优化

优化索引,没啥效果,那就接着优化sql,毕竟改sql的成本优于代码

下面整理部分小技巧:

在这里插入图片描述

3.服务远程调用

比如有这样的业务场景:

在用户信息查询接口中需要返回:用户名称、性别、等级、头像、积分、成长值等信息。

而用户名称、性别、等级、头像在用户服务中,积分在积分服务中,成长值在成长值服务中。为了汇总这些数据统一返回,需要另外提供一个对外接口服务。

于是,用户信息查询接口需要调用用户查询接口、积分查询接口 和 成长值查询接口,然后汇总数据统一返回。

调用流程图:

在这里插入图片描述

调用远程接口总耗时530ms,显然这种串行的调用方式不是最佳姿势,那么应该如何优化服务接口呢?

3.1 服务并行调用

将服务接口修改为并行方式执行,如图:

在这里插入图片描述

接口服务调用耗时200>530,基本达到优化目的;

java8 之前是通过Callable接口, 以后建议使用CompleteFuture实现;

public UserInfo getUserInfo(Long id) throws InterruptedException,ExecutionException {
  final UserInfo userInfo = new UserInfo();
  CompletableFuture userFuture = CompletableFuture.supplyAsync(() -> {
    getRemoteUserAndFill(id, userInfo);
    return Boolean.TRUE;
  }, executor);

  CompletableFuture bonusFuture = CompletableFuture.supplyAsync(() -> {
    getRemoteBonusAndFill(id, userInfo);
    return Boolean.TRUE;
  }, executor);

  CompletableFuture growthFuture = CompletableFuture.supplyAsync(() -> {
    getRemoteGrowthAndFill(id, userInfo);
    return Boolean.TRUE;
  }, executor);
  CompletableFuture.allOf(userFuture, bonusFuture, growthFuture).join();

  userFuture.get();
  bonusFuture.get();
  growthFuture.get();

  return userInfo;

3.2 数据异构

上面的介绍都是接口访问三方服务或者DB,我们可以考虑将查询的用户数据缓存起来,比如:redies存的数据结构就是用户信息查询接口所需要的内容,接口只需要通过用户ID查询redis,进而数据组合。

在这里插入图片描述

4、重复调用

1、避免循环里面查询数据库,采用只查询一次方式,访问数据库前,将需要查询的条件临时放入列表中;

2、避免程序死循环及递归查询无限制

5、 异步处理

梳理接口服务逻辑, 核心业务处理采用同步方式执行,非核心逻辑可采用异步方式执行,比如:mq或者线程

普通方式的调用流程图(核心和非核心的逻辑都在一起调用):

在这里插入图片描述

5.1 线程池

采用线程方式执行的,改造后的流程图如下:

在这里插入图片描述

短信和日志功能,被提交到了线程池中。

这样接口中重点关注的是业务操作,把其他的逻辑交给线程异步执行,这样改造之后,让接口性能瞬间提升了。

但使用线程池有个小问题就是:如果服务器重启了,或者是需要被执行的功能出现异常了,无法重试,会丢数据。

5.2 消息

采用mq后,改造后的流程图:

在这里插入图片描述

6、避免大事务

@Transactional注解这种声明式事务的方式提供事务功能,提升开发效率,但是易引发大事务,一般大事务引发的问题:

锁等待、接口超时、数据库主从延迟、死锁、回滚时间长、并发情况数据库连接池被占满。

该如何优化大事务呢?

  1. 少用@Transactional注解
  2. 将查询(select)方法放到事务外
  3. 事务中避免远程调用
  4. 事务中避免一次性处理太多数据
  5. 有些功能可以非事务执行
  6. 有些功能可以异步处理

7、锁的粒度

在某些业务场景中,为了防止多个线程并发修改某个共享数据,造成数据异常。

为了解决并发场景下,多个线程同时修改数据,造成数据不一致的情况。通常的做法:加锁

但如果锁加得不好,导致锁的粒度太粗,也会非常影响接口性能。

7.1、synchronized

synchronized可对方法和执行代码块加锁。

public synchronized doSave(String fileUrl) {
    mkdir();
    uploadFile(fileUrl);
    sendMessage(fileUrl);
}

加锁的目的是为了防止并发的情况下,创建了相同的目录,第二次会创建失败,影响业务功能。

但这种直接在方法上加锁,锁的粒度有点粗。因为doSave方法中的上传文件和发消息方法,是不需要加锁的。只有创建目录方法,才需要加锁。

我们都知道文件上传操作是非常耗时的,如果将整个方法加锁,那么需要等到整个方法执行完之后才能释放锁。显然,这会导致该方法的性能很差,变得得不偿失。

public void doSave(String path,String fileUrl) {
    synchronized(this) {
      if(!exists(path)) {
          mkdir(path);
       }
    }
    uploadFile(fileUrl);
    sendMessage(fileUrl);
}

这样改造之后,锁的粒度一下子变小了,只有并发创建目录功能才加了锁。而创建目录是一个非常快的操作,即使加锁对接口的性能影响也不大。

最重要的是,其他的上传文件和发送消息功能,任然可以并发执行。

当然,这种做在单机版的服务中,是没有问题的。但现在部署的生产环境,为了保证服务的稳定性,一般情况下,同一个服务会被部署在多个节点中。如果哪天挂了一个节点,其他的节点服务任然可用。

多节点部署避免了因为某个节点挂了,导致服务不可用的情况。同时也能分摊整个系统的流量,避免系统压力过大。

synchronized只能保证一个节点加锁是有效的,但如果有多个节点如何加锁呢? 只能采用redis分布式锁、zookeeper分布式锁 和 数据库分布式锁。

7.2、 redis分布式锁

在分布式系统中,由于redis分布式锁相对于更简单和高效,成为了分布式锁的首先,被我们用到了很多实际业务场景当中。

redis分布式锁测试代码:

public void doSave(String path,String fileUrl) {
  try {
    String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
    if ("OK".equals(result)) {
      if(!exists(path)) {
         mkdir(path);
         uploadFile(fileUrl);
         sendMessage(fileUrl);
      }
      return true;
    }
  } finally{
      unlock(lockKey,requestId);
  }  
  return false;
}

跟之前使用synchronized关键字加锁时一样,这里锁的范围也太大了,换句话说就是锁的粒度太粗,这样会导致整个方法的执行效率很低。

其实只有创建目录的时候,才需要加分布式锁,其余代码根本不用加锁

修改后的测试代码如下:

public void doSave(String path,String fileUrl) {
   if(this.tryLock()) {
      mkdir(path);
   }
   uploadFile(fileUrl);
   sendMessage(fileUrl);
}

private boolean tryLock() {
    try {
    String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
    if ("OK".equals(result)) {
      return true;
    }
  } finally{
      unlock(lockKey,requestId);
  }  
  return false;
}

上面代码将加锁的范围缩小了,只有创建目录时才加了锁。这样看似简单的优化之后,接口性能能提升很多。redis分布式锁虽说好用,但它在使用时,有很多注意的细节,隐藏了很多坑,如果稍不注意很容易踩中

7.3、数据库分布式锁

mysql数据库中主要有三种锁:

  • 表锁:加锁快,不会出现死锁。但锁定粒度大,发生锁冲突的概率最高,并发度最低。
  • 行锁:加锁慢,会出现死锁。但锁定粒度最小,发生锁冲突的概率最低,并发度也最高。
  • 间隙锁:开销和加锁时间界于表锁和行锁之间。它会出现死锁,锁定粒度界于表锁和行锁之间,并发度一般。

并发度越高,意味着接口性能越好。

所以数据库锁的优化方向是:优先使用行锁,其次使用间隙锁,再其次使用表锁

8、分表分库

有时候,接口性能受限的不是别的,而是数据库。

当系统发展到一定的阶段,用户并发量大,会有大量的数据库请求,需要占用大量的数据库连接,同时会带来磁盘IO的性能瓶颈问题。

此外,随着用户数量越来越多,产生的数据也越来越多,一张表有可能存不下。由于数据量太大,sql语句查询数据时,即使走了索引也会非常耗时。

在这里插入图片描述

图中将用户库拆分成了三个库,每个库都包含了四张用户表。

如果有用户请求过来的时候,先根据用户id路由到其中一个用户库,然后再定位到某张表。

路由的算法挺多的:

  • 根据id取模,比如:id=7,有4张表,则7%4=3,模为3,路由到用户表3。
  • 给id指定一个区间范围,比如:id的值是0-10万,则数据存在用户表0,id的值是10-20万,则数据存在用户表1。
  • 一致性hash算法

分库分表主要有两个方向:垂直水平

说实话垂直方向(即业务方向)更简单。

在水平方向(即数据方向)上,分库和分表的作用,其实是有区别的,不能混为一谈。

  • 分库:是为了解决数据库连接资源不足问题,和磁盘IO的性能瓶颈问题。
  • 分表:是为了解决单表数据量太大,sql语句查询数据时,即使走了索引也非常耗时问题。此外还可以解决消耗cpu资源问题。
  • 分库分表:可以解决 数据库连接资源不足、磁盘IO的性能瓶颈、检索数据耗时 和 消耗cpu资源等问题。

如果在有些业务场景中,用户并发量很大,但是需要保存的数据量很少,这时可以只分库,不分表。

如果在有些业务场景中,用户并发量不大,但是需要保存的数量很多,这时可以只分表,不分库。

如果在有些业务场景中,用户并发量大,并且需要保存的数量也很多时,可以分库分表。

说实话垂直方向(即业务方向)更简单。

在水平方向(即数据方向)上,分库和分表的作用,其实是有区别的,不能混为一谈。

  • 分库:是为了解决数据库连接资源不足问题,和磁盘IO的性能瓶颈问题。
  • 分表:是为了解决单表数据量太大,sql语句查询数据时,即使走了索引也非常耗时问题。此外还可以解决消耗cpu资源问题。
  • 分库分表:可以解决 数据库连接资源不足、磁盘IO的性能瓶颈、检索数据耗时 和 消耗cpu资源等问题。

如果在有些业务场景中,用户并发量很大,但是需要保存的数据量很少,这时可以只分库,不分表。

如果在有些业务场景中,用户并发量不大,但是需要保存的数量很多,这时可以只分表,不分库。

如果在有些业务场景中,用户并发量大,并且需要保存的数量也很多时,可以分库分表。

  • 11
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值