接口调优的大致思路

5 篇文章 1 订阅
5 篇文章 1 订阅

接口调优的大致思路

前言:本文仅根据自己所知,列出大致思路,若有不当之处,请不吝赐教

要解决接口查询缓慢的问题,首先要清楚从请求发出到返回,经过了哪些过程。

1、线程池配置

后端接收到请求,首先是后端分配线程处理,而该线程是SpringBoot默认的线程池中的线程。线程池的默认配置在文件TaskExecutionProperties.class中可以看到,大致如下:

private int queueCapacity = 2147483647; // 队列最大容量
private int coreSize = 8; // 核心线程数
private int maxSize = 2147483647; //最大线程数
private Duration keepAlive = Duration.ofSeconds(60L); // 非核心线程存活时长

假设8个核心线程都正在处理请求,再来新的请求,会进入队列等候,直到核心线程有空出,或者队列满了,线程池会开启非核心线程,来处理队列中等候的任务,但是默认线程池的队列这么长,相比之下,核心线程有空出的几率大一些。如果这8个核心线程的任务处理都耗时2秒,新的请求,只能等两秒,所以合理设置参数,是优化接口的关键一步。

可通过以下方式设置SpringBoot中的线程池配置,配置值需要根据实际情况调整

@Bean
public Executor threadPool() {
   ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
   //最大线程数
   executor.setMaxPoolSize(Runtime.getRuntime().availableProcessors() * 2);
   //核心线程数
   executor.setCorePoolSize(Runtime.getRuntime().availableProcessors());
   //任务队列的大小
   executor.setQueueCapacity(100);
   //线程池名的前缀
   executor.setThreadNamePrefix("threadpool-prefix");
   //允许线程的空闲时间300秒
   executor.setKeepAliveSeconds(5 * 60);
   //设置线程池关闭的时候等待所有任务都完成再继续销毁其他的Bean
   executor.setWaitForTasksToCompleteOnShutdown(true);
   //设置线程池中任务的等待时间,如果超过这个时候还没有销毁就强制销毁,以确保应用最后能够被关闭,而不是阻塞住
   executor.setAwaitTerminationSeconds(60);
   // 线程池拒绝策略,让调用的线程自己处理
   executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
   //线程初始化
   executor.initialize();
   return executor;
}

2、业务系统/处理

请求被线程池被分配了线程后,线程运行的就是业务代码,就是通常的mvc模式:controller -> service -> mapper。该过程是个人开发能力的体现,针对业务处理,有几个注意点:

2.1、避免循环IO

只要是有网络传输的,都含有IO操作,例如:http请求,数据库查询,redis查询,dubbo请求等,只要涉及不同服务器的数据传输,都含IO操作。而耗时方面,IO操作>内存操作>CPU操作,耗时在十甚至百倍以上,代码中的数据,都是存储在内存中。

比如有一个用户,有10个收货地址,可以通过用户id一次性查出所有的收货地址(以空间换时间),比花10次查数据库要省时得多。更新、修改、删除同理,如果可以批量,都尽量使用批量处理。

注:redis查询,也属于IO操作

2.2、使用异步处理

2.2.1、接口异步处理

SpringBoot可以通过Callable、DeferredResult或者WebAsyncTask等方式实现接口异步请求

2.2.2、业务异步处理

有些操作不影响主体流程的调用,可以使用异步处理,如发送短信通知,假设有一个请求,要插入业务数据,插入成功后,要求发送短信,发送短信的操作,可以使用异步,@EnableAsync、@EventListener + @Async、消息队列等方式可以帮助完成异步处理。

2.2.3、CompletableFuture

有一种场景,使用该方式,有很好的效果,例如,操作3要等操作1和操作2都执行完才能执行,但操作1和操作2互不影响,CompletableFuture可以很方便的帮助我们完成,CompletableFuture还有很多其他使用的场景。

2.3、避免Full GC

性能良好的 JVM,Full GC的频率较低,Full GC会导致Stop The World情况的出现,严重影响性能。频繁Full GC,会导致系统频繁卡死。所以如果你的接口慢,可能并不是你的问题,而是其他地方的代码不规范,导致频繁Full GC。

