系列目錄:【NET CORE微服務一條龍應用】開始篇與目錄html
在分佈式或者微服務系統裏,經過配置文件來管理配置內容,是一件比較使人痛苦的事情,再謹慎也有溼鞋的時候,這就是在項目架構發展的過程當中,配置中心存在的意義。git
其實配置中心的組件已經有很是出名的案例,好比攜程的阿波羅配置中心(https://github.com/ctripcorp/apollo)github
爲何又造輪子,由於不想發佈項目的時候處處切管理平臺。redis
做爲一個通用的配置組件,須要支持以下功能:json
一、客戶端定時刷新獲信最新配置信息並進行熱更新數組
二、配置有更新服務端主動推送重載或更新命令至客戶端進行配置獲取架構
因此涉及相對應組件以下:app
一、支持廣播的消息通知組件,目前使用redis(StackExchange.Redis)、Zookeeper(Rabbit.Zookeeper)實現客戶端全局監聽服務,服務端能夠推送不一樣組建不一樣的命令async
二、支持定時獲取最新配置,目前使用HostedService實現全局統一啓動,客戶端實現全局啓動接口,接口使用Timer進行定時獲取配置分佈式
三、支持net core原生IConfiguration接口獲取配置中心數據
管理服務端主要實現:
一、三表增刪改查
二、配置內容表,每次新增或者修改,當前配置信息版本號爲,因此配置最大版本號而後加一
三、應用表列表增長主動通知功能
主要提供配置信息的查詢接口
一、接口入參以下
public class QueryConfigInput { [NotEmpty("config_001","AppId不能爲空")] public string AppId { set; get; } public long Version { set; get; } [NotEmpty("config_002", "簽名不能爲空")] public string Sign { set; get; } [NotEmpty("config_005", "NamespaceName不能爲空")] public string NamespaceName { set; get; } public string Env { set; get; } }
二、查詢邏輯
2.1 入參基本驗證
2.2 AppId 密鑰進行簽名驗證
2.3 請求配置環境定位
2.4 查詢當前請求應用和共有配置應用
2.5 查詢大於當前查詢版本號的配置信息並返回
一、配置信息請求,當前Http請求,需根據配置信息組合請求url,而後請求獲取配置,每次請求帶上當前配置最大版本號(在之後請求時只獲取有更新的配置)
二、配置信息本地存儲(容災),第一次獲取成功後,把配置信息進行版本文件存儲,之後的請求中當有配置更新時再進行文件存儲。
三、當配置請求失敗時進行本地文件配置信息的還原應用。
四、配置定時獲取
五、客戶端接收更新或者重載命令
六、原生IConfiguration配置查詢支持
"ConfigServer": { "AppId": "PinzhiGO", "AppSercet": "xxxxxxxxxxxxx", "ServerUrl": "http://10.10.188.136:18081/", // 配置查詢服務端地址 "NamespaceName": "Pinzhi.Identity.WebApi", "Env": "dev", "RefreshInteval": 300 },
查看AddJsonFile源碼,能夠發現實現自定義配置源,須要集成和實現ConfigurationProvider和IConfigurationSource兩個方法
代碼以下
public class BucketConfigurationProvider : ConfigurationProvider, IDataChangeListener, IConfigurationSource { private readonly ConfigurationHelper _configurationHelper; public BucketConfigurationProvider(BucketConfigOptions options) { _configurationHelper = new ConfigurationHelper(options); Data = new ConcurrentDictionary<string, string>(); } public override void Load() { DataChangeListenerDictionary.Add(this); Data = _configurationHelper.Get().ConfigureAwait(false).GetAwaiter().GetResult(); } private void SetData(ConcurrentDictionary<string, string> changeData) { foreach(var dic in changeData) { if (Data.ContainsKey(dic.Key)) Data[dic.Key] = dic.Value; else Data.Add(dic); } // Data = new Dictionary<string, string>(_configRepository.Data, StringComparer.OrdinalIgnoreCase); } public void OnDataChange(ConcurrentDictionary<string, string> changeData) { SetData(changeData); OnReload(); } public IConfigurationProvider Build(IConfigurationBuilder builder) => this; }
當有配置更新時,咱們須要更新到ConfigurationProvider的Data中,因此咱們須要實現自定義接口IDataChangeListener的OnDataChange方法,當客戶端請求發現有配置更新時,會調用接口的OnDataChange把最新的配置信息傳遞進來。
啓用原生IConfiguration方法以下:
.ConfigureAppConfiguration((hostingContext, _config) => { _config .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile("appsettings.json", true, true) .AddJsonFile($"appsettings.{hostingContext.HostingEnvironment.EnvironmentName}.json", true, true) .AddEnvironmentVariables(); // 添加環境變量 var option = new BucketConfigOptions(); _config.Build().GetSection("ConfigServer").Bind(option); _config.AddBucketConfig(option); })
常規作法是寫一個hostedservice的方法,而後寫一個timer去定時獲取,因爲其餘的組件可能都須要有定時的狀況,咱們統一處理了一下定時的任務,每一個組件實現IExecutionService接口,而後組件會在啓動的時候循環調用IExecutionService的StartAsync的方法,組件包Bucket.Config.HostedService,原理比較簡單,使用代碼以下:
// 添加全局定時任務 services.AddBucketHostedService(builder => { builder.AddAuthorize().AddConfig().AddErrorCode(); });
public class AspNetCoreHostedService : IBucketAgentStartup { private readonly IEnumerable<IExecutionService> _services; public AspNetCoreHostedService(IEnumerable<IExecutionService> services) { _services = services; } public async Task StartAsync(CancellationToken cancellationToken = default(CancellationToken)) { foreach (var service in _services) await service.StartAsync(cancellationToken); } public async Task StopAsync(CancellationToken cancellationToken = default(CancellationToken)) { foreach (var service in _services) await service.StopAsync(cancellationToken); } }
和上面原則同樣,也進行了統一的封裝,目前監聽主要實現了redis和zookeeper,下面舉例redis
組件監聽需實現接口
public interface IBucketListener { string ListenerName { get; } Task ExecuteAsync(string commandText); }
命令序列化實體
public class NetworkCommand { public string NotifyComponent { set; get; } public string CommandText { set; get; } } public enum NetworkCommandType { /// <summary> /// 更新 /// </summary> Refresh, /// <summary> /// 重載 /// </summary> Reload, }
在hostedservice啓動時實現
public Task StartAsync(CancellationToken cancellationToken = default(CancellationToken)) { _subscriber = _redisClient.GetSubscriber(_redisListenerOptions.ConnectionString); return _subscriber.SubscribeAsync(RedisListenerKey, (channel, message) => { var command = JsonConvert.DeserializeObject<Bucket.Values.NetworkCommand>(message); _extractCommand.ExtractCommandMessage(command); }); }
在接口IExtractCommand裏會根據各個監聽組件的ListenerName進行對應的調用
使用方法以下:
// 添加應用監聽 services.AddListener(builder => { //builder.UseRedis(); builder.UseZookeeper(); builder.AddAuthorize().AddConfig().AddErrorCode(); });
因此對應組件實現的命令監聽只要關心自身邏輯便可嗎,代碼以下
public class BucketConfigListener : IBucketListener { public string ListenerName => "Bucket.Config"; private readonly IDataRepository _dataRepository; public BucketConfigListener(IDataRepository dataRepository) { _dataRepository = dataRepository; } public async Task ExecuteAsync(string commandText) { if (!string.IsNullOrWhiteSpace(commandText) && commandText == NetworkCommandType.Refresh.ToString()) await _dataRepository.Get(); if (!string.IsNullOrWhiteSpace(commandText) && commandText == NetworkCommandType.Reload.ToString()) await _dataRepository.Get(true); } }
配置中心使用配置以下
.ConfigureAppConfiguration((hostingContext, _config) => { _config .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile("appsettings.json", true, true) .AddJsonFile($"appsettings.{hostingContext.HostingEnvironment.EnvironmentName}.json", true, true) .AddEnvironmentVariables(); // 添加環境變量 var option = new BucketConfigOptions(); _config.Build().GetSection("ConfigServer").Bind(option); _config.AddBucketConfig(option); }) // ConfigureServices // 添加配置服務 services.AddConfigServer(Configuration); // 添加應用監聽 services.AddListener(builder => { //builder.UseRedis(); builder.UseZookeeper(); builder.AddAuthorize().AddConfig().AddErrorCode(); }); // 添加全局定時任務 services.AddBucketHostedService(builder => { builder.AddAuthorize().AddConfig().AddErrorCode(); }); //使用 private readonly IConfiguration _configuration; private readonly IConfig _config; public AuthController(IConfiguration configuration, IConfig config) { _configuration= configuration; _config= config; } // 獲取值 _configuration.GetValue<string>("qqqq"); _config.StringGet("qqqq");
因爲配置中心客戶端實現了原生的IConfiguration,因此appsetting的相關配置咱們徹底能夠移至配置中心中,因爲appsetting使用的是json,因此在配置中心服務端配置信息的Key須要轉換,舉例:
"BucketListener": { "Redis": { "ConnectionString": "127.0.0.1:6379,allowadmin=true", "ListenerKey": "Bucket.Sample" }, "Zookeeper": { "ConnectionString": "localhost:2181", "ListenerKey": "Bucket.Sample" } }
在配置中心key以下:
BucketListener:Redis:ConnectionString
BucketListener:Redis:ListenerKey
......
數組使用以下:
DbConfig:0:Name
DbConfig:0:DbType
DbConfig:1:Name
DbConfig:1:DbType
我的寫做水平有限,涉及的東西也不少,篇幅有限因此只作了大致介紹,忘諒解
本章涉及源碼
https://github.com/q315523275/FamilyBucket/tree/master/src/Config 客戶端組件