配置熱更新,不想重啟,如何更新Bean的狀態?

  • 2019 年 12 月 10 日
  • 筆記

點擊上方"IT牧場",選擇"設為星標"技術乾貨每日送達!

拋出疑問 ❓

通過配置中心,應用可以實時的接收到配置的變更,但是,應用中一些 Bean 是通過 Spring 容器來管理的,配置變更之後,怎麼來修改 Spring 容器中對應 Bean 的狀態呢?

舉個例子:如果在運行時修改了 JDBC 的參數配置,此時是重啟應用呢?還是修改對應 DataSource Bean 的屬性呢?如果是修改 Bean 的屬性,直接修改有用嗎?要怎麼做呢?

本文將以運行時修改 JDBC 參數為例,來討論嘗試運行時修改配置,最後提出普遍的解決方案。

畫外音:思路比結果更重要。

難點分析 ??‍?

難點一:動態修改 JDBC 參數,假設修改的是 URL 和 password,那舊連接使用的還是舊的配置,這個時候怎麼辦呢?舊連接立刻失效還是一段時間後在失效?在使用舊連接的線程要怎麼處理呢?

在動態切換的過程中,必然會有一個過渡過程,從舊連接過渡到新連接,這個過渡的過程應該是儘可能的平滑。比如可以通過運維層面來做到:在就修改完 URL 和 password 之後,還是會有一段時間可以支持舊的連接的正常訪問,以保證程序的平穩過渡。

難點二:DataSource Bean 到底在哪裡被引用了?能不能替換乾淨?舊連接如何放棄使用,並關閉?

在修改完 JDBC 參數之後,下一步要做的就是查找 DataSource Bean 的使用方,將使用方使用的 DataSource Bean 換成新的配置。然後,將舊的連接關閉,讓使用方使用使用新的連接。

嘗試解決 ?

文本將以 HikariCP 連接池為例來嘗試解決這個問題。HikariCP 是 SpringBoot2.0 之後的默認數據庫連接池,號稱是當前 Java 領域最快的數據庫連接池。

方案一:HikariCP 自帶動態修改配置 API

HikariCP 自帶了一些 API 來支持動態的修改數據庫的相關配置。

引用 1:https://github.com/brettwooldridge/HikariCP/wiki/FAQ#q9

FQA

引用 2:https://github.com/brettwooldridge/HikariCP/wiki/MBean-(JMX)-Monitoring-and-Management

使用jxm

...  HikariDataSource dataSource = ....  HikariConfigMXBean hikariConfigMXBean = dataSource.getHikariConfigMXBean();  hikariConfigMXBean.setPassword("...");  ...

筆者沒有仔細去嘗試這個方案(為什麼呢?當然是下面有更好的方案啦~),不過其 Github 的文檔是這樣描述的,官方第一手資料,出錯的可能性比較小,如果有問題也可以去 Github 提相關的 issue。

該方案的優點:使用原生 API 來動態修改配置,簡單、可靠。缺點:能修改的參數有限,同時強綁定了 DataSource 的實現,假設以後改用別的數據庫連接池,不一定有提供這些原生 API 來修改參數。

方案二:動態修改 DataSource

廢話不多說,直接上代碼:

DynamicDataSource

方案二的思路來自於org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource。這個類是用來解決多數據源問題的,可以根據不同的 key 找到不同的 DataSource,然後再獲取相應的 Connection。

同理,DynamicDataSource實現了DataSource接口,同時在其內部有一個成員變量AtomicReference<DataSource> dataSourceReference,由dataSourceReference來提供DataSource。當程序在運行時修改了 JDBC 參數時,可以通過創建一個新的DataSource對象來替換dataSourceReference的值,而對外暴露的是DynamicDataSource對象,這對使用方來說是無感知的。

那麼是不是這樣做就夠了呢?思考一分鐘。

還記得之前提的難點嗎?使用這種方法,底層可以悄悄的把 DataSource 的實例對象替換掉,那被替換下來的舊 DataSource 的連接怎麼關閉呢?

HikariCP 提供了相應的方法來關閉連接。如果使用別的數據庫連接池也應該可以找到類似的方法。

ShutdownDataSource

小結:該方法通過DynamicDataSource來包裝真實的DataSource提供者,允許在運行時動態的替換底層的 DataSource 實例對象。同時,替換之後,要記得將舊的 DataSource 關閉。相比於方案一,該方案可以支持修改任意的 JDBC 屬性,同時也沒有強依賴DataSource實現者的 API,更加通用、靈活。

相關代碼地址:https://github.com/shenjianeng/dynamic-change-data-source

任意 Bean 屬性的動態修改 ?

完善方案二

解決完 DataSource 個例之後,能不能對上述方案進一步抽象,以支持動態修改任意的 Bean 的屬性呢?

DynamicRefreshProxy

其實事情遠沒有想着中的那麼簡單,即使抽象出了DynamicRefreshProxy,還是會有以下幾個難點:

  1. 需要動態修改配置的 Bean 要通過DynamicRefreshProxy來創建代理對象
  2. 動態修改配置之後,需要修改AtomicReference<Object> atomicReference的引用值
  3. 需要提供關閉舊對象相關資源的方法,在替換完舊對象之後,調用該方法

這裡筆者可以提供一個思路來解決這些問題:

通過自定義註解,例如@DynamicRefreshable,然後提供一個 BeanPostProcessor 來創建代理對象替換原對象,同時保存對應的DynamicRefreshProxy對象,監聽到對應屬性發生變化之後,替換DynamicRefreshProxy對象中的atomicReference,然後調用原始對象的相關方法來關閉資源。

Spring Cloud Refresh Scope

Spring Cloud 中提供了一種新的 Scope:Refresh Scope

Refresh Scope 相關文檔: https://cloud.spring.io/spring-cloud-static/spring-cloud.html#_refresh_scope

Refresh Scope

標記為@RefreshScope 的 Bean 在配置更改時,重新初始化,不過這需要調用ContextRefresher#refresh或者RefreshScope#refreshAll。不同的是,ContextRefresher#refresh方法內部不僅調用了RefreshScope#refreshAll,還調用了ContextRefresher#refreshEnvironment

一個簡單的 DEMO 程序如下:

demo

重啟大法好?✌️

還記得開篇提出的問題和難點嗎?

難點二:DataSource Bean 到底在哪裡被引用了?能不能替換乾淨?舊連接如何放棄使用,並關閉?

是否已經正常的關閉資源是一個很難驗證的事情,它會和當前項目的具體運行狀態相關聯。

在上文中,雖然在替換 DataSource 實例之後,調用了相關的 API 來關閉連接,但是,doShutdownDataSource方法只會嘗試幾次,超過一定次數之後,就會直接調用 close 方法來關閉數據庫。如果在嘗試數次之後,連接還是沒有關閉呢?close 方法能保證關閉所有相關資源嗎?還是重啟大法好?!

小調查:你們的做法是熱更新 Bean 呢?還是選擇重啟呢?