2.4、数据缓存

将经常访问的数据放在redis,redis为内存数据库,比mysql查找数据快一些

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

集成配置如下(首先要集成redis)

@Configuration
public class CacheConfig extends CachingConfigurerSupport {

    private static final String SYSTEM_CACHE_REDIS_KEY_PREFIX = "china:shanghai";

    @Autowired
    private RedisConnectionFactory redisConnectionFactory;

    @Autowired
    private RedisTemplate redisTemplate;

    @Bean
    @Override
    public CacheManager cacheManager() {
        // 默认30分钟过期
        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()               .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(redisTemplate.getValueSerializer()))
                .computePrefixWith(cacheKeyPrefix()).entryTtl(Duration.ofMinutes(30));
        RedisCacheManager cacheManager = RedisCacheManager.builder(RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory)).cacheDefaults(redisCacheConfiguration).build();
        return cacheManager;
    }

    @Bean
    public CacheKeyPrefix cacheKeyPrefix() {
        return cacheName -> {
            StringBuilder sb = new StringBuilder(100);
            sb.append(SYSTEM_CACHE_REDIS_KEY_PREFIX).append(":");
            sb.append(cacheName).append(":");
            return sb.toString();
        };
    }
}

3、数据库

3.1、连接池配置

说到数据库连接池,先简单聊聊对“池化”技术,池化技术指的是提前准备一些资源,在需要时可以重复使用这些预先准备的资源。平时开发中,见得最多的是线程池,数据库连接池,http连接池,redis连接池等。还有比较少见的对象池等,apache有现成的工具包辅助完成池化技术。

<dependency>
	<groupId>org.apache.commons</groupId>
	<artifactId>commons-pool2</artifactId>
</dependency>

数据库连接池,跟线程池类似,只不过线程池针对的是http请求,而数据库连接池,针对的是SQL查询。SpringBoot默认使用的数据库连接池是Hikari,Hikari的默认配置,在文件HikariConfig中可以查看,可通过设置application.peroerties文件配置,如下所示

spring.datasource.minimum-idle=5
spring.datasource.maximum-pool-size=20
spring.datasource.hikari.auto-commit=true
spring.datasource.hikari.idle-timeout=600000
spring.datasource.hikari.pool-name=DatebookHikariCP
spring.datasource.hikari.max-lifetime=1800000
spring.datasource.hikari.connection-timeout=120000

假设使用的默认配置,同时有40个SQL查询,则会有20个查询需要等前面的20个查询完成,才能执行后20个,这时可以适当调大连接池大小,但该值不能无限制,最大不能超过数据库配置的最大会话连接数,否则(MySQL)会提示:Too many connections,可通过"set GLOBAL max_connections=1000;"设置最大连接数。如果数据库只被一个应用使用,可以占满全部会话连接数,如果其他应用也在使用,需要做适当调整,数据库的最大连接数是16384。

3.2、SQL优化

SQL优化,并不是一上来就建立索引,如果表中或者返回的结果没有大字段,200w的单表,查询应该是没有什么问题的。建立索引后,2000w数据以上,性能会出现严重下将,阿里巴巴《Java 开发手册》提出单表行数超过 500 万行或者单表容量超过 2GB,推荐进行分库分表。具体情况,还是要具体分析,有些历史数据,可能查询的概率极低,可以考虑冷热分离的方式,将超过指定时间的数据,移入冷表。热表只放查询频率较高的数据。

针对SQL优化,有以下几点方式:

3.2.1、缩小联表范围

该方式在联表查询时,比较好用。假设有表A(500w),表B(500w),需要进行联表查询,联表条件(ON):A.id = B.aId, 查询条件(where):A.name = ‘张三’ and A.createTime > ‘2022-01-01’ and B.name = ‘李四’ and B.createTime > ‘2022-01-01’, 则:

select * from (select * from A where name = ‘张三’ and createTime > ‘2022-01-01’) a inner join (select * from B where name = ‘李四’ and createTime > ‘2022-01-01’) b on a.id = b.aId

优于

select * from A a inner join B b on a.id = b.aId where a.name = ‘张三’ and a.createTime > ‘2022-01-01’ and b.name = ‘李四’ and b.createTime > ‘2022-01-01’;

