6

常见接口优化

 2 years ago
source link: https://blog.csdn.net/qq_40739917/article/details/123040981
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

提示:写完文章后,目录可以自动生成,如何生成可参考右边的帮助文档


提示:这里可以添加本文要记录的大概内容:
例如:随着人工智能的不断发展,机器学习这门技术也越来越重要,很多人都开启了学习机器学习,本文就介绍了机器学习的基础内容。


提示:以下是本篇文章正文内容,下面案例可供参考

1、索引失效

1.1没加索引

sql语句中where条件的关键字段,或者order by后面的排序字段,忘了加索引,这个问题在项目中很常见。

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

后来,随着业务的发展,表中数据量越来越多,就不得不加索引了。

可以通过命令:

show index from `表名`;

能单独查看某张表的索引情况。

也可以通过命令:

show create table `表名`;

查看整张表的建表语句,里面同样会显示索引情况。

通过ALTER TABLE命令可以添加索引:

ALTER TABLE `order` ADD INDEX idx_name (name);

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

CREATE INDEX idx_name ON `order` (name);

目前在mysql中如果想要修改索引,只能先删除索引,再重新添加新的。
使用 navicat sql工具可以可视化修改

删除索引可以用DROP INDEX命令:

ALTER TABLE `order` DROP INDEX idx_name;

用DROP INDEX命令也行:

DROP INDEX idx_name ON `order`;

1.2 索引没生效

以使用explain命令,查看mysql的执行计划,它会显示索引的使用情况:

explain select * from `order` where code='002';

在这里插入图片描述
通过这几列可以判断索引使用情况,执行计划包含列的含义如下图所示:
在这里插入图片描述
说实话,sql语句没有走索引,排除没有建索引之外,最大的可能性是索引失效了。

下面说说索引失效的常见原因:
在这里插入图片描述
如果不是上面的这些原因,则需要再进一步排查一下其他原因。

1.3 选错索引

此外,你有没有遇到过这样一种情况:明明是同一条sql,只有入参不同而已。有的时候走的索引a,有的时候却走的索引b?

没错,有时候mysql会选错索引。

必要时可以使用force index来强制查询sql走某个索引。

2、 SQL优化

sql常见优化的15个方法
在这里插入图片描述

3、远程调用、第三方服务

很多时候,我们需要在某个接口中,调用其他服务的接口。

比如有这样的业务场景:

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

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

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

调用过程如下图所示:
在这里插入图片描述

调用远程接口总耗时 530ms = 200ms + 150ms + 180ms

串行调用远程接口性能是非常不好的,调用远程接口总的耗时为所有的远程接口耗时之和。

3.1 并行调用

既然串行调用多个远程接口性能很差,为什么不改成并行呢?
在这里插入图片描述
调用远程接口总耗时 200ms = 200ms(即耗时最长的那次远程接口调用)

在java8之前可以通过实现Callable接口,获取线程返回结果。

java8以后通过CompleteFuture类实现该功能。我们这里以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;
}

温馨提醒一下,这两种方式别忘了使用线程池。示例中用到了executor,表示自定义的线程池,为了防止高并发场景下,出现线程过多的问题。

3.2 数据异构

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

那么,我们能不能把数据冗余一下,把用户信息、积分和成长值的数据统一存储到一个地方,比如:redis,存的数据结构就是用户信息查询接口所需要的内容。然后通过用户id,直接从redis中查询数据出来,不就OK了?

如果在高并发的场景下,为了提升接口性能,远程接口调用大概率会被去掉,而改成保存冗余数据的数据异构方案。
在这里插入图片描述
但需要注意的是,如果使用了数据异构方案,就可能会出现数据一致性问题。

用户信息、积分和成长值有更新的话,大部分情况下,会先更新到数据库,然后同步到redis。但这种跨库的操作,可能会导致两边数据不一致的情况产生。

4、异步处理

比如有个用户请求接口中,需要做业务操作,发站内通知,和记录操作日志。为了实现起来比较方便,通常我们会将这些逻辑放在接口中同步执行,势必会对接口性能造成一定的影响。
在这里插入图片描述
图片示例,发站内通知和用户操作日志功能,对实时性要求不高,即使晚点写库,用户无非是晚点收到站内通知,或者运营晚点看到用户操作日志,对业务影响不大,所以完全可以异步处理。

通常异步主要有两种:多线程mq

使用线程池改造之后,接口逻辑如下:。
在这里插入图片描述

5、避免大事务

使用spring框架开发项目时,为了方便,喜欢使用@Transactional注解提供事务功能。

使用@Transactional注解这种声明式事务的方式提供事务功能,能少写很多代码,提升开发效率。

也容易造成大事务,引发其他的问题。

下面用一张图看看大事务引发的问题。

在这里插入图片描述

优化大事务

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

6、锁粒度

为了防止多个线程并发修改某个共享数据,造成数据异常。

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

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

6.1 synchronized

java中提供了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);
}

6.2 redis分布式锁


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK