有你在真好 的个人博客
.NET5用数据库做配置中心加载Configuration
阅读:2206 添加日期:2021/3/27 23:16:05 原文链接:https://www.toutiao.com/item/6934523731175916046/

本文介绍了一个在.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的多行数据,其中Id值最大的一条的值生效,这样就实现了简单的配置版本管理。因此,如果不确认一个新的配置项一定成功的话,可以先新增一条同名的配置,如果出现问题,只要把这条数据删除就可以回滚到旧的配置项。

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

下面这个数据就是后续演示使用的数据:


.NET5用数据库做配置中心加载Configuration

演示数据

第二步:

创建一个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();

}

}

关于把读取出来的配置如何使用就不再介绍了。我这里只是把配置显示到界面上。你可以把配置修改后,再刷新界面,就可以看到修改后的配置。


.NET5用数据库做配置中心加载Configuration

运行效果

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、 结论

Zack.AnyDBConfigProvider是一个可以用数据库做配置中心服务器的开源库,让你可以在不增加额外的配置中心服务器的情况下,让项目具备简单的版本管理的配置中心,而且以一种可读性很强的格式来进行配置。希望这个开源项目能够帮助大家,欢迎使用过程中反馈问题,如果感觉好用,欢迎推荐给其他朋友。

ICP备案号:苏ICP备14035786号-1 苏公网安备 32050502001014号