从Redis读取.NET Core配置
在本文中,我們將創建一個自定義的.NET Core應用配置源和提供程序,用于從Redis中讀取配置。在此之前,您需要稍微了解一些.NET Core配置提供程序的工作原理,相關的內容可以在Microsoft開發者官網搜索到。另外您可能還需要了解一些Redis的基礎知識,比如Redis的基礎數據類型,持久化等等。
一、配置的數據格式
.NET Core應用支持多種配置源(例如json、xml、ini文件,環境變量,內存字典,自定義源等),并且支持同時添加多個配置源,這也是本文的前提條件。應用程序會按照加入的先后順序替換或補充配置。默認情況下,.NET Core應用的配置是存儲在appsettings.json文件中的。在早期的.NET Core應用中,Program.cs的CreateHost方法里面還能看到AddJsonFile("appsettings.json").AddJsonFile($"appsetting.{env.Environment}.json")這樣的代碼,但是.NET 5以后,這段代碼默認被隱藏了。
看過源碼的朋友應該知道,.NET Core應用讀取配置后,會將數據轉換為一個Key和Value都是string的字典。Key的格式為Node1:Node2:abc。例如:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"ConnectionStrings": {
"DefaultConnection": "Server=myserver;Database=mydb;User=myuser;Password=mypassword;"
},
"AppSettings": {
"ApiBaseUrl": "https://api.example.com",
"ApiKey": "your-api-key"
},
"AllowedHost":["foo1.com","foo2.com"]
}
轉換后的數據為:
Logging:LogLevel:Default=Information
Logging:LogLevel:Microsoft=Warning
Logging:LogLevel:Microsoft.Hosting.Lifetime=Information
ConnectionStrings:DefaultConnection=Server=myserver;Database=mydb;User=myuser;Password=mypassword;
AppSettings:ApiBaseUrl=https://api.example.com
AppSettings:ApiKey=your-api-key
AllowedHost:0=foo1.com
AllowedHost:1=foo2.com
二、Redis的Hash類型
通過上面介紹,Redis的Hash數據結構剛好完美的切合了這一特點。先簡單的介紹一下:
在Redis中,Hash是一種數據結構,用于存儲鍵值對的集合,其中每個鍵都映射到一個值。Redis的Hash是一個鍵值對的無序集合,其中的每個鍵都是唯一的。Hash是一個類似于字典或關聯數組的概念,在其他編程語言中也稱為Map或Dictionary。
三、代碼實現
創建好項目之后,我們需要安裝一個NuGet包,就是大家熟知的StackExchange.Redis,到目前為止應該是.NET應用程序使用最多的Redis客戶端。
PM> Install-Package StackExchange.Redis -v 2.7.10
您也可以通過Visual Studio、Rider自帶的NuGet客戶端安裝,或者是直接在csproj文件中加入<PackageReference Include="StackExchange.Redis" Version="2.7.10" />。
RedisConfigurationProvider.cs
public sealed class RedisConfigurationProvider : ConfigurationProvider, IAsyncDisposable
{
private readonly ConnectionMultiplexer _connection;
private readonly IDatabase _database;
private readonly string _key;
public RedisConfigurationProvider(RedisConfigurationSource source)
{
_key = source.Key;
_connection = ConnectionMultiplexer.Connect(source.ConnectionString);
_database = _connection.GetDatabase(source.Database);
}
/// <inheritdoc />
public override void Load()
{
Data = _connection.HashGetAll(_key).ToDictionary(x => x.Name.ToString(), x => ReadRedisValue(x.Value);
}
private static string ReadRedisValue(RedisValue value)
{
if (value.IsNull)
{
return null;
}
return value.IsNullOrEmpty ? string.Empty : value.ToString();
}
/// <inheritdoc />
public async ValueTask DisposeAsync()
{
await _connection.CloseAsync();
await _connection.DisposeAsync();
}
}
RedisConfigurationSource.cs
public sealed class RedisConfigurationSource : IConfigurationSource
{
/// <summary>
/// The Redis connection string.
/// </summary>
[DisallowNull]
public string ConnectionString { get; set; }
/// <summary>
/// Gets or sets the Redis database ID.
/// </summary>
public int Database { get; set; } = -1;
/// <summary>
/// Gets or sets the Redis key this source will read from.
/// </summary>
/// <remarks>
/// The key is expected to be a hash.
/// </remarks>
public string Key { get; set; } = "appsettings";
/// <inheritdoc />
public IConfigurationProvider Build(IConfigurationBuilder builder)
{
return new RedisConfigurationProvider(this);
}
}
關鍵代碼就這些,看上去似乎很簡單……事實上確實很簡單。
添加配置源
添加配置源的方法也很簡單
// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Configuration.Add(new RedisConfigurationSource
{
ConnectionString = "localhost:6379",
Key = "appsettings.dev"
});
RedisConfigurationSource里面總共只有三個屬性,ConnectionString用于配置Redis連接字符串,Database用于指定從哪個數據庫讀取數據,也可以在連接字符串里面指定。Key用于指定要讀取的鍵名稱。
通過編寫一些簡單的代碼,我們實現了一個能滿足基本需求的分布式.NET Core配置提供程序。
Starfish.Redis
不想動手的朋友可以直接用我已經制作好的包
https://www.nuget.org/packages/Starfish.Redis
安裝
Visual Studio包管理器搜索Starfish.Redis,或者執行dotnet add package Starfish.Redis。
配置
// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddRedis("127.0.0.1:6379,defaultDatabase=0,connectTimeout=5000,connectRetry=3", "appsettings");
啟用Redis Keyspace Notifications
Starfish.Redis有兩種機制用于實現ReloadOnChanged(配置修改后重新加載數據),一種是定時查詢指定的Key,時效性稍微差一些。另一種是利用Redis的Keyspace Event和Pub/Sub模式來實現,當訂閱的Key發生變化(刪除、修改、過期等)時會主動發送通知給訂閱者,使用這種模式需要配置Redis服務的notify-keyspace-events。
關于notify-keyspace-events配置,可參考下面的描述:
- K:Keyspace事件,將會以__keyspace@
__作為事件的前綴 - E:Keyevent事件,將會以__keyevent@
__作為事件的前綴 - g:非特定類型的通用命令,例如DEL、EXPIRE、RENAME等
- $:字符串命令,例如SET、INCR等
- l:列表命令,例如LPUSH、LPOP等
- s:集合命令,例如SADD、SREM等
- h:哈希表命令,例如HSET、HINCRBY等
- z:有序集合命令,例如ZSET、ZREM等
- t:流命令,例如XADD、XDEL等
- x:過期事件(在每個發生鍵過期的時侯產生)
- e:淘汰事件(在每個發生鍵被淘汰的時候產生)
- m:未命中事件(在訪問某個不存在的鍵使產生)
- A:配置g$lshztxe的別名,但不包括未命中事件m
簡單起見,我們直接配置為AKE(啟用所有事件的通知)。
方法一:redis-cli
redis-cli config set notify-keyspace-events AKE
方法二:docker參數
docker run -d --name redisname -p 6379:6379 redis --notify-keyspace-events AKE
方法三:配置文件
找到并打開打開redis.conf,在末尾加上
notify-keyspace-events AKE
注意事項
- Redis本身自帶持久化策略,但是有的企業/團隊沒有開啟或者是特意關閉了持久化,因此需要謹慎使用此方案。
- 強烈建議將存儲配置數據的key設置為永不過期(TTL設置為-1),避免key過期帶來一些不必要的麻煩。
導入appsettings.json到Redis
微軟.NET庫提供了一個內部類JsonConfigurationFileParser用于將json格式的配置轉換為Dictionary<string, string>。
namespace Microsoft.Extensions.Configuration.Json
{
internal sealed class JsonConfigurationFileParser
{
private JsonConfigurationFileParser() { }
private readonly Dictionary<string, string?> _data = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
private readonly Stack<string> _paths = new Stack<string>();
public static IDictionary<string, string?> Parse(Stream input)
=> new JsonConfigurationFileParser().ParseStream(input);
private Dictionary<string, string?> ParseStream(Stream input)
{
var jsonDocumentOptions = new JsonDocumentOptions
{
CommentHandling = JsonCommentHandling.Skip,
AllowTrailingCommas = true,
};
using (var reader = new StreamReader(input))
using (JsonDocument doc = JsonDocument.Parse(reader.ReadToEnd(), jsonDocumentOptions))
{
if (doc.RootElement.ValueKind != JsonValueKind.Object)
{
throw new FormatException(SR.Format(SR.Error_InvalidTopLevelJSONElement, doc.RootElement.ValueKind));
}
VisitObjectElement(doc.RootElement);
}
return _data;
}
private void VisitObjectElement(JsonElement element)
{
var isEmpty = true;
foreach (JsonProperty property in element.EnumerateObject())
{
isEmpty = false;
EnterContext(property.Name);
VisitValue(property.Value);
ExitContext();
}
SetNullIfElementIsEmpty(isEmpty);
}
private void VisitArrayElement(JsonElement element)
{
int index = 0;
foreach (JsonElement arrayElement in element.EnumerateArray())
{
EnterContext(index.ToString());
VisitValue(arrayElement);
ExitContext();
index++;
}
SetNullIfElementIsEmpty(isEmpty: index == 0);
}
private void SetNullIfElementIsEmpty(bool isEmpty)
{
if (isEmpty && _paths.Count > 0)
{
_data[_paths.Peek()] = null;
}
}
private void VisitValue(JsonElement value)
{
Debug.Assert(_paths.Count > 0);
switch (value.ValueKind)
{
case JsonValueKind.Object:
VisitObjectElement(value);
break;
case JsonValueKind.Array:
VisitArrayElement(value);
break;
case JsonValueKind.Number:
case JsonValueKind.String:
case JsonValueKind.True:
case JsonValueKind.False:
case JsonValueKind.Null:
string key = _paths.Peek();
if (_data.ContainsKey(key))
{
throw new FormatException(SR.Format(SR.Error_KeyIsDuplicated, key));
}
_data[key] = value.ToString();
break;
default:
throw new FormatException(SR.Format(SR.Error_UnsupportedJSONToken, value.ValueKind));
}
}
private void EnterContext(string context) =>
_paths.Push(_paths.Count > 0 ?
_paths.Peek() + ConfigurationPath.KeyDelimiter + context :
context);
private void ExitContext() => _paths.Pop();
}
}
點關注,不迷路。
如果您喜歡這篇文章,請不要忘記點贊、關注、轉發,謝謝!如果您有任何高見,歡迎在評論區留言討論……
總結
以上是生活随笔為你收集整理的从Redis读取.NET Core配置的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【eBPF-01】初见:基于 BCC 框
- 下一篇: 教育方法文献的论文