擼了一個簡易的設定中心,順帶整合到了SpringCloud

2022-10-14 15:02:53

大家好,我是三友~~

最近突然心血來潮(就是閒的)就想著擼一個簡單的設定中心,順便也照葫蘆畫瓢給整合到SpringCloud。

本文大綱

設定中心的概述

隨著歷史的車輪不斷的前進,技術不斷的進步,單體架構的系統都逐漸轉向微服務架構。雖然微服務架構有諸多優點,但是隨著越來越多的服務範例的數量,設定的不斷增多,傳統的組態檔方式不能再繼續適用業務的發展,所以急需一種可以統一管理組態檔應用,在此之下設定中心就誕生了。

所以設定中心就是用來統一管理各種服務設定的一個元件,本質上就是一個web應用。

設定中心的核心功能

一個設定中心的核心功能其實主要包括兩個:

  • 設定的存取
  • 設定變更的通知

設定的存取是設定中心不可缺失的功能,設定中心需要能夠將設定進行儲存,存在磁碟檔案也好,又或是資料庫也罷,總之需要持久化,同時設定中心也得提供設定查詢的功能。

設定變化的通知也是一個很重要的功能,一旦設定中心的設定有變動的話,那麼使用到這個設定的使用者端就需要知道這個設定有變動,從而可以做到相應的變動的操作。

手擼一個簡易的設定中心

上文分析了一個設定中心的核心功能,接下來就實現這兩個核心的功能。

一、檔案工程整體分析

檔案工程整體分為使用者端與伺服器端

  • 伺服器端:單獨部署的一個web應用,埠是8888,提供了對於設定增刪改查的http介面
  • 使用者端(SDK):業務系統需要參照對應的依賴,封裝了跟伺服器端互動的程式碼

二、伺服器端實現詳解

1、組態檔的資料儲存模型ConfigFile

在設定中心儲存設定的時候,需要指明以下資訊

public class ConfigFile {

    private String fileId;

    private String name;

    private String extension;

    private String content;

    private Long lastUpdateTimestamp;

}
  • fileId: 檔案的唯一id,由設定中心伺服器端在新增組態檔儲存的時候自動生成,全域性唯一
  • name: 就是檔案的名字,沒有什麼要求,見名知意就行
  • extension: 檔案字尾名,指的是該設定是什麼型別的檔案,比如是properties、yml等
  • content: 就是組態檔的內容,不同的字尾名有不同的格式要求
  • lastUpdateTimestamp: 上一次檔案更新的時間戳。當檔案儲存或者更新的時候,需要更新時間戳,這個欄位是用來判斷檔案是否有改動
2、檔案儲存層ConfigFileStorage

對於檔案儲存層,我提供了一個ConfigFileStorage介面,

public interface ConfigFileStorage {

    void save(ConfigFile configFile);

    void update(ConfigFile configFile);

    void delete(String fileId);

    ConfigFile selectByFileId(String fileId);

    List<ConfigFile> selectAll();

}

這個介面提供了對於設定儲存的crud操作,目前我已經實現了基於記憶體和磁碟檔案的儲存的程式碼

可以在專案啟動的時候,在組態檔指定是基於磁碟檔案儲存還是基於記憶體儲存,預設是基於磁碟檔案儲存。

當然,如果想把設定資訊儲存到資料庫,只要新增一個儲存到資料的實現就行。

3、ConfigController

ConfigController提供了對於組態檔的crud的http介面

ConfigController是通過呼叫ConfigManager來完成組態檔的crud

4、ConfigManager

其實就是一個service層,就是簡單的引數封裝,最終是呼叫ConfigFileStorage儲存層的實現來完成設定的儲存功能。

這樣設定中心的配的存取的功能就實現了。

所以,伺服器端還是比較簡單的。其實就是跟平時寫的業務系統的crud沒什麼區別,就是將資料庫儲存替換成了磁碟檔案的儲存。

至於前面說的組態檔變更通知的功能,我是基於使用者端來實現的。

三、使用者端的實現

使用者端工程程式碼如下

1、ConfigFileChangedListener
ConfigFileChangedListener
ConfigFileChangedListener

設定變動的監聽器,當用戶端對某個設定監聽的時候,如果這個設定的內容有變化的話,使用者端就會回撥這個監聽器,傳入最新的設定

2、ConfigService

封裝了使用者端的核心功能,可以新增對某個檔案的監聽器和獲取某個檔案的設定內容。

使用範例:

// 建立一個ConfigService,傳入設定中心伺服器端的地址
ConfigService configService = new ConfigService("localhost:8888");

