前言:
是這樣的,這周三我在測試一個接口的時候,發現竟然超時了。我們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條了,那怎么辦?
求各位老哥給一個建議。