5年華為架構師1小時把SpringBoot專案併發提升了10倍,網友:牛掰

今日分享開始啦,請大家多多指教~

場景:一次迭代在灰度環境發版時,測試反饋說我開發的那個功能,查詢介面有部分欄位資料是空的,後續排查日誌,發現日誌如下:

feign。RetryableException: cannot retry due to redirection, in streaming mode executing POST

5年華為架構師1小時把SpringBoot專案併發提升了10倍,網友:牛掰

下面是業務、環境和分析過程下面是業務、環境和分析過程:

介面的業務場景 :我這個介面類似是那種報表統計的介面,它會請求多個微服務,把請求到的資料,統一返回給前端,相當於設計模式中的門面模式了。

後續由於這個介面 是序列請求其他微服務的,速度有些慢,後面修改程式碼從序列請求,改成並行(多執行緒)獲取資料。

運維那邊是透過判斷http請求中cookie 或者 header中的某個資料,來區分請求是否要把流量打到灰度。

分析得出:應該是介面非同步請求的時候cookie丟失,沒走到灰度環境,找不到 這次迭代新開發的介面,導致的重定向到錯誤頁面了。

驗證:由於我程式碼是透過@Async非同步註解,實現並行請求的,臨時把五個介面的非同步註解註釋掉了,灰度在發版驗證,資料能返回正常,說明流量打到灰度了。

說明問題就是併發請求的時候,子執行緒獲取不到主執行緒的request 頭資訊,導致沒有走到灰度。

下圖就是灰度環境的流程圖:

5年華為架構師1小時把SpringBoot專案併發提升了10倍,網友:牛掰

問題定位出來了,解決方案就是:讓子執行緒能獲取到主執行緒的 request 頭資訊,主執行緒把 資料透傳到子執行緒。

我使用的是RequestContextHolder來透傳資料

什麼是 RequestContextHolder?

RequestContextHolder 是spring mvc的一個工具類,顧名思義,持有上下文的Request容器。

如何使用:

//獲取當前執行緒 request請求的屬性

RequestAttributes requestAttributes = RequestContextHolder。currentRequestAttributes();

//設定當前執行緒 request請求的屬性

RequestContextHolder。setRequestAttributes(attributes);

RequestContextHolder的會用到的幾個方法

currentRequestAttributes:獲得當前執行緒請求的屬性(頭資訊之類的)

setRequestAttributes(attributes):設定當前執行緒 屬性(設定頭資訊)

resetRequestAttributes:刪除當前執行緒 繫結的屬性

下面是他們的原始碼,可以簡單看一下,原理是透過ThreadLocal來繫結資料的:

5年華為架構師1小時把SpringBoot專案併發提升了10倍,網友:牛掰

5年華為架構師1小時把SpringBoot專案併發提升了10倍,網友:牛掰

下面我編寫了一套遇到問題的程式碼例子,以及解決的程式碼:

TestUserController

測試介面

5年華為架構師1小時把SpringBoot專案併發提升了10倍,網友:牛掰

5年華為架構師1小時把SpringBoot專案併發提升了10倍,網友:牛掰

TestRequestService

聚合資料的類

5年華為架構師1小時把SpringBoot專案併發提升了10倍,網友:牛掰

5年華為架構師1小時把SpringBoot專案併發提升了10倍,網友:牛掰

5年華為架構師1小時把SpringBoot專案併發提升了10倍,網友:牛掰

下面是兩個請求 使用者和訂單請求類

OrderService 請求訂單的服務的聚合方法

5年華為架構師1小時把SpringBoot專案併發提升了10倍,網友:牛掰

5年華為架構師1小時把SpringBoot專案併發提升了10倍,網友:牛掰

5年華為架構師1小時把SpringBoot專案併發提升了10倍,網友:牛掰

5年華為架構師1小時把SpringBoot專案併發提升了10倍,網友:牛掰

UserService 請求訂單的服務的聚合方法

5年華為架構師1小時把SpringBoot專案併發提升了10倍,網友:牛掰

5年華為架構師1小時把SpringBoot專案併發提升了10倍,網友:牛掰

5年華為架構師1小時把SpringBoot專案併發提升了10倍,網友:牛掰

OrderController 你可以理解成其他其他微服務的介面(模擬寫的一個介面,用來測試 請求介面的時候是否攜帶 請求頭了)

5年華為架構師1小時把SpringBoot專案併發提升了10倍,網友:牛掰

5年華為架構師1小時把SpringBoot專案併發提升了10倍,網友:牛掰

5年華為架構師1小時把SpringBoot專案併發提升了10倍,網友:牛掰

5年華為架構師1小時把SpringBoot專案併發提升了10倍,網友:牛掰

下面三個介面的由來:

/v1/testUser/listUser 介面:就是序列呼叫其他服務介面 ,效能比較慢。

/v1/testUser/listUser2 介面:是透過@Async 非同步註解,並行呼叫其他 系統的介面,效能是提升上去了,但灰度環境 是需要根據請求頭裡面的資料判斷是否把流量打到灰度環境。

/v1/testUser/listUser3介面:對@Async註解沒有找到透傳 主執行緒request頭資訊的方案,就使用執行緒池+CompletableFuture。supplyAsync的方式 每次執行非同步執行緒的時候,把主執行緒的 請求引數設定到子執行緒,然後透過try-finally 引數使用完之後RequestContextHolder。resetRequestAttributes() 刪除引數。

