简

一套鎖實現已經標記 Deprecated,Spring 卻還在創建它。沿着 Bean、實際註冊入口和現有鎖機制查下去,我才確認這次該做的是清理,而不是遷移。

刪掉 418 行之前,我先確認了什麼:Apache ShenYu 廢棄鎖清理
13 分鐘
2522 字

這次 ShenYu PR,最後沒有新增一行代碼,只刪掉了 418 行。和之前沿着同步鏈路排查 Bug 不同,這次面對的是一套已經標記 @Deprecated 的註冊鎖實現。

但真正讓我停下來的,是一個看起來矛盾的地方:類上已經寫着廢棄,Spring 配置裏卻還留着創建它的 Bean。它到底只是歷史遺留,還是仍然在某條業務路徑上保護着併發註冊?

我不敢只憑一個註解就刪掉它。畢竟這裏不只是幾個工具方法,還涉及事務、數據庫行鎖和線程裏的事務狀態。於是,這次的大部分工作都發生在按下刪除之前:查消費者,追當前註冊入口,再把旁邊仍然在用的鎖機制區分開。

最後確認下來,這次需要做的不是遷移鎖邏輯,而是把已經退出倉庫內業務調用鏈的舊實現清理乾淨。下面記錄的是,我怎麼走到這個判斷。

標記廢棄,不等於已經退出調用鏈h2

Issue #6606 指向 shenyu-admin.lock 下的五個舊類型:

  • RegisterExecutionLock
  • RegisterExecutionRepository
  • ForUpdateBackedRegisterExecutionLock
  • PlatformTransactionRegisterExecutionRepository
  • RegisterTransactionUtil

這些類型都已經標記 @Deprecated。其中的接口和實現還在 Javadoc 中指向 JdbcLockRegistry#obtain 或 DefaultLockRepository,提示使用替代方案。

與此同時,RegisterCenterConfiguration 裏仍然有一個 registerExecutionRepository(...) Bean,用來創建舊的 PlatformTransactionRegisterExecutionRepository。

這時候其實有兩個方向。如果業務還在使用它,就需要先處理遷移,不能直接刪除;如果業務已經不走這裏,留下來的才是舊 Bean 和舊實現。@Deprecated 能告訴我它不再被推薦使用,卻不能替我回答現在有沒有人在用。

而這套實現也不是空殼。舊的 ForUpdateBackedRegisterExecutionLock 會開啓事務、設置超時,通過 RegisterTransactionUtil 把事務狀態放進 ThreadLocal,再調用 pluginMapper.selectByNameForUpdate(...) 獲取數據庫行鎖。解鎖時,按事務狀態提交或回滾,最後清掉線程中的狀態。

越往裏看,越覺得不能憑“看起來很舊”做判斷。我需要查的是這套職責還有沒有消費者,而不是它的實現寫得完整不完整。

Bean 還在創建,業務卻已經走了另一條路h2

我先把“Spring 會創建這個 Bean”和“業務會使用這個 Bean”分開查。

沿着舊類型和配置查引用後,沒有找到倉庫內的生產代碼消費者。剩下的是舊實現之間的引用、創建 Bean 的配置,以及專門測試這套實現的兩個測試文件。註冊服務已經不再依賴它。

不過,確認舊接口沒有消費者,只回答了一半問題。我還需要知道:當前 context-path 註冊路徑,由誰來負責加鎖?

繼續追到 AbstractContextPathRegisterService.registerContextPath(...),可以看到它注入的是 Spring Integration 的 LockRegistry。方法先根據 context-path 構造 key,通過 registry.obtain(key) 取得鎖對象,再在 try 中加鎖並執行註冊,最後在 finally 中解鎖。

圖 1 · 舊實現清理後,這條 context-path 註冊鎖路徑仍然保留
保留的 context-path 註冊鎖路徑 registerContextPath 構造鎖 key,使用 LockRegistry.obtain 取得鎖對象,在 try 中調用 lock.lock 並執行註冊。已有 Rule 時提前返回。正常完成、提前返回或異常退出都會執行 finally 中的 lock.unlock。此圖不是所有註冊入口的概括。 進入 context-path 註冊方法 LockRegistry.obtain(key) 取得鎖對象,key 來自 context-path lock.lock() 執行 Selector / Rule 註冊邏輯 已有 Rule 時提前 return 正常完成 · 提前返回 · 異常退出 finally:lock.unlock() 退出 try 時執行解鎖
只展示本次核對的 context-path 路徑,不代表所有註冊入口。窄屏可在圖內左右滑動查看。

這裏還有一個容易忽略的細節:發現同名 Rule 已存在時,方法會提前 return,但依然會走 finally。我確認的不是“找到了另一個叫 lock 的類”,而是具體入口裏的加鎖、業務操作和解鎖確實連在一起。