// 從伺服器端獲取組態檔的內容,檔案的id是新增組態檔時候自動生成
ConfigFile config = configService.getConfig("69af6110-31e4-4cb4-8c03-8687cf012b77");

// 對某個組態檔進行監聽
configService.addListener("69af6110-31e4-4cb4-8c03-8687cf012b77"new ConfigFileChangedListener() {
    @Override
    public void onFileChanged(ConfigFile configFile) {
        System.out.printf("fileId=%s組態檔有變動,最新內容為:%s%n", configFile.getFileId(), configFile.getContent());
    }
});

這裡說一下設定變更通知的實現原理。

首先對於使用者端來說,要想知道哪個組態檔進行了改動,有兩種方式

第一種是通過push的方式來實現。當組態檔發生變動的時候,伺服器端主動將變動的組態檔push給使用者端。這種方式實現起來比較麻煩,一方面是伺服器端還得儲存使用者端的服務的資訊,因為伺服器端得知道push到哪臺伺服器上;另一方面,使用者端需要提供一個介面來接收伺服器端push的請求,所以這種方式整體實現起來比較麻煩。但是這種push方式時實性比較好,一旦組態檔有變動,第一時間使用者端就能夠知道設定有變動。

第二種方式就是基於pull模式來實現。使用者端定時主動去伺服器端拉取組態檔,判斷檔案內容是否有變動,一旦有變動就進行監聽器的回撥。這種實現相比push來說簡單不少,因為伺服器端不需要關心使用者端的資訊,所有的操作都由使用者端來完成。但是這個定時的時間間隔不好控制,太長可能會導致時實性差,太短會導致可能無效請求過多,因為設定壓根可能沒有變化。

但是這裡我選擇了第二種方式,因為實現起來簡單。。

變動通知程式碼實現
變動通知程式碼實現

到這,一個簡單的設定中心的伺服器端的和使用者端就完成了,這裡畫張圖來總結一下設定中心的核心原理。

接下來就把這個簡易的設定中心整合到SpringCloud中。

SpringCloud設定中心的原理

1、專案啟動是如何從設定中心載入資料的?

在SpringCloud環境下,當專案啟動的時候,在SpringBoot應用容器建立之前,會先建立一個容器,這個容器非常重要,這個容器是用來跟設定中心互動,拉取設定的。

這個容器在啟動的時候會幹兩件事:

  • 載入bootstrap組態檔,這就是為什麼設定中心的設定資訊需要寫在bootstrap組態檔的重要原因

  • 載入所有spring.factories檔案中的鍵為org.springframework.cloud.bootstrap.BootstrapConfiguration對應的設定類,將這些設定類注入到這個容器中,注意這裡是不會載入@EnbaleAutoConfiguration自動裝配的類

當這兩件事都做好之後,會從這個容器中獲取到所有的PropertySourceLocator這個介面的實現類物件,依次呼叫locate方法。

PropertySourceLocator
PropertySourceLocator

這個類很重要,先來看看註釋

Strategy for locating (possibly remote) property sources for the Environment. Implementations should not fail unless they intend to prevent the application from starting.

扔到有道翻譯如下:

為環境定位(可能是遠端)屬性源的策略。實現不應該失敗,除非它們打算阻止應用程式啟動。

說的簡單點就是用來定位到(也就是獲取的意思)專案啟動所需要的屬性資訊。同時要注意到括號內的 可能是遠端 告訴我們一個很重要的資訊,那就是獲取的設定資訊不僅僅可以存在本地,而且還可以存在遠端。

遠端?作者這裡就差直接告訴你可以從設定中心獲取了。。

所以從這個註釋就可以發現,原來PropertySourceLocator就是起到在SpringCloud環境下從設定中心獲取設定的作用。

PropertySourceLocator是一個介面,所以只要不同的設定中心實現這個介面,那麼不同的設定中心就可以整合到了SpringCloud,從而實現從設定中心載入設定屬性到Spring環境中了。

2、如何實現注入到Bean中的屬性動態重新整理?

上面講了在專案啟動的時候SpringCloud是如何從設定中心載入資料的,主要是通過新建一個容器,載入bootstrap組態檔和一些設定類,最後會呼叫PropertySourceLocator來從設定中心獲取到設定資訊。

那麼在SpringCloud環境下,是如何實現注入到Bean中的屬性動態重新整理的呢?

舉個例子

UserService
UserService

當在類上加一個@RefreshScope註解之後,那麼當設定中心sanyou.username的屬性有變化的時候,那麼此時注入的username也會跟著變化。

這種變化是如何實現的呢?

SpringCloud中規定,當設定中心使用者端一旦感知到伺服器端的某個設定有變化的時候,需要釋出一個RefreshEvent事件來告訴SpringCloud設定有變動。

