上一篇介绍了.net core的配置原理已经系统提供的一些常用的配置,但有时我们的配置是存放在Zookeeper,DB,Redis中的,这就需要我们自己去实现集成了。
这里再介绍几个我们用的多的配置集成方案,可以加强我们对.net core配置机制的理解。
Zookeeper
.net core集成使用Zookeeper的做法在之前的博客中已经介绍过了,祥看:Zookeeper基础教程(六):.net core使用Zookeeper
通用配置
对于配置,数量一般不会很多,放在内存中一般时候是可以接受的,这样一方面方便使用,另一方面避免了频繁访问数据库或者Redis等第三方设置带来的性能消耗。
其实,对于数据在数据库或者redis等其他存储设备上的集成,官方给我们提供了一个InMemory解决方案,也就是上一篇说到的从内存集合中获取配置的方案。在使用时,我们只需要在程序启动时从数据库或者Redis等位置将数据读取出来,然后已InMemory的方式集成进去就行了。但是这样做有个不足,就是当配置被修改时,我们需要重新启动应用服务才能生效,这样就是放弃了.net core配置重新加载机制,因此我们需要自己去实现这个集成。
通过上述,我们可以认为数据库和Redis是同一类东西,但是又不能使用InMemory解决方案,因此,为满足以后的其它第三方配置需求,我们希望有一种通用的配置提供者,它需要满足两点:
1、提供配置所需的数据 2、能触发配置的重新加载更新
为满足这两点,我们可以提供一个接口,如:
public interface IDataProvider { /// <summary> /// 获取配置数据 /// </summary> /// <returns></returns> IDictionary<string, string> Process(); /// <summary> /// 开启监听,决定什么时候触发重新加载配置 /// </summary> /// <param name="trigger"></param> void Watch(Action trigger); }
在上一篇我们已经讲到,集成配置需要我们自己实现两个接口:IConfigurationSource 和 IConfigurationProvider 。另外还提到,如果我们的配置来自其它文件,推荐分别继承 FileConfigurationSource 和 FileConfigurationProvider 两个抽象类,否则推荐自己实现 IConfigurationSource 接口,但是 IConfigurationProvider 接口的实现类继承 ConfigurationProvider 抽象类,显然数据库和Redis之类的都不是文件,于是我们可以有这样两个实现类:
public class CommonConfigurationSource : IConfigurationSource { public Type DataProviderType { get; set; } /// <summary> /// 是否监控源数据变化 /// </summary> public bool ReloadOnChange { get; set; } = true; /// <summary> /// 加载延迟 /// </summary> public int ReloadDelay { get; set; } = 250; public IConfigurationProvider Build(IConfigurationBuilder builder) { if (!typeof(IDataProvider).IsAssignableFrom(DataProviderType)) { throw new ArgumentException("Data Provider Type must implement IDataProvider"); } return new CommonConfigurationProvider(this); } }
public class CommonConfigurationProvider : ConfigurationProvider, IDisposable { ConfigurationReloadToken _reloadToken = new ConfigurationReloadToken(); CommonConfigurationSource commonConfigurationSource; IDisposable _changeTokenRegistration; IDataProvider dataProvider; public CommonConfigurationProvider(CommonConfigurationSource commonConfigurationSource) { this.commonConfigurationSource = commonConfigurationSource; this.dataProvider = Activator.CreateInstance(commonConfigurationSource.DataProviderType) as IDataProvider; if (commonConfigurationSource.ReloadOnChange) { dataProvider.Watch(() => { OnReload(); }); _changeTokenRegistration = ChangeToken.OnChange( () => GetReloadToken(), () => { Thread.Sleep(commonConfigurationSource.ReloadDelay); Load(); }); } } /// <summary> /// 加载配置 /// </summary> public override void Load() { Data = dataProvider.Process(); } /// <summary> /// 释放 /// </summary> public void Dispose() { _changeTokenRegistration?.Dispose(); } }
还没有完,同上一篇介绍的.net core自带的配置一样,我们还需要提供拓展方法去集成:
public static class CommonConfigurationExtensions { /// <summary> /// 集成通用配置 /// </summary> /// <param name="builder"></param> /// <param name="dataProviderType"></param> /// <param name="reloadOnChange"></param> /// <param name="reloadDelay"></param> /// <returns></returns> public static IConfigurationBuilder AddCommonConfiguration(this IConfigurationBuilder builder, Type dataProviderType, bool reloadOnChange = false, int reloadDelay = 250) { return builder.Add<CommonConfigurationSource>(source => { source.DataProviderType = dataProviderType; source.ReloadDelay = reloadDelay; source.ReloadOnChange = reloadOnChange; }); } /// <summary> /// 集成通用配置 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="builder"></param> /// <param name="reloadOnChange"></param> /// <param name="reloadDelay"></param> /// <returns></returns> public static IConfigurationBuilder AddCommonConfiguration<T>(this IConfigurationBuilder builder, bool reloadOnChange = false, int reloadDelay = 250) where T : IDataProvider, new() { return builder.AddCommonConfiguration(typeof(T), reloadOnChange, reloadDelay); } }
接下来我们看看怎么使用。
数据库(MySql)
数据库我们以mysql为例,接下来我们只需要实现IDataProvider接口,用来实现配置数据的提供和配置的重新加载,比如我的数据中有数据:
而接下来我们的IDataProvider接口实现类是:
/// <summary> /// 要求存在空构造函数 /// </summary> public class MysqlDataProvider : IDataProvider { private IDbConnection GetDbConnection() { string connectionString = @"Server=192.168.209.128;Port=3306;Database=test;Uid=root;Pwd=123456"; return new MySqlConnector.MySqlConnection(connectionString); } /// <summary> /// 获取配置数据 /// </summary> /// <returns></returns> public IDictionary<string, string> Process() { using (var con = GetDbConnection()) { if (con.State != ConnectionState.Open) { con.Open(); } using (var cmd = con.CreateCommand()) { cmd.CommandText = "SELECT `Key`,`Value` FROM Configurations"; var reader = cmd.ExecuteReader(CommandBehavior.CloseConnection); IDictionary<string, string> dict = new Dictionary<string, string>(); while (reader.Read()) { var key = reader.GetString(0); var value = reader.GetString(1); dict[key] = value; } return dict; } } } /// <summary> /// 开启监听,决定什么时候触发重新加载配置 /// </summary> /// <param name="trigger"></param> public void Watch(Action trigger) { //表示什么时候出发一次配置重新加载 //比如定时加载 var timer = new System.Timers.Timer(); timer.Interval = 1000 * 60;//一分钟加载一次 timer.Elapsed += new System.Timers.ElapsedEventHandler((sender, e) => { timer.Enabled = false; trigger.Invoke(); timer.Enabled = true; }); timer.Start(); } }
其中,在实现Watch方法时,我们采用了一个定时间,定时从数据库获取配置,当然,最佳做法是使用一条消息总线来实现,比如采用消息队列等,这里只是简单的说明介绍一下。
接下来看看怎么使用:
static void Main(string[] args) { ConfigurationBuilder builder = new ConfigurationBuilder(); builder.AddCommonConfiguration<MysqlDataProvider>(true); var configuration = builder.Build(); do { Console.Write("请输入指令:"); var line = Console.ReadLine(); if (line == "reload" || line == "r") { configuration.Reload(); } else if (line == "print" || line == "p") { var collections = configuration.AsEnumerable(); foreach (var item in collections) { Console.WriteLine("{0}={1}", item.Key, item.Value); } } } while (true); }
如果是.net core webapi或者mvc,只需要:
public class Program { public static void Main(string[] args) { CreateHostBuilder(args).Build().Run(); } public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); webBuilder.ConfigureAppConfiguration((hostingContext, config) => { config.AddCommonConfiguration<MysqlDataProvider>(true); }); }); }
运行后输入p:
接下来可以修改数据库中的配置:
接下来只需要等1分钟(MysqlDataProvider 中Watch方法中的定时器1分钟刷新),或者直接输入r,使用IConfigurationRoot的Reload方法强制刷新。
刷新之后,再输入p,查看到配置已自动更新了。
可以看到,数据库配置以被重新加载。
Redis
接下来说说Redis,假如我们的Redis有如下结构的配置,其中Config表示的是前缀,表明这个前缀下的RedisKey都是配置:
在集成之前,同样的,我们需要先实现IDataProvider接口:
public class RedisDataProvider : IDataProvider { StackExchange.Redis.ConnectionMultiplexer connectionMultiplexer; public StackExchange.Redis.IConnectionMultiplexer GetConnectionMultiplexer() { if (connectionMultiplexer == null) { var configurationOptions = new StackExchange.Redis.ConfigurationOptions(); configurationOptions.DefaultDatabase = 0; configurationOptions.EndPoints.Add("192.168.209.128:6379"); connectionMultiplexer = StackExchange.Redis.ConnectionMultiplexer.Connect(configurationOptions); } return connectionMultiplexer; } public IDictionary<string, string> Process() { var connectionMultiplexer = GetConnectionMultiplexer(); int database = 0; var server = connectionMultiplexer.GetServer(connectionMultiplexer.GetEndPoints().First()); var db = connectionMultiplexer.GetDatabase(database); IDictionary<string, string> dict = new Dictionary<string, string>(); int pageSize = 10; int pageOffset = 0; string prefix = "Config";//加载配置节点的前缀 while (true) { var keys = server.Keys( database: database, pattern: new StackExchange.Redis.RedisValue(prefix + "*"), pageSize: pageSize, pageOffset: pageOffset ); if (keys.Count() == 0) break; foreach (var key in keys) { try { var value = db.StringGet(key); dict[key.ToString().Substring(prefix.Length).TrimStart(':')] = value.ToString(); } catch { continue; } } pageOffset += pageSize; } return dict; } public void Watch(Action trigger) { //采用Redis的发布订阅模式进行监听 var connectionMultiplexer = GetConnectionMultiplexer(); var subscriber = connectionMultiplexer.GetSubscriber(); subscriber.Subscribe(new StackExchange.Redis.RedisChannel("Watch_RedisChannel", StackExchange.Redis.RedisChannel.PatternMode.Auto), (channel, message) => { trigger?.Invoke(); }); } }
在介绍数据库的使用时,提到在Watch中推荐使用消息总线来实现重新加载,于是乎,我们利用Redis的订阅与发布来实现这个功能,真是一举两得
另外,在开发过程中,我们应该讲Redis中配置与缓存等数据分开,比如使用不同的database来存放,这样允许我们在读取配置的时候避免读取到大量的缓存数据而影响到性能。
接下来看看使用:
static void Main(string[] args) { ConfigurationBuilder builder = new ConfigurationBuilder(); builder.AddCommonConfiguration<RedisDataProvider>(true); var configuration = builder.Build(); do { Console.Write("请输入指令:"); var line = Console.ReadLine(); if (line == "reload" || line == "r") { configuration.Reload(); } else if (line == "publish") { var configurationOptions = new StackExchange.Redis.ConfigurationOptions(); configurationOptions.DefaultDatabase = 0; configurationOptions.EndPoints.Add("192.168.209.128:6379"); var connectionMultiplexer = new RedisDataProvider().GetConnectionMultiplexer(); connectionMultiplexer.GetSubscriber().Publish("Watch_RedisChannel", "Reload"); } else if (line == "print" || line == "p") { var collections = configuration.AsEnumerable(); foreach (var item in collections) { Console.WriteLine("{0}={1}", item.Key, item.Value); } } } while (true); }
如果是.net core webapi或者mvc,只需要:
public class Program { public static void Main(string[] args) { CreateHostBuilder(args).Build().Run(); } public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); webBuilder.ConfigureAppConfiguration((hostingContext, config) => { config.AddCommonConfiguration<RedisDataProvider>(true); }); }); }
运行后输入p:
接下来,我们修改Redis中的配置,比如使用RedisManager工具修改,比如修改一个配置:
保存后,在控制台输入publish发布重新加载的订阅消息,或者输入r,使用IConfigurationRoot的Reload方法强制刷新。
刷新之后,再输入p,查看到配置已自动更新了。
可见集成完成!
总结
通过这里的例子,应该能对.net core提供的配合有个更完整的了解。这里虽然给出了一种通用集成配置的方案,比如数据库和Redis的集成,但是还需要读者提供一个IDataProvider接口的实现类,不过这也算是一种练习吧。