這讓我能把兩件事區分開:如果舊實現仍在承擔業務職責,那是遷移;現在這條現存路徑已經使用 LockRegistry,舊類型又沒有倉庫內消費者,這次才是清理。它們的 diff 可能都很小,但前提完全不同。

名字裏都有 lock,也不能一起刪h2

確認可以清理舊實現之後,我沒有把搜索結果裏所有帶 lock、JDBC 或 ZooKeeper 的代碼都算進來。ShenYu Admin 還有其他鎖機制,同樣的關鍵詞不代表同樣的職責。

這次的邊界可以拆成下面幾塊:

範圍這次怎麼處理原因
五個舊註冊鎖類型刪除已廢棄,倉庫內沒有生產消費者
registerExecutionRepository Bean 及相關 imports刪除只負責創建舊實現
兩個舊鎖測試文件刪除專門驗證這套被移除的實現
context-path 的 LockRegistry 路徑保留當前仍在使用的註冊加鎖路徑
JDBC / ZooKeeper 集羣選主、鎖配置保留仍在運行,且不是這套舊註冊鎖的職責
數據庫 schema、其他活躍的 selectByNameForUpdate 調用保留不能因為舊鎖使用過它們,就認為它們也已廢棄

例如,舊鎖調用過 selectByNameForUpdate,但這個 mapper 方法還有活躍的調用點。刪除舊調用方,不等於數據庫行鎖本身已經沒用了。

最後的修改因此很集中:五個生產類型、配置中的一個 Bean 和相關 imports、兩個舊測試文件,總共涉及八個 shenyu-admin 文件。沒有改數據庫結構,也沒有順手調整現有鎖配置。

舊測試可以刪,留下來的行為仍然要檢查h2

這個 PR 沒有新增測試,還刪除了兩個測試文件:ForUpdateBackedRegisterExecutionLockTest 和 RegisterExecutionRepositoryTest。

它們驗證的就是舊實現。對應的生產代碼刪掉後,繼續保留這些測試沒有意義。但這不代表這次不需要驗證——重點變成了:清理舊實現之後,真正還在用的路徑有沒有受到影響?

我保留並運行了 context-path 註冊、JDBC 選主和 ZooKeeper 選主對應的測試。PR 提交記錄裏,三個相關測試類分別通過了 1、2、5 個用例,完整 Admin 測試套件也通過:1,718 個測試,0 failures、0 errors,以及 1 個原有 skipped test。Checkstyle 和 Apache RAT 同樣通過。

以前我更容易從“這次新增了什麼測試”看一個修改。這次我關心的卻是留下來的部分:不再測試已經不存在的舊實現,而是確認清理沒有誤傷現有行為。

倉庫內沒有消費者,不等於沒有兼容性成本h2

還有一個邊界需要説清楚。這些類型雖然已經廢棄,卻是發佈在 shenyu-admin artifact 中的公開類型,不是純粹的私有實現。

所以,我能通過搜索確認倉庫內部沒有消費者,卻不能據此説外部也沒人依賴它們。外部代碼如果還實現 RegisterExecutionRepository,或者直接引用舊實現,刪除後就會遇到源碼或二進制兼容性問題。這個成本在 PR 描述中也明確寫了出來。

舊 Javadoc 已經提示替代方案,但“有遷移提示”和“所有用户都已經遷移”仍然不是一回事。最終這次清理經過維護者 review 後合併;對我來説,重要的是把內部調用鏈的結論和公開 API 的兼容性影響分別講清楚,而不是拿 @Deprecated 把後者一筆帶過。

刪除之前的判斷,才是這次的主要工作h2

以前想到開源貢獻,我更容易想到修 Bug、補功能,或者寫出一段新的實現。這次的結果卻是零新增、418 行刪除。單看 diff 很簡單,但真正花時間的,是弄清楚為什麼它現在可以被刪除。

我也更具體地理解了 @Deprecated:它像是在告訴我,這個 API 正在退出,而不是宣佈它已經消失。往後再遇到類似清理,我會先找消費者和實際入口,確認現存路徑怎麼工作,再決定清理到哪裏為止。Bean 還在不代表業務還在用;名字相似,也不代表職責相同。

這次讓我有把握提交的,不是“這些代碼看起來沒人要了”,而是我能解釋清楚:倉庫內沒有舊實現的生產消費者,當前 context-path 加鎖路徑仍然完整,旁邊活躍的選主和數據庫機制也沒有被動到。至於公開 API 的兼容性成本,需要單獨説明,不能假裝不存在。

我覺得這種貢獻的價值,不一定體現在寫了多少行。有時候,把一套已經退出業務鏈路、卻還會讓後來的人困惑的實現清掉,也是在幫項目往前走。比“我刪了 418 行”更值得記住的,是我終於能把為什麼要刪、為什麼只刪這些講清楚。