在SpringCloud中RefreshEventListener類會去監聽這個事件,一旦監聽到這個事件,就會進行兩步操作來重新整理注入到物件的屬性。

RefreshEventListener
RefreshEventListener
  • 從設定中心再次拉取屬性值,而這個拉取的程式碼邏輯跟專案啟動時拉取的屬性值核心邏輯幾乎是一樣的,也是建立一個新的spring容器,載入組態檔和設定類,最後通過PropertySourceLocator獲取屬性,這一部分核心的程式碼邏輯是複用的。

  • 有了最新的屬性之後,就開始重新整理物件的屬性。

重新整理的邏輯實現的非常的巧妙,可不是你以為的簡單地將新的屬性重新注入物件中,而是通過動態代理的方式來實現的。

對於在類上加了@RefreshScope註解的Bean,Spring在生成這個Bean的時候,會進行動態代理。

這裡我們就上面舉個UserService例子來分析,在生成UserService有兩步操作

  • 生成一個UserService物件,將從設定中心拉到的設定sanyou.username注入給UserService物件

  • 由於加了@RefreshScope,會給上一步驟生成的UserService物件進行代理,生成一個代理物件

最後真正暴露出去供我們使用的其實是就是這個代理物件,如圖所示

由於暴露出去的是一個代理物件,所以當呼叫getUsername方法的時候,其實是呼叫UserService的代理物件的getUsername方法,從而就會找到UserService,呼叫UserService的getUsername獲取到username的屬性值。

當設定中心的設定有變動重新整理屬性的時候,Spring會把UserService這個物件(非代理物件)給銷燬,重新建立一個UserService物件,注入最新的屬性值。

當再次通過UserService代理物件獲取username屬性的時候,就會找最新建立的那個UserService物件,此時就能獲取到最新的屬性值。

設定每重新整理一次,UserService物件就會先銷燬再重新建立,但是暴露出去的UserService代理物件一直不會變。

這樣,對於使用者來說,好像是UserService物件的屬性自動重新整理了,其實本質上是UserService代理物件最終找的UserService物件發生了變化。

到這應該就知道為什麼加了@RefreshScope的物件能夠實現設定的自動重新整理了,其實依靠的是動態代理完成的。

3、原始碼執行流程圖

由於上面並沒有涉及整體執行流程的原始碼分析,所以我特地結合原始碼畫了兩張原始碼的執行流程圖,有興趣的小夥伴可以對照著圖翻一翻具體的原始碼。

3.1啟動時載入設定流程

最終從設定中心獲取到的屬性會放在專案啟動時建立的 Environment 物件裡面。

3.2設定重新整理原始碼流程

這個圖新增了對於加了@ConfigurationProperties資料繫結的物件原理的分析。

整合SpringCloud和測試

一、整合SpringCloud

1、ConfigCenterProperties

設定中心的設定資訊,這裡需要設定設定中心伺服器端的地址和使用的組態檔的id。當然這部分資訊需要寫在bootstrap組態檔中,前面也說過具體的原因。

2、ConfigCenterPropertySourceLocator

上面分析知道,專案啟動和重新整理的時候,SpringCloud是通過PropertySourceLocator的實現從設定中心載入設定資訊,所以這裡就得實現一下

核心的邏輯就是根據所設定的檔案的id,從設定中心拉取設定資訊,然後解析設定。

3、ConfigContextRefresher

這個是用來註冊檔案變動的監聽器,來重新整理檔案的資訊的。

因為上面提到,當設定發生變化的時候,需要釋出一個RefreshEvent事件來觸發重新整理設定的功能。

核心的邏輯就是當專案啟動的時候,對所使用的組態檔進行註冊一個監聽器,監聽器的實現就是當發生設定改動的時候,就釋出一個RefreshEvent事件。

4、兩個設定類
4.1 ConfigCenterBootstrapConfiguration

設定了ConfigCenterPropertySourceLocator、ConfigCenterProperties、ConfigService

4.2 ConfigCenterAutoConfiguration

設定了ConfigContextRefresher、ConfigCenterProperties、ConfigService

最後需要將兩個設定類在spring.factories設定一下。

這裡有個需要注意,前面說過,SpringCloud會建立新的容器來載入設定,而這個容器只會載入spring.factories檔案中鍵為@BootstrapConfiguration註解的設定類,所以需要將ConfigCenterBootstrapConfiguration跟BootstrapConfiguration配對,因為ConfigCenterBootstrapConfiguration設定了ConfigCenterPropertySourceLocator。

好了,到這裡真的就完成了對SpringCloud整合了。

二、測試

