记一次线上优化实战
前言:
是这样的,这周三我在测试一个接口的时候,发现竟然超时了。我们RPC框架用的DUBBO,我超时设置的时间为 timeout=3s。
按照道理,一个方法超过3s,对用户是非常不友好的,用户会立马会感觉是反应十分的慢。
所以进行排查 + 优化
排查一阶段:
因为这个方法中,有很多个小方法,大概如下:
因为不知道哪个小方法执行的特比慢,所以首先想到了,计算每个小方法的时间。
于是乎写写写,写出了下面的代码
1 @Aspect 2 @Component 3 @Slf4j 4 public class TimeWatchAspect { 5 6 @Pointcut(value = "execution(* com.tianya..*(..))") 7 public void log() { 8 9 } 10 11 @Around(value = "log()") 12 public void test(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { 13 Stopwatch stopwatch = Stopwatch.createStarted(); 14 log.info("开始计算时间"); 15 proceedingJoinPoint.proceed(); 16 long duration = stopwatch.elapsed(TimeUnit.MILLISECONDS); 17 log.info("结束计算时间, 花费时间为:{}", duration); 18 } 19 }
一个简单的AOP,拦截所有请求,并计算方法。
我以为大事万吉了。没想到!!!
该AOP只会拦截这个方法,而不会拦截里面的小方法,我十分的疑惑,然后掉入了一个更大的坑中去了。
这个我稍微说下,为啥这是一个大坑。
Spring的AOP拦截,其实是为这个类生成了代理类。比如类A,为类A生成了一个ProxyA类。然后为每个方法加上AOP的before,AOP的after等等
但一个方法中调用另一个方法其实是:this.x();
那么假设代码如下:
1 class A { 2 public void a() { 3 System.out.println("我是a方法"); 4 b(); 5 } 6 7 private void b() { 8 System.out.println("我是b方法"); 9 } 10 } 11 12 class ProxyA { 13 public void a() { 14 aopBefore(); 15 System.out.println("我是a方法"); 16 A.b(); 17 aopAfter(); 18 } 19 }看懂了嘛?a方法里面的b方法,调用的还是旧的A类中的b方法,所以AOP不会进行一个拦截。
真的是一个大坑,我查了很多资料,没有特别好的解决办法。最后只能自己注入自己,然后调用即可,但正式不建议用,太影响业务逻辑了,而且主要是!!!代码太丑了!!!会被后人骂死的。
最后经过一个函数一个函数的排查。排查到了一条查询数据库语句特别特别的慢,竟然达到了3-5s,这尼玛能忍受。
排查二阶段:
我们首先看这条SQL语句
数据库大概200w+的数据,根据条件查询大概花费了5s+,基本稳定在3s左右。条件shopId有索引,初步判断是查询出来的数据量太大了,
每个JSON字段,太占用内存了。但Mybatis逆向工程默认全部查询出来,这一点是非常不友好的,即使我只用到一两个字段
我们再看下面的这条SQL语句:
毫秒级别,不用说了吧。而且我只需要这个id字段即可,不需要其他的字段。既然找到了原因那么就动手解决它吧。
解决:
自己新建一个Mapper文件,如下即可:
1 <?xml version="1.0" encoding="UTF-8"?> 2 <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> 3 <mapper namespace="com.tianya.items.mapper.ItemsMapperSpeed"> 4 5 <!-- 慢sql优化 --> 6 <resultMap id="ShopMap" type="java.util.HashMap"> 7 <id column="id" jdbcType="INTEGER" property="id" /> 8 </resultMap> 9 <select id="selectItemIdByShopId" parameterType="com.tianya.items.entity.ItemsExample" resultMap="ShopMap"> 10 select id from items 11 <if test="_parameter != null"> 12 <include refid="Example_Where_Clause" /> 13 </if> 14 <if test="orderByClause != null"> 15 order by ${orderByClause} 16 </if> 17 <if test="limit != null"> 18 <if test="offset != null"> 19 limit ${offset}, ${limit} 20 </if> 21 <if test="offset == null"> 22 limit ${limit} 23 </if> 24 </if> 25 </select> 26 </mapper>
我们看到xml文件的第10行,就是只把id查出来,而不查询全部。优化开始了
然后建立一个与Mapper映射的类
1 public interface ItemsMapperSpeed { 2 /** 3 * 这个属于慢sql优化 对应ItemsMapperSpeed.xml文件 4 * @yizhen 5 * @return 6 */ 7 List<Map<String, Object>> selectItemIdByShopId(ItemsExample example); 8 }
最后直接调用即可
结果:
优化前基本稳定在5s+,优化后基本稳定在1-2s+。还是太慢了。原因大概有以下几个原因:
1:数据库表设计太多。大概涉及到了5张表左右,其中2张表达到了200w+的级别,3张表达到了20W+的级别
2:其中很多处业务都需要查表,每次查表都是查询出全部字段,而不是特定的字段,查询十分的慢
3:业务复杂。后期排查代码大觉一个方法里面调用了两三个方法,两三个方法又调用了两三个方法。时间复杂度基本在O(N^2)级别,个别方法甚至达到了O(N^3),这点是非常不好的
4:自己代码写的太搓了,挫的一批
优化思路:
如果这个接口特别的慢,我主要会从以下思路进行优化:
1:SQL层面 ==> SQL语句搓,进行SQL语句的优化。实在不行,加一个缓存,过期一般60s即可,会快特比的多。特别建议ehcache比较好用
2:代码层面 ==> 类似HashMap, HashSet都可以达到O(1)级别的时间复杂度,可以多考虑用空间换时间的策略。
3:代码尽量写工整易懂,导师天天说我代码写的搓。也是很不错的一个优化
后记:
问大家一个问题,就是我现在的业务场景。
一个按照条件搜索的需求,涉及的表大概有5-6张,每张表大概达到了百万的级别,按照条件进行筛选查找。
这种肯定是不能用join的,那么怎么做效率才比较高呢?
如果我不从每张表把数据取出来,最后按照其他条件筛选,那么可能筛选都不符合0条了,那怎么办?
求各位老哥给一个建议。