首頁>技術>

本文介紹了一個在.NET中用資料庫做配置中心伺服器的方式,介紹了讀取配置的開源自定義ConfigurationProvider,並且講解了主要實現原理。

1、 為什麼用資料庫做配置中心

在開發youzack.com這個學英語網站的時候,需要儲存第三方介面AppKey、JWT等配置資訊。youzack是一個由登入註冊、聽力精聽、背單詞、背單詞第二版等4個子網站組成,為了保證網站的可用性,網站採用叢集式部署,同一個子網站部署2臺Web伺服器例項,因此整個系統部署了2*4=8個Web服務例項。配置資訊如果都儲存到本地配置檔案的話,管理特別麻煩,比如,如果一個配置項要修改的話,就要修改8個地方,因此需要儲存到一個配置中心伺服器上,各個應用都從配置中心伺服器讀取配置。

目前,有Apollo、Nacos、Spring Cloud Config等開源的配置中心可供使用,功能非常強大,不過需要單獨部署維護配置中心伺服器。我這個網站並不複雜,為了避免運維的麻煩,我要儘量減少網站中使用的服務的數量。

youzack所在的阿里雲也有對應的配置中心服務可以用,不用自己去部署維護,但是我不想讓網站依賴於特定雲服務商,而且那樣的話在本地開發環境也要特殊處理。

因為這些子網站都要連線資料庫,因此把配置資訊存到資料庫裡,用資料庫來做配置中心伺服器,最符合我的要求。

2、 專案優點

由於網站採用.NET 5開發,為了方便各個專案讀取配置,我開發了一個自定義的ConfigurationProvider,名字叫做Zack.AnyDBConfigProvider。

這個Zack.AnyDBConfigProvider的優點如下:

1. 配置儲存到資料庫表中,管理簡單;

2. 支援幾乎所有關係資料庫,只要.NET能連上的資料庫都支援;

3. 支援配置的版本化管理;

4. 支援符合.NET配置命名規則的多級配置的覆蓋;

5. 配置項的值型別支援豐富,既支援簡單的字串、數字等型別,也支援json等格式;

6. 採用.Net Standard2開發,因此可以支援.NET Framework、.NET Core等。

專案GitHub地址:https://github.com/yangzhongke/Zack.AnyDBConfigProvider

3、 Zack.AnyDBConfigProvider用法

第一步:

在資料庫中建一張表,預設名字是T_Configs,這個表名允許自定義為其他名字,具體見後續步驟。表必須有Id、Name、Value三個列,Id定義為整數、自動增長列,Name和Value都定義為字串型別列,列的最大長度根據系統配置資料的長度來自行確定,Name列為配置項的名字,Value列為配置項的值。

Name列的值遵循.NET中配置的“多層級資料的扁平化”(詳見微軟文件 https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/?view=aspnetcore-5.0),如下都是合法的Name列的值:

Api:Jwt:Audience

Age

Api:Names:0

Api:Names:1

Value列的值用來儲存Name類對應的配置的值。Value的值可以是普通的值,也可以使用json陣列,也可以是json物件。比如下面都是合法的Value值:

["a","d"]

{"Secret": "afd3","Issuer": "youzack","Ids":[3,5,8]}

ffff

3

下面這個資料就是後續演示使用的資料:

演示資料

第二步:

建立一個ASP.NET 專案,演示案例是使用Visual Studio 2019建立.NET Core 3.1的ASP.NET Core MVC專案,但是Zack.AnyDBConfigProvider的應用範圍並不侷限於這個版本。

透過NuGet安裝開發包:

Install-Package Zack.AnyDBConfigProvider

第三步:配置資料庫的連線字串

雖然說專案中其他配置都可以放到資料庫中了,但是資料庫本身的連線字串仍然需要單獨配置。它既可以配置到本地配置檔案中,也可以透過環境變數等方式配置,下面用配置到本地json檔案來舉例。

開啟專案的appsettings.json,增加如下節點:

"ConnectionStrings": {

"conn1": "Server=127.0.0.1;database=youzack;uid=root;pwd=123456"

},

接下來在Program.cs裡的CreateHostBuilder方法的webBuilder.UseStartup<Startup>();之前增加如下程式碼:

webBuilder.ConfigureAppConfiguration((hostCtx, configBuilder)=>{

var configRoot = configBuilder.Build();

string connStr = configRoot.GetConnectionString("conn1");

configBuilder.AddDbConfiguration(() => new MySqlConnection(connStr),reloadOnChange:true,reloadInterval:TimeSpan.FromSeconds(2));

});

上面程式碼的第3行用來從本地配置中讀取到資料庫的連線字串,然後第4行程式碼使用AddDbConfiguration來新增Zack.AnyDBConfigProvider的支援。我這裡是使用MySql資料庫,所以使用new MySqlConnection(connStr)建立到MySQL資料庫的連線,你可以換任何你想使用的其他資料庫管理系統。reloadOnChange引數表示是否在資料庫中的配置修改後自動載入,預設值是false。如果把reloadOnChange設定為true,則每隔reloadInterval這個指定的時間段,程式就會掃描一遍資料庫中配置表的資料,如果資料庫中的配置資料有變化,就會重新載入配置資料。AddDbConfiguration方法還支援一個tableName引數,用來自定義配置表的名字,預設名稱為T_Configs。

不同版本的開發工具生成的專案模板不一樣,所以初始程式碼也不一樣,所以上面的程式碼也許並不能原封不動的放到你的專案中,請根據自己專案的情況來定製化配置的程式碼。

第四步:

剩下的就是標準的.NET 中讀取配置的方法了,比如我們要讀取上面例子中的資料,那麼就如下配置。

