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

共 10269字,需浏览 21分钟

 ·

2021-03-08 23:02


本文介绍了一个在.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

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



第二步:

创建一个ASP.NET 项目,演示案例是使用VisualStudio 2019创建.NET Core 3.1的ASP.NETCore 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(()=> newMySqlConnection(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 readonlyILogger<HomeController> _logger;

       private readonlyIConfiguration config;

       private readonlyIOptionsSnapshot<Ftp> ftpOpt;

       private readonlyIOptionsSnapshot<Cors> corsOpt;

 

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

       {

              _logger = logger;

              this.config =config;

              this.ftpOpt =ftpOpt;

              this.corsOpt =corsOpt;

       }

 

       public IActionResultIndex()

       {

              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 DbExceptionis thrown, restore to the original data.

              this.Data =clonedData;

              throw;

       }

       finally

       {

              lockObj.ExitWriteLock();

       }

       //OnReload cannot bebetween EnterWriteLock and ExitWriteLock, or "A read lock may not beacquired 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 maynot 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是一个可以用数据库做配置中心服务器的开源库,让你可以在不增加额外的配置中心服务器的情况下,让项目具备简单的版本管理的配置中心,而且以一种可读性很强的格式来进行配置。希望这个开源项目能够帮助大家,欢迎使用过程中反馈问题,如果感觉好用,欢迎推荐给其他朋友。



往期精彩回顾




【推荐】.NET Core开发实战视频课程 ★★★

.NET Core实战项目之CMS 第一章 入门篇-开篇及总体规划

【.NET Core微服务实战-统一身份认证】开篇及目录索引

Redis基本使用及百亿数据量中的使用技巧分享(附视频地址及观看指南)

.NET Core中的一个接口多种实现的依赖注入与动态选择看这篇就够了

10个小技巧助您写出高性能的ASP.NET Core代码

用abp vNext快速开发Quartz.NET定时任务管理界面

在ASP.NET Core中创建基于Quartz.NET托管服务轻松实现作业调度

现身说法:实际业务出发分析百亿数据量下的多表查询优化

关于C#异步编程你应该了解的几点建议

C#异步编程看这篇就够了


浏览 49
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报