用SpringAOP最佳化IN查詢,效率提升巨大!

👉 這是一個或許對你有用的社群
🐱 一對一交流/面試小冊/簡歷最佳化/求職解惑,歡迎加入芋道快速開發平臺知識星球。下面是星球提供的部分資料:
👉這是一個或許對你有用的開源專案
國產 Star 破 10w+ 的開源專案,前端包括管理後臺 + 微信小程式,後端支援單體和微服務架構。
功能涵蓋 RBAC 許可權、SaaS 多租戶、資料許可權、商城、支付、工作流、大屏報表、微信公眾號、ERPCRMAI 大模型等等功能:
  • Boot 多模組架構:https://gitee.com/zhijiantianya/ruoyi-vue-pro
  • Cloud 微服務架構:https://gitee.com/zhijiantianya/yudao-cloud
  • 影片教程:https://doc.iocoder.cn
【國內首批】支援 JDK 17/21 + SpringBoot 3.3、JDK 8/11 + Spring Boot 2.7 雙版本 

前言

我們在專案中經常遇到IN查詢,同時IN的引數太多甚至大幾百上千,會導致PG效能下降嚴重進而介面反應太慢。這個應該是前期沒規劃好,但是事已至此還是要對此進行最佳化。第一個就是想到透過多執行緒去查,比如原來是
SELECT

 * 

FROM

 device 

WHEREidIN

 (

1

2

3

4

)

拆分為
SELECT

 * 

FROM

 device 