注意:parallelStream它也是屬於並行流操作,也要設定 請求頭資訊,雖說子執行緒(getDateResp3方法)能獲取到主執行緒的請求頭資訊了,但是parallelStream 又相當於子執行緒的子執行緒了,它是獲取不到的 主執行緒的attributes的,當時我就是沒在parallelStream設定attributes,它沒有走到灰度環境, 讓我 耗費了兩個多小時,程式碼加了四五次日誌輸出,才把這個問題定位出來,這是一個坑。。。

下面是程式碼:

5年華為架構師1小時把SpringBoot專案併發提升了10倍,網友:牛掰

上面說到,之前使用了@Async註解,子執行緒無法獲取到上下文資訊,導致流量無法打到灰度,然後改成 執行緒池的方式,每次呼叫非同步呼叫的時候都手動透傳下文(硬編碼)解決了問題。

後面查閱了資料,找到了方案不用每次硬編碼,來上下文透傳資料了。

方案一:

繼承執行緒池,重寫相應的方法,透傳上下文。

方案二:(推薦)

執行緒池ThreadPoolTaskExecutor,有一個TaskDecorator裝飾器,實現這個介面,透傳上下文。

方案一:繼承執行緒池,重寫相應的方法,透傳上下文。

1、ThreadPoolTaskExecutor spring封裝的執行緒池

ThreadPoolTaskExecutor 執行緒池程式碼如下:

5年華為架構師1小時把SpringBoot專案併發提升了10倍,網友:牛掰

5年華為架構師1小時把SpringBoot專案併發提升了10倍,網友:牛掰

1、MyCallable是繼承Callable,建立MyCallable物件的時候已經把Attributes物件賦值給屬性context了(建立MyCallable物件的時候因為實在當前主執行緒建立的,所以是能獲取到請求的Attributes),在執行call方法前,先執行了RequestContextHolder。setRequestAttributes(context);

【把這個MyCallable物件的屬性context 設定到setRequestAttributes中】 所以在執行具體業務時,當前執行緒(子執行緒)就能取得主執行緒的Attributes。

2、MyThreadPoolTaskExecutor類是繼承了ThreadPoolTaskExecutor 重寫了submit和submitListenable方法。

為什麼是重寫submit和submitListenable這兩個方法了?

@Async AOP原始碼的方法位置是在:AsyncExecutionInterceptor。invoke

doSubmit方法能看出來

無返回值呼叫的是執行緒池方法:submit()

有返回值,根據不同的返回型別也知道:

返回值型別是:Future。class 呼叫的是方法:submit()

返回值型別是:ListenableFuture。class 呼叫的方法是:submitListenable(task)

返回值型別是:CompletableFuture。class呼叫的是CompletableFuture。supplyAsync這個在非同步註解中暫時用不上的,就不考慮重寫了。

5年華為架構師1小時把SpringBoot專案併發提升了10倍,網友:牛掰

5年華為架構師1小時把SpringBoot專案併發提升了10倍,網友:牛掰

2、ThreadPoolExecutor 原生執行緒池

ThreadPoolExecutor執行緒池程式碼如下:

5年華為架構師1小時把SpringBoot專案併發提升了10倍,網友:牛掰

5年華為架構師1小時把SpringBoot專案併發提升了10倍,網友:牛掰

像ThreadPoolExecutor主要重寫execute方法,在啟動新執行緒的時候先把Attributes取到放到MyRunnable物件的一個屬性中,MyRunnable在具體執行run方法的時候,把屬性Attributes賦值到子執行緒中,當run方法執行完了在把Attributes清空掉。

為什麼只要重寫了execute方法就可以了?

ThreadPoolExecutor大家都知道主要是由submit和execute方法來執行的。

看ThreadPoolExecutor類的submit具體執行方法是由父類AbstractExecutorService#submit來實現。

具體程式碼在下面貼出來了,可以看到submit實際上最後呼叫的還是execute方法,所以我們重寫execute方法就好了。

submit方法路徑及原始碼:

java。util。concurrent。AbstractExecutorService#submit(java。lang。Runnable)

5年華為架構師1小時把SpringBoot專案併發提升了10倍,網友:牛掰

方案二:(推薦)

ThreadPoolTaskExecutor執行緒池

實現TaskDecorator介面,把實現類設定到taskExecutor。setTaskDecorator(new MyTaskDecorator());

5年華為架構師1小時把SpringBoot專案併發提升了10倍,網友:牛掰

為什麼設定了setTaskDecorator就能實現透傳資料了?

主要還是看taskExecutor。initialize()方法,主要是重寫了ThreadPoolExecutor的execute方法,用裝飾器模式 增強了Runnable介面,原始碼如下:

5年華為架構師1小時把SpringBoot專案併發提升了10倍,網友:牛掰

5年華為架構師1小時把SpringBoot專案併發提升了10倍,網友:牛掰

總結

無論是方案1還是方案2,原理都是先在當前執行緒獲取到Attributes,然後把Attributes賦值到Runnable的一個屬性中,在起一個子執行緒後,具體執行run方法的時候,把Attributes設定給當子執行緒,當run方法執行完了,在清空Attributes。

方案2實現比較優雅,所以推薦使用它。

工作沒多久的時候覺得spring的使用很麻煩,但是工作久了慢慢發現spring一些小細節、設計模式運用得非常巧妙,很容易解決遇到的問題,只能說spring厲害。

今日份分享已結束,請大家多多包涵和指點!

相關文章