尽可能小的减少联表的记录数,虽然查询的语句比较复杂,但数据量较大时,效率客观。

3.2.2、减少返回字段

当表的数据量较大时,尽可能的减少select的字段,比如3.2.1中的sql,select * from A = select col1,col2…col100 from A,返回查询需要的字段即可,中间表,派生表都一样,效率可观。

3.2.3、选择合适的字段类型

1> 更小的通常更好
2> 尽量避免NULL

例如:int 优于DECIMAL,如果是两位小数,可以将DECIMAL类型*100,TIMESTAMP优于DATETIME,枚举、int优于字符串等。

3.2.4、使用自增主键

无论是分页,还是插入数据,自增主键都有优势,且占用空间,比UUID更小,另外大表分页优化,自增主键优化效率很高。

3.2.5、创建索引

单表索引数量不宜太多,因为索引也是占用空间的,而且索引越多,插入数据效率也会更低。索引类型有如下几类:

1> 聚簇索引,简单理解就是索引跟数据是在一起的,mysql的主键就是聚簇索引。如果索引是一棵树,聚簇索引的数据就是挂在树叶上的,innodb不能单独建立聚簇索引,一般默认以主键为聚簇索引。

2> 非聚簇索引,索引没跟数据放在一起,非聚簇索引查询完数据后,需要回表查询,才能找到对应的数据,可以理解为非聚簇索引存放的数据是索引。

说明:因为要回表,所以非聚簇索引的查询速度,比聚簇索引慢

3> 唯一索引

创建索引的列,不可以有相同值,唯一索引效果较好(利用唯一索引,可以去重)

4> 普通索引

创建索引的列,可以有相同值,如果某列的相同值过多,则不建议创建索引,因为效果不佳,例如性别,只有男、女、未知。

5> 组合索引

不要求唯一,方便查询,在联表查询时,将联表的字段(ON建立关联条件的字段)建立组合索引,效果极佳

6> 覆盖索引

查询列要被创建的索引覆盖,不必回表读取数据行,覆盖索引,可以避免回标查询,效率较高

索引建好后,应该使用规范,避免索引失效,这样不仅浪费了空间,还没达到想要的效果,避免索引失效,大概有一下注意点:

1、索引存在转换操作

1.1、显示调用方法,再与条件做比较,比如concat(col1,col2) = ‘中国台湾’,或col2 + ‘1’ = ‘2221’,+ 也可以看作函数

1.2、隐式转换,表的字段col1是int类型,结果比较时:col1=‘1’,是mysql将字段col1转成了字符串,再和‘1’做比较的

2、索引没有从头开始匹配

2.1、like,column like ‘%张三’ 或 column like ‘_张三’

2.2、组合索引(a,b,c)column2 = b and column3 = c

3、or连接的条件,存在不是索引的列

col1,col2建立了唯一索引,col3没有建立索引,col1 = 1 or col2 = 2,索引不会失效,col1 = 1 or col3 = 3,索引会失效

4、左连接或右连接字段,编码不一样

5、in语法,col1 in (‘1’,‘2’…‘n’),col1建立了索引,如果n大于表总记录的30%(由优化器决定,大概是30%),就不走索引,小于就走索引,Oracle的in语法,有数量限制,默认是1000个

3.3、分区

mysql分区表总共有四种类型range,list,hash,key。

1、range,基于属于一个给定连续区间的列值,把多行分配给分区。一般会使用时间的字段来作为分区的列,记录每天的数据。

2、list,list和range类似,区别在于list是离散型数值,range是连续的。

3、hash,基于用户定义的表达式的返回值来进行选择的分区,该表达式使用将要插入到表中的这些行的列值进行计算

4、key,key分区允许多列,而hash分区只允许一列

3.4、分表

数据库即使建立索引,在数据量较大时,也会存在瓶颈,此时可以分表,分表详见基于ShardingSphere-JDBC的MySQL分库分表

3.5、读写分离

读写分离架构出现的原因是因为在读远远比写多的情况下,为了缓解主库的压力,将大多数读请求压力均摊到多个从库上

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值