WHEREidIN

 (

1

2

SELECT

 * 

FROM

 device 

WHEREidIN

 (

3

4

)

並行執行,然後將返回結果合併。
因為用的地方多,每次都要寫很麻煩,所以結合SpringAOP寫了一個基於註解最佳化方案,只需要打上註解就可以提升效能了。實現效果以及具體實現邏輯如下:
@SplitWorkAnnotation

(setThreadPool = LIST_DEVICE_EXECUTOR, splitLimit = 

20

, splitGroupNum = 

10

)

publiclistDeviceDetail(Long projectId,@NeedSplitParam List<Long> deviceId)

{

......

}

適用場景和不適用場景
主要適用大批次IN查詢,或者某個引數特別大導致效能問題的同時結果能簡單合併的,就是說符合以下公式的:

fun(a,b,bigList) = fun(a,b,bigListPart1) + fun(a,b,bigListPart2)

這裡的加可以是:合併運算,SUM,COUNT以及求TOPN(合併後再取TOPN)
不適用的典型場景有分頁以及不符合上面公式的場景
基於 Spring Boot + MyBatis Plus + Vue & Element 實現的後臺管理系統 + 使用者小程式,支援 RBAC 動態許可權、多租戶、資料許可權、工作流、三方登入、支付、簡訊、商城等功能
  • 專案地址:https://github.com/YunaiV/ruoyi-vue-pro
  • 影片教程:https://doc.iocoder.cn/video/

定義AOP註解

需要定義的註解引數:
  • setThreadPool: 執行緒池,可能阻塞比較大,不要用公共的執行緒池最好自己定義一個
  • handlerReturnClass: 返回值回撥函式,對應不同返回值處理邏輯:可能是合併可能取前十條可能求和
  • splitLimit: 超過多少需要拆分
  • splitGroupNum: 拆分時每組多少個
@Target

(ElementType.METHOD)

@Retention

(RetentionPolicy.RUNTIME)

public@interface

 SplitWorkAnnotation {

/**

     * 設定執行緒池

     *

     * 

@return

 {

@link

 ThreadPoolEnum}

     */


ThreadPoolEnum setThreadPool()

;

/**

     * 返回值處理

     *

     * 

@return

 {

@link

 Class}<{

@link

 ?} {

@link

 extends} {

@link

 HandleReturn}>

     */

    Class<? extends HandleReturn> handlerReturnClass() 

default

 MergeFunction

.class

;

/**

     * 超過多少開始拆分 >

     *

     * 

@return

 int

     */


intsplitLimit()default 1000

;

/**

     * 拆分後每組多少

     *

     * 

@return

 int

     */


intsplitGroupNum()default 100

;

}

標記需要拆分引數的註解
加在需要拆分的引數上,只支援一個。因為兩兩組合情況非常複雜,也一般不符合實際使用情況。
@Retention

(RetentionPolicy.RUNTIME)

@Target

(ElementType.PARAMETER)

public@interface

 NeedSplitParam {
}

基於 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 實現的後臺管理系統 + 使用者小程式,支援 RBAC 動態許可權、多租戶、資料許可權、工作流、三方登入、支付、簡訊、商城等功能
  • 專案地址:https://github.com/YunaiV/yudao-cloud
  • 影片教程:https://doc.iocoder.cn/video/

使用AOP實現拆分多執行緒併發呼叫合併邏輯

@Aspect
@Component
@Slf

4j

publicclassSplitWorkAspect

{

/**

     * 切入點表示式,攔截方法上有

@NeedSplitParaAnnotation

註解的所有方法

     *

     * 

@return

 void

     * 

@author

 tangsiqi

     * 

@date

 2021/8/9 18:17

     */


@Pointcut

(

"@annotation( com.demo.SplitWorkAnnotation)"

)

publicvoidneedSplit()

{

    }

/**

     * 

@param

 pjp

     * 

@return

 {

@link

 Object}

     * 

@throws

 Throwable

     */


@Around

(

"needSplit()"

)

public Object around(ProceedingJoinPoint pjp)throws Throwable 

{

        Signature signature = pjp.getSignature();

        MethodSignature methodSignature = (MethodSignature) signature;

        Method targetMethod = methodSignature.getMethod();

        SplitWorkAnnotation splitWorkAnnotation = targetMethod.getAnnotation(SplitWorkAnnotation

.class)

;

        Object[] args = pjp.getArgs();

int

 splitLimit = splitWorkAnnotation.splitLimit();

int

 splitGroupNum = splitWorkAnnotation.splitGroupNum();

if

 (args == 

null

 || args.length == 

0

 || splitLimit <= splitGroupNum) {

return

 pjp.proceed();

        }

int

 needSplitParamIndex = -

1

;

for

 (

int

 i = 

0

; i < targetMethod.getParameters().length; i++) {

            Parameter parameter = targetMethod.getParameters()[i];

            NeedSplitParam needSplitParam = parameter.getAnnotation(NeedSplitParam

.class)

;

if

 (needSplitParam != 

null

) {

                needSplitParamIndex = i;

break

;

            }

        }

if

 (needSplitParamIndex == -

1

) {

return

 pjp.proceed();

        }

        Object needSplitParam = args[needSplitParamIndex];

//只能處理Object[] 和 Collection
if

 (!(needSplitParam 

instanceof

 Object[]) && !(needSplitParam 

instanceof

 List) && !(needSplitParam 

instanceof

 Set)) {

return

 pjp.proceed();

        }

//如果目標引數長度小於拆分下限跳過
boolean

 notMeetSplitLen = (needSplitParam 

instanceof

 Object[] && ((Object[]) needSplitParam).length <= splitLimit)

                || (needSplitParam 

instanceof

 List && ((List) needSplitParam).size() <= splitLimit)

                || (needSplitParam 

instanceof

 Set && ((Set) needSplitParam).size() <= splitLimit);

if

 (notMeetSplitLen) {

return

 pjp.proceed();

        }

// 去重,這一步看情況也可以不要
if

 (needSplitParam 

instanceof

 List) {

            List<?> list = (List<?>) needSplitParam;

if

 (list.size() > 

1

) {

                needSplitParam = 

new

 ArrayList<>(

new

 HashSet<>(list));

            }

        }

//算出拆分成幾批次
int

 batchNum = getBatchNum(needSplitParam, splitGroupNum);

if

 (batchNum == 

1

) {

return

 pjp.proceed();

        }

        CompletableFuture<?>[] futures = 

new

 CompletableFuture[batchNum];

        ThreadPoolEnum threadPool = splitWorkAnnotation.setThreadPool();

if

 (threadPool == 

null

) {

return

 pjp.proceed();

        }

try

 {

for

 (

int

 currentBatch = 

0

; currentBatch < batchNum; currentBatch++) {

int

 finalNeedSplitParamIndex = needSplitParamIndex;

int

 finalCurrentBatch = currentBatch;

                Object finalNeedSplitParam = needSplitParam;

                futures[currentBatch] = CompletableFuture.supplyAsync(() -> {

                    Object[] dest = 

new

 Object[args.length];

//這一步很重要!!!因為多執行緒執行不能用原理的引數列表了,不然會導致混亂

                    System.arraycopy(args, 

0

, dest, 

0

, args.length);

try

 {

//將其他引數保持不變,將需要拆分的引數替換為part引數

                        dest[finalNeedSplitParamIndex] = getPartParam(finalNeedSplitParam, splitGroupNum, finalCurrentBatch);

return

 pjp.proceed(dest);

                    } 

catch

 (Throwable e) {

thrownew

 RuntimeException(e);

                    }

                }, threadPool.getThreadPoolExecutor());

            }

            CompletableFuture<Void> all = CompletableFuture.allOf(futures);

            all.get();

            Class<? extends HandleReturn> handleReturn = splitWorkAnnotation.handlerReturnClass();
            List<Object> resultList = 

new

 ArrayList<>(futures.length);

for

 (CompletableFuture<?> future : futures) {

                resultList.add(future.get());

            }

//獲取到每個part的結果然後呼叫處理函式
return

 handleReturn.getDeclaredMethods()[

0

].invoke(handleReturn.getDeclaredConstructor().newInstance(), resultList);

        } 

catch

 (InterruptedException | ExecutionException e) {

thrownew

 RuntimeException(e);

        }

    }

/**

     * 獲取批次數目

     *

     * 

@param

 needSplitParam1

     * 

@param

 splitGroupNum

     * 

@return

 {

@link

 Integer}

     */


public Integer getBatchNum(Object needSplitParam1, Integer splitGroupNum)

{

if

 (needSplitParam1 

instanceof

 Object[]) {

            Object[] splitParam = (Object[]) needSplitParam1;

return

 splitParam.length % splitGroupNum == 

0

 ? splitParam.length / splitGroupNum : splitParam.length / splitGroupNum + 

1

;

        } 

elseif

 (needSplitParam1 

instanceof

 Collection) {

            Collection<?> splitParam = (Collection<?>) needSplitParam1;

return

 splitParam.size() % splitGroupNum == 

0

 ? splitParam.size() / splitGroupNum : splitParam.size() / splitGroupNum + 

1

;

        } 

else

 {

return1

;

        }

    }

/**

     * 獲取當前批次引數

     *

     * 

@param

 needSplitParam

     * 

@param

 splitGroupNum

     * 

@param

 batch

     * 

@return

 {

@link

 Object}

     * 

@throws

 NoSuchMethodException

     * 

@throws

 InvocationTargetException

     * 

@throws

 InstantiationException

     * 

@throws

 IllegalAccessException

     */


public Object getPartParam(Object needSplitParam, Integer splitGroupNum, Integer batch)throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException 

{

if

 (needSplitParam 

instanceof

 Object[]) {

            Object[] splitParam = (Object[]) needSplitParam;

int

 end = Math.min((batch + 

1

) * splitGroupNum, splitParam.length);

return

 Arrays.copyOfRange(splitParam, batch * splitGroupNum, end);

        } 

elseif

 (needSplitParam 

instanceof

 List) {

            List<?> splitParam = (List<?>) needSplitParam;

int

 end = Math.min((batch + 

1

) * splitGroupNum, splitParam.size());

return

 splitParam.subList(batch * splitGroupNum, end);

        } 

elseif

 (needSplitParam 

instanceof

 Set) {

            List splitParam = 

new

 ArrayList<>((Set) needSplitParam);

int

 end = Math.min((batch + 

1

) * splitGroupNum, splitParam.size());

//引數具體化了

            Set<?> set = (Set<?>) needSplitParam.getClass().getDeclaredConstructor().newInstance();

            set.addAll(splitParam.subList(batch * splitGroupNum, end));

return

 set;

        } 

else

 {

returnnull

;

        }

    }

}

定義處理返回值的介面

/**

 * 處理返回結果介面

 *

 * 

@author

: TangSiQi

 * 

@date

: 2021年08月13日15:42

 **/


publicinterfaceHandleReturn

{

/**

     * 處理返回結果方法

     *

     * 

@param

 t 拆分後多次請求結果

     * 

@return

 R 處理後的返回結果

     * 

@author

 tangsiqi

     * 

@date

 2021/8/13 15:55

     */


Object handleReturn(List t)

;

}

實現了一個簡單合併的

/**

 * 集合List等合併策略

 *

 * 

@author

: TangSiQi

 * 

@date

: 2021年08月13日15:32

 **/


publicclassMergeFunctionimplementsHandleReturn

{

@Override
public Object handleReturn(List results)

{

if

 (results == 

null

) {

returnnull

;

        }

if

 (results.size() <= 

1

) {

//todo
return

 results.get(

0

);

        }

//這裡自己要知道具體返回型別

        List first = (List) results.get(

0

);

for

 (

int

 i = 

1

; i < results.size(); i++) {

            first.addAll((List) results.get(i));

        }

return

 first;

    }

}

有用的話希望大佬們點點贊,點點關注!

歡迎加入我的知識星球,全面提升技術能力。
👉 加入方式,長按”或“掃描”下方二維碼噢
星球的內容包括:專案實戰、面試招聘、原始碼解析、學習路線。
文章有幫助的話,在看,轉發吧。
謝謝支援喲 (*^__^*)

相關文章