1、新增一個組態檔

啟動設定中心的server端,然後開啟ApiPost,新增一個組態檔

新增檔案型別為properties一個設定,內容為sanyou.username=sanyou鍵值對,當然可以寫很多鍵值對,我這裡就寫了一個,新增成功之後,返回了檔案的id:79765c73-c1ef-4ea2-ba77-5d27a64c4685

2、測試使用者端

這裡我為了方便,就把測試程式碼跟使用者端寫在同一個服務了,正常情況肯定是把跟SpringCloud程式碼打成一個依賴引到專案中。

在bootstrap.yml檔案中設定設定中心的相關資訊

  • 設定中心伺服器端的地址是:localhost:8888
  • 使用的組態檔的id是剛才建立的:79765c73-c1ef-4ea2-ba77-5d27a64c4685

測試Controller

提供一個介面,注入上面提到的UserService

啟動專案,呼叫介面

從斷這裡可以看出,實際注入的是一個UserService代理物件,並且最終找的是com.sanyou.configcenter.test.UserService@3a1e4fd3這個UserService物件

此時這次呼叫的返回值就是:sanyou

接下來測試一下自動重新整理屬性的功能

現在修改一下設定中心的sanyou.username為sanyou666

靜靜等待5秒鐘。。

此時控制檯列印出 Refresh keys changed: [sanyou.username] ,也就是sanyou.username屬性變了

此時再次獲取username

可以看出,UserService代理物件沒變,但是UserService物件已經變成了com.sanyou.configcenter.test.UserService@4237b3cd

此時獲取到的username就已經變成了sanyou666

所以,到這裡就成功將我們自己寫的那個簡易版的設定中心整合到了SpringCloud中了。

不足和改進

雖然我們這裡的設定中心有了設定中心基本的功能,但是其實還有很多的不足和可以改進的地方。

1、設定變更推播問題

問題前面也說過,在判斷設定是否變更的時候,這裡是每隔5s從伺服器端獲取一次,這裡就會可能5s之後才能感知到設定有變化,達不到真正時實的效果,並且由於這裡是由使用者端根據來判斷,會導致無效的請求過多,因為可能設定壓根沒有變化,但是還是每隔5s獲取一次設定資訊,白白浪費資源

解決這個問題可以換成上面提到的push方式來做,或者將輪詢方式改成長輪詢的方式實現也是可以的,如果不清楚push、輪詢、長輪詢的,可以翻一下 RocketMQ的push消費方式實現的太聰明瞭這篇文章。

2、高可用問題

這裡伺服器端的範例只有一個,不支援叢集的方式,就會有單點故障的問題,不支援高可用。在實際專案中,肯定要支援叢集的方式,保證即使有服務範例掛了,整個叢集仍然可以繼續對外提供服務,比如nacos就支援叢集的方式,並且可以自由選擇是使用AP模式還是CP模式。

3、通訊協定和序列化協定

對於通訊協定,這裡為了方便,我選擇了使用者端和伺服器端的通訊方式是基於http協定的,當然也可以自定義協定,或者使用其它的協定,比如gRPC協定。其實在nacos2.x的版本中,nacos開始全面擁抱gRPC協定了。

至於序列化協定,這裡選擇了json協定,因為很簡單、常見、使用範圍廣、跨語言,當然也可以選擇其它的,比如hessian序列化協定等等。

4、多租戶隔離

一個合格的設定中心需要能支援不同應用的隔離,還有同一個應用不同環境的隔離,這裡就圖省事,直接就是有一個檔案id來表示,雖然也可以做到隔離(不同系統用不同的檔案id),但是這種方式比較low。像nacos會自動根據設定的名稱和字尾名之類的,生成檔案id(dataId),同時還有分組的概念,其實就是為了做到隔離的效果。

5、鑑權

鑑權是一個系統比較常見的東西,這裡就不做過多贅述

6、控制頁面

上面所有對於設定的crud都是基於ApiPost來的,但是實際怎麼也得通過一個頁面來操作吧,至於這裡我為啥不自己寫個頁面,給你個眼神自己體會~~

最後,本文程式碼地址:

https://github.com/sanyou3/sanyou-config-center

往期熱門文章推薦

RocketMQ保姆級教學

三萬字盤點Spring/Boot的那些常用擴充套件點

RocketMQ的push消費方式實現的太聰明瞭

一網打盡非同步神器CompletableFuture

@Async註解的坑,小心

掃碼或者搜尋關注公眾號 三友的java日記 ,及時乾貨不錯過,公眾號致力於通過畫圖加上通俗易懂的語言講解技術,讓技術更加容易學習,回覆 面試 即可獲得一套面試真題。