首先建立Ftp類(有IP、UserName、Password三個屬性)、Cors類(有string[]型別的Origins、Headers兩個屬性)。

然後在Startup.cs的ConfigureServices方法中增加如下程式碼:

services.Configure<Ftp>(Configuration.GetSection("Ftp"));

services.Configure<Cors>(Configuration.GetSection("Cors"));

然後在Controller中讀取配置:

public class HomeController : Controller

{

private readonly ILogger<HomeController> _logger;

private readonly IConfiguration config;

private readonly IOptionsSnapshot<Ftp> ftpOpt;

private readonly IOptionsSnapshot<Cors> corsOpt;

public HomeController(ILogger<HomeController> logger, IConfiguration config, IOptionsSnapshot<Ftp> ftpOpt, IOptionsSnapshot<Cors> corsOpt)

{

_logger = logger;

this.config = config;

this.ftpOpt = ftpOpt;

this.corsOpt = corsOpt;

}

public IActionResult Index()

{

string redisCS = config.GetSection("RedisConnStr").Get<string>();

ViewBag.s = redisCS;

ViewBag.ftp = ftpOpt.Value;

ViewBag.cors = corsOpt.Value;

return View();

}

}

關於把讀取出來的配置如何使用就不再介紹了。我這裡只是把配置顯示到介面上。你可以把配置修改後,再重新整理介面,就可以看到修改後的配置。

執行效果

4、 原始碼原理講解

專案github地址是https://github.com/yangzhongke/Zack.AnyDBConfigProvider,最核心的類是DBConfigurationProvider。

.NET中自定義配置提供者都要實現IConfigurationProvider介面,一般都直接繼承自ConfigurationProvider這個抽象類。ConfigurationProvider中最重要的方法就是Load(),自定義配置提供者都要實現Load方法來載入資料,載入的資料按照鍵值對的形式儲存到Data屬性中。Data屬性是IDictionary<string, string>型別,Key為配置的名字,遵循.NET的“多層級資料的扁平化”規範。如果配置項發生了改變則呼叫OnReload()方法來通知監聽配置改變的程式碼。

上面介紹了ConfigurationProvider類的基本工作機制,我們下面再分析一下Zack.AnyDBConfigProvider中的DBConfigurationProvider類的主要程式碼的原理。

首先是DBConfigurationProvider類的建構函式:

ThreadPool.QueueUserWorkItem(obj => {

while (!isDisposed)

{

Load();

Thread.Sleep(interval);

}

});

可以看到,如果啟用了ReloadOnChange,那麼每隔指定的時間,就會呼叫Load重新載入資料。

下面是Load方法的主要程式碼:

public override void Load()

{

base.Load();

var clonedData = Data.Clone();

string tableName = options.TableName;

try

{

lockObj.EnterWriteLock();

Data.Clear();

using (var conn = options.CreateDbConnection())

{

conn.Open();

DoLoad(tableName, conn);

}

}

catch(DbException)

{

//if DbException is thrown, restore to the original data.

this.Data = clonedData;

throw;

}

finally

{

lockObj.ExitWriteLock();

}

//OnReload cannot be between EnterWriteLock and ExitWriteLock, or "A read lock may not be acquired with the write lock held in this mode" will be thrown.

if (Helper.IsChanged(clonedData, Data))

{

OnReload();

}

}

Load方法的主要思路就是:首先建立Data屬性的一個複製clonedData,用於稍後比較“資料是否修改了”。因為如果啟用了ReloadOnChange,那麼Load是在一個執行緒中被定期呼叫的,而讀取配置的程式碼最終會呼叫TryGet方法來讀取配置,為了避免TryGet讀到Load載入一半的資料造成資料混亂,因此需要使用鎖來控制讀寫的同步。因為通常讀的頻率高於寫的頻率,為了避免用普通的鎖造成的效能問題,這裡使用ReaderWriterLockSlim類來實現“只允許一個執行緒寫入,但是允許多個執行緒讀”。把載入配置寫入Data屬性的程式碼放到EnterWriteLock()、ExitWriteLock()之間,而把讀取配置的程式碼(見TryGet方法),用EnterReadLock()和ExitReadLock()包裹起來即可。

需要注意,在Load方法中,一定要注意把OnReload()放到ExitWriteLock()之後,否則會導致執行時報“A read lock may not be acquired with the write lock held in this mode”異常。因為OnReload方法會導致程式呼叫TryGet讀取資料,而TryGet中用了“讀鎖”,這樣就造成了“寫鎖”中巢狀“讀鎖”這個預設不允許的行為。

在DoLoad方法中,會從資料庫中讀取資料載入到Data中。在Load方法的最後,就會把之前儲存的Data屬性的複製值clonedData和載入之後的新的Data屬性值比較一下,如果發現數據有變化,就呼叫OnReload()通知“資料變化了,來載入新資料吧”。

DoLoad方法中就是載入配置的值到Data屬性了,雖然程式碼比較多,但是邏輯並不複雜,主要就是根據“多層級資料的扁平化”規範來解析和載入資料。因為我之前對於這個規範沒有吃透,導致走了一些彎路。這塊也是我的這個開源專案的一個亮點,因為如果只是按照“多層級資料的扁平化”規範來儲存配置的話,資料庫中的name就必須“Ftp:IP”、“Ftp:UserName”、“Cors:Origins:0”、“Cors:Origins:1”、“Cors:Origins:2”這樣的方式寫,但是經過我的處理,配置的值就可以用可讀性非常強的json格式了(當然仍然相容嚴格的“多層級資料的扁平化”規範)。

5、 結論

3
最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • 25道Java經典面試題(附答案)