常见接口优化

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

文章目录

  • 前言
  • 1、索引失效
    • 1.1没加索引
    • 1.2 索引没生效
    • 1.3 选错索引
  • 2、 SQL优化
  • 3、远程调用、第三方服务
    • 3.1 并行调用
    • 3.2 数据异构
  • 4、异步处理
  • 5、避免大事务
  • 6、锁粒度
    • 6.1 synchronized
    • 6.2 redis分布式锁


前言

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


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

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';

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

下面说说索引失效的常见原因:
常见接口优化_第2张图片
如果不是上面的这些原因,则需要再进一步排查一下其他原因。

1.3 选错索引

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

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

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

2、 SQL优化

sql常见优化的15个方法
常见接口优化_第3张图片

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

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

比如有这样的业务场景:

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

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

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

调用过程如下图所示:
常见接口优化_第4张图片

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

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

3.1 并行调用

既然串行调用多个远程接口性能很差,为什么不改成并行呢?
常见接口优化_第5张图片
调用远程接口总耗时 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了?

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

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

4、异步处理

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

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

使用线程池改造之后,接口逻辑如下:。
常见接口优化_第8张图片

5、避免大事务

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

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

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

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

常见接口优化_第9张图片

优化大事务

  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分布式锁

你可能感兴趣的:(SpringBoot,java,mysql,数据库,database)