我们都知道,在日常开发中我们经常遇到在钉钉群或者在业务群中会出现各种各样的慢业务的接口,比如某个接口在钉钉群疯狂出现,然后就有某些领导艾特你来解决这个慢业务问题,今天阿粉就来说说如何通过各种手段来定位慢业务问题,以及如何解决慢业务的问题。
定位慢业务问题
首先我们先来说这么慢业务问题,一般的慢业务问题,总归就那么几种,SQL 问题,代码业务问题,前端解析问题,前端的解析问题我们就不说了,为什么呢?因为如果是前端解析缓慢的话,身为后端,我们也没什么好的处理办法,但是如果另外的两种情况,那么我们就可以来好好的掰扯一下了。
代码业务问题
那么什么是会出现代码业务问题呢?
循环调用:
这种情况,一般都循环调用同一段代码,每次循环的逻辑一致,前后不关联。比如说,我们要初始化一个列表,预置12个月的数据给前端
List<Model> list = new ArrayList<>();
for(int i = 0 ; i < 12 ; i ++) {
// 计算某个月的数据,逻辑比较复杂,难以批量计算,效率也无法很高
Model model = calOneMonthData(i);
list.add(model);
}
这只是其中来计算某些数据,但是甚至还有人会在循环中去查询一些表的数据,也就是我们通常所说的最不可取的那种 for 循环中有查询。
如果这时候每个月的数据计算相互都是独立的,我们完全可以采用多线程方式进行:
// 建立一个线程池,注意要放在外面,不要每次执行代码就建立一个,具体线程池的使用就不展开了
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);
}
这是循环调用的那种,但是还有其他的,比如出现顺序调用,那么就是执行两个方法,执行方法a,然后再执行方法B,这种情况也是可以进行优化的。
A a = methodA();
B b = methodB();
这时候我们可以使用 JDK8 中的异步编程来实现,
CompletableFuture<A> futureA = CompletableFuture.supplyAsync(() -> methodA());
CompletableFuture<B> futureB = CompletableFuture.supplyAsync(() -> methodB());
CompletableFuture.allOf(futureA,futureB) // 等a b 两个任务都执行完成
这样A B 两个逻辑可以并行执行。
CompletableFuture 这个阿粉就不讲了,为什么呢?因为阿粉在之前的文章中已经详细的讲过了,大家如果有兴趣的话,可以翻看一下。
如果你检查过你的代码之后,你发现并没有能出现慢业务的操作,那么接下来就是重头戏了。
SQL导致的慢业务
SQL导致的慢业务,这个是七成以上的开发都会遇到的问题。因为有百分之70左右的慢业务都是因为自己的慢SQL引起的。
那么我们该怎么去定位这个慢SQL呢?
慢查询日志记录慢SQL
定位慢SQL可以通过慢查询日志来查看慢SQL,默认的情况下,MySQL数据库不开启慢查询日志(slow query log),需要手动把它打开
SET GLOBAL slow_query_log = ‘ON’;
查看下慢查询日志配置
SHOW VARIABLES LIKE ‘slow_query_log%’
- slow_query_log:表示慢查询开启的状态
- slow_query_log_file:表示慢查询日志存放的位置
explain查看分析SQL执行计划
当我们去定位自己表中增加的索引有没有生效的时候,我们使用的一半都是 explain 关键字,通过关键字给我们返回的内容,我们就能判断我们写的SQL 有没有命中索引。
那么他反馈的参数分别都是什么意思呢?
- id
- id 值相同时,被视为一组从上向下执行。
- 如果是子查询,id 值会递增,id 值越高,优先级越高
- id为NULL最后执行
- select_type
- simple: 简单的select, 查询中不包含子查询或者 union。例如: select name from student where id= 100
- primary: 子查询中最外层查询, 查询中若包含任何复杂的子部分, 最外层的select被标记为primary
- derived:在 from 的列表中包含的子查询被标记成 derived(派生表)。例如: explain select id from (select id,name from student) student1 where name= ‘name100’
- subquery:在 select 或 where 列表中包含了子查询,则子查询被标记成 subquery。例如: explain select id from student where score = (select score from student where name=‘name100’);
- union: union中的第二个或后面的select语句. 例如: EXPLAIN select id from student where id<12691055 UNION all select id from student where id<12691060;
- table
显示这一步所访问数据库中表名称. 有时候不是真实的表名, 可能是简称
- partitions
该字段看table所在的分区, 值为NULL表示表未被分区
- possible_keys
可能会使用到的索引
- type
表示连接类型,查看索引执行情况的一个重要指标 以下性能从好到坏依次:system > const > eq_ref > ref >ref_or_null > index_merge > unique_subquery > index_subquery > range >index > ALL
system:这种类型要求数据库表中只有一条数据,是const类型的一个特例,一般情况下是不会出现的
const:通过一次索引就能找到数据,一般用于主键或唯一索引作为条件,这类扫描效率极高,速度非常快
eq_ref:常用于主键或唯一索引扫描,一般指使用主键的关联查询 ref : 常用于非主键和唯一索引扫描
ref_or_null:这种连接类型类似于ref,区别在于MySQL会额外搜索包含NULL值的行
index_merge:使用了索引合并优化方法,查询使用了两个以上的索引
unique_subquery:类似于eq_ref,条件用了in子查询
index_subquery:区别于unique_subquery,用于非唯一索引,可以返回重复值
range:常用于范围查询,比如:between … and 或 In 等操作
index:全索引扫描
ALL:全表扫描
- key
实际使用到的索引
- key_len
实际使用到的索引的长度
- rows
该列表示MySQL估算找到我们所需的记录,需要读取的行数
- filtered
该列是一个百分比,是满足条件的记录数量与我们查询了多少记录数量的比值
- extra
该字段包含有关MySQL如何解析查询的其他信息,它一般会出现这几个值:
Usingfilesort:表示按文件排序,一般是在指定的排序和索引排序不一致的情况才会出现,一般见于order by语句
Using index:表示是否用了覆盖索引
Using temporary: 表示是否使用了临时表,性能特别差,需要重点优化,一般多见于groupby语句,或者union语句
Using where : 表示使用了where条件过滤
Using index condition:MySQL5.6之后新增的索引下推,在存储引擎层进行数据过滤,而不是在服务层过滤,利用索引现有的数据减少回表的数据
这个关键字是非常需要大家掌握的,因为能非常准确的反映出你写的 SQL 语句到底有没有命中索引,如果你的 SQL 都没有命中索引的话,那么就可以从你的 SQL 上下手来解决这个慢业务的问题了。
你学会怎么定位慢业务问题了么?