我跟你說@RefreshScope跟Spring事件監聽一起用有坑!

waynaqua發表於2024-07-09

本文記錄一下我在 Spring 自帶的事件監聽類新增 @RefreshScope 註解時遇到的坑,原本這兩個東西單獨使用是各自安好,但當大家將它們組合在一起時,會發現我們的事件監聽程式碼被重複執行。希望大家引以為鑑,避免重複踩坑。耐心看完,你一定會有所收穫!

前置描述

最近有一個使用者拉新的需求,需要在新使用者註冊時判斷使用者是否有對應的邀請關係,如果有則需要給新使用者贈送系統資源。

原有的使用者註冊邏輯裡使用了 Spring 自帶的事件監聽工具,也就是 applicationEventPublisher(事件釋出類)以及 ApplicationListener(事件監聽類),在使用者註冊完畢寫入使用者記錄並生成 token 後,會觸發 RegisterEvent(註冊事件)的釋出。虛擬碼如下,

// 1. 使用者註冊,寫入資料庫
RegisterResponseVO registerResponseVO = memberRegisterService.register(new RegisterRequestVO(request);

// 2. 生成token
String token = getToken(memberEntity.getId(), request.getSource());
log.info("login mobile {} login token {}", request.getMobile(), token);

// 3. 釋出註冊事件,會觸發登入日誌監聽、優惠券贈送監聽等
applicationEventPublisher.publishEvent(new RegisterEvent(request, memberEntity, token));

由於之前程式碼已經使用事件監聽邏輯,所以這裡我們的新使用者註冊判斷邀請關係的邏輯就直接新建一個 NewUserInvitedListener 監聽類即可。虛擬碼如下,

@Slf4j
@RefreshScope
@AllArgsConstructor
@Component
public class NewUserInvitedListener implements ApplicationListener<RegisterEvent> {

    @Async("asyncServiceExecutor")
    @Override
    public void onApplicationEvent(RegisterEvent registerEvent) {
        UserLoginRequestVO requestVO = registerEvent.getRequestVO();
        MemberEntity memberEntity = registerEvent.getMemberEntity();
        log.info("================ NewUserInvitedListener =============== registerEvent is {}", registerEvent);
        // 1. 校驗邏輯
        validateUser(memberEntity);
        // 2. 判斷使用者是否有邀請關係
        // 3. 如果有則贈送系統資源
        ...
    }
}

OK,程式碼邏輯也不復雜,寫完提測交給測試下班(週五下午寫完)。

發現問題

週一一來,測試就在群裡 @ 後端人員說是新使用者贈送的系統資源送了兩次,說實話我一開始是不太信的,直到我去查了日誌,發現 NewUserInvitedListener 監聽類的日誌確實被列印了兩次,也就是說我們的 NewUserInvitedListener 監聽類被觸發了兩次。

OK,到這裡我們的問題就確確實實產生了,接下來就是解決問題。

image

解決思路

問題產生通常都有很多種解決方法,我們如何選擇一個最適合我們當前場景的方法才能體現出我們對業務、技術的理解。
在這個監聽類重複觸發的場景裡,就有多種解決方式,我簡單列舉幾個,

  1. 新增冪等處理,防止重複執行
  2. 加鎖,防止重複執行
  3. 解決下為什麼監聽類會重複觸發

這三個解決方案各有優劣,透過對監聽類的業務邏輯新增冪等邏輯或者加鎖邏輯都是可以解決的,但是這不是問題根源,問題根源是在於監聽類為什麼會被重複觸發。
在本文中,我也將帶著大家一步一步探索並解決這個問題。

檢查下之前的事件監聽類是否也有重複觸發的問題

因為這個程式碼是照著之前的邏輯寫的,新加的 NewUserInvitedListener 被發現重複觸發,那以前的 MemberLoginLogListener 是否也有重複觸發的問題。虛擬碼如下,

@Slf4j
@Component
@AllArgsConstructor
public class MemberLoginLogListener implements ApplicationListener<RegisterEvent> {
    private MemberLoginLogService memberLoginLogService;

    @Async("asyncServiceExecutor")
    @Override
    public void onApplicationEvent(RegisterEvent event) {
        MemberEntity memberEntity = event.getMemberEntity();
        log.info("================ MemberLoginLogListener ===============, mobile is {}", memberEntity.getMobile());
        MemberLoginLogEntity memberLoginLogEntity = MemberLoginLogConvertor.buildLoginLogEntity(event.getRequestVO(),
                event.getMemberEntity());
        memberLoginLogEntity.setToken(event.getToken());
        memberLoginLogService.save(memberLoginLogEntity);
    }
}

查詢 MemberLoginLogListener 監聽類的日誌,發現只有一次列印,說明之前寫的 MemberLoginLogListener 監聽類沒有重複觸發的問題,那這裡就很奇怪了。對比一下 NewUserInvitedListener 監聽類與 MemberLoginLogListener 監聽類的差別,很明顯我們發現 NewUserInvitedListener 監聽類上多了一個 @RefreshScope 註解。

OK,問題有可能就是 @RefreshScope 註解導致,我們去掉 @RefreshScope 註解在看看日誌列印。

去掉 @RefreshScope 註解

當我們去掉 @RefreshScope 註解後,神奇的事情發生了,NewUserInvitedListener 監聽類的日誌列印正常了,只觸發了一次!

OK,到這裡我們也就發現了問題出在 @RefreshScope 註解上。

如何搜尋問題

雖然我們知道了問題出在 @RefreshScope 註解上,但是我們怎麼向搜尋引擎描述這個問題嘞?

很多人發現了問題,但是不知道如何描述問題,怎麼描述問題才能讓別人一聽就懂,從而能給你提供幫助。你需要把問題的重點描述出來,搜尋引擎才能給予精準幫助。

在我們這個新使用者註冊判斷邀請關係的場景裡,很顯然我們的搜尋詞可以是 “spring 事件監聽重複觸發 @RefreshScope”可以看到我的搜尋關鍵詞有 3 個,分別是 spring、事件監聽重複觸發以及 @RefreshScope。讓我們來看看搜尋結果。
image

前 5 個搜尋結果中,只有第五個的標題可能符合我們的搜尋內容,我們點進去看一看。

image

很遺憾,跟我們的問題場景並不相符,我們並沒有搜尋到我們想要的東西。在這裡我們的搜尋關鍵詞“spring 事件監聽重複觸發 @RefreshScope”並沒有給予我們幫助。

回到問題本身

既然我們的問題已經定位到了,在於 @RefreshScope 會導致監聽類的重複觸發,可是這個關鍵詞並沒有相關搜尋結果,那麼我們只能換個角度。

為什麼會重複觸發?

在 NewUserInvitedListener 監聽類中,我們使用 @Component 註解,預設註冊了一個單例 bean,這個 bean 用於接收使用者註冊事件。既然 bean 是單一的,那就是說 Spring 傳送了 2 次 RegisterEvent 事件嗎?結合上文提到的 MemberLoginLogListener 監聽類只觸發一次的日誌,很顯然,Spring 只會傳送了 1 次 RegisterEvent 事件。

難道說問題在於 Spring 裡出現了兩個 NewUserInvitedListener 型別的 bean?

那麼到這裡恭喜我們終於定位到了重複觸發問題的根源。

如果大家瞭解 @RefreshScope 的原理相信大家已經猜出來了。

@RefreshScope 原理

Spring 中 @scope 註解的原理就是在建立 Scope=singleton 的 Bean 時,IOC 會儲存例項在一個 Map 中,保證這個 Bean 在一個 IOC 上下文有且僅有一個例項。

SpringCloud 新增了一個自定義的作用域:refresh(可以理解為“動態重新整理”),同樣用了一種獨特的方式改變了 Bean 的管理方式,使得其可以透過外部化配置(.properties)的重新整理,在應用不需要重啟的情況下熱載入新的外部化配置的值。

這個 scope 是如何做到熱載入的呢?RefreshScope 主要做了以下動作:
單獨管理 Bean 生命週期

建立 Bean 的時候如果是 RefreshScope 就快取在一個專門管理的 ScopeMap 中,這樣就可以管理 Scope 是 Refresh 的 Bean 的生命週期了(所以含 RefreshScope 的其實一共建立了兩個 bean)。

重新建立 Bean

外部化配置重新整理之後,會觸發一個動作,這個動作將上面的 ScopeMap 中的 Bean 清空,這樣這些 Bean 就會重新被 IOC 容器建立一次,使用最新的外部化配置的值注入類中,達到熱載入新值的效果。

看完 @RefreshScope 的原理相信大家已經知道了出現兩個 NewUserInvitedListener 型別 bean 的原因是在於 @RefreshScope 導致。這是由於 @RefreshScope 註解的內部實現建立了另外一個相同型別的 NewUserInvitedListener bean,導致我們的新使用者監聽邏輯被重複執行。

回到搜尋關鍵詞

假如我是說假如,假如我們不知道 @RefreshScope 的原理,自然不知道專案中出現了兩個 NewUserInvitedListener 型別的 bean 是 @RefreshScope 導致。 那麼我們怎麼透過搜尋關鍵詞來找到這個問題嘞?

到這裡也就是本文的重點所在,怎麼透過搜尋關鍵詞來解決我們的問題。

先定義問題

在這個場景裡我們使用的是 Spring 專案,問題本質是 @RefreshScope 在 Spring 自帶的事件監聽類搭配使用時,會導致 bean 重複進而導致監聽類邏輯被重複執行,當我們去掉 @RefreshScope 後,也就沒有這種情況。

也就是說這句話我們換個說話:“@RefreshScope 在 Spring 自帶的事件監聽類搭配使用時,會生成另外一個相同的 bean 導致監聽類被重複觸發”

總結關鍵詞

在上面的先定義問題中,我們提煉一下關鍵詞,

  • Spring:這個關鍵詞在 Spring 專案中必帶,大家應該沒有意見把
  • @RefreshScope:我們的問題根源,搜尋也得帶上
  • 生成同一個 bean:這是一個描述語句,簡要描述一下我們發現的問題

看一看搜尋結果,
image

點進第一個結果,

image

OK,大功告成,看到我們框選中的地方了嗎,上文的 @RefreshScope 原理解釋,就是複製與這裡。

貼一下原文地址:https://blog.csdn.net/m0_71777195/article/details/127223544

一些思考

實話實說,我在測試給我上報問題,到發現這個問題來自於 @RefreshScope 註解只用了 10 分鐘,如上文所說,我透過對比以前寫的 MemberLoginLogListener 監聽類,早早的定位到問題來自於 @RefreshScope 註解。可是到我完整修復這個問題,提交到測試環境,卻花了 2 個半小時,原因是因為我在研究這個問題的根源,這也是這篇文章的由來。

假如說這個問題發生線上上,那麼我根本不可能花這麼多時間來研究,我需要的就是迅速解決這個問題並修復上線,避免影響更多使用者。

一樣的,大家在遇到這種相似問題時,如果境況緊急出現在生產環境,大家本著對工作負責的態度,應該迅速解決並做故障覆盤。如果是出現在測試環境我們可以本著對技術執著可以認真專研下這個問題。

其實我還想說的是在這個問題裡,我能 10 分鐘定位到問題來自於 @RefreshScope 註解,可能也有運氣成分。但是很多情況下當我們照驢子畫馬寫程式碼,發現出了問題時,這種情況大部分還是我們“畫蛇添足”導致。大家可以透過對比以前程式碼迅速找出問題原因。

找出了問題後是如何解決問題。這篇文章裡,我給大家講了講我的搜尋關鍵詞心得。第一是講重點、第二是找到問題本質,這樣才能從搜尋引擎嘴裡找出我們想要的答案。

如果覺得這篇文章寫的不錯的話,可以關注我的公眾號【程式設計師wayn】,我會更新更多技術乾貨、專案教學、經驗分享的文章。

相關文章