经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 数据库/运维 » Redis » 查看文章
从Redis读取.NET Core配置
来源:cnblogs  作者:赵榕  时间:2023/12/26 9:52:30  对本文有异议

在本文中,我们将创建一个自定义的.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。例如:

  1. {
  2. "Logging": {
  3. "LogLevel": {
  4. "Default": "Information",
  5. "Microsoft": "Warning",
  6. "Microsoft.Hosting.Lifetime": "Information"
  7. }
  8. },
  9. "ConnectionStrings": {
  10. "DefaultConnection": "Server=myserver;Database=mydb;User=myuser;Password=mypassword;"
  11. },
  12. "AppSettings": {
  13. "ApiBaseUrl": "https://api.example.com",
  14. "ApiKey": "your-api-key"
  15. },
  16. "AllowedHost":["foo1.com","foo2.com"]
  17. }

转换后的数据为:

  1. Logging:LogLevel:Default=Information
  2. Logging:LogLevel:Microsoft=Warning
  3. Logging:LogLevel:Microsoft.Hosting.Lifetime=Information
  4. ConnectionStrings:DefaultConnection=Server=myserver;Database=mydb;User=myuser;Password=mypassword;
  5. AppSettings:ApiBaseUrl=https://api.example.com
  6. AppSettings:ApiKey=your-api-key
  7. AllowedHost:0=foo1.com
  8. AllowedHost:1=foo2.com

二、Redis的Hash类型

通过上面介绍,Redis的Hash数据结构刚好完美的切合了这一特点。先简单的介绍一下:

在Redis中,Hash是一种数据结构,用于存储键值对的集合,其中每个键都映射到一个值。Redis的Hash是一个键值对的无序集合,其中的每个键都是唯一的。Hash是一个类似于字典或关联数组的概念,在其他编程语言中也称为Map或Dictionary。

三、代码实现

创建好项目之后,我们需要安装一个NuGet包,就是大家熟知的StackExchange.Redis,到目前为止应该是.NET应用程序使用最多的Redis客户端。

  1. PM> Install-Package StackExchange.Redis -v 2.7.10

您也可以通过Visual Studio、Rider自带的NuGet客户端安装,或者是直接在csproj文件中加入<PackageReference Include="StackExchange.Redis" Version="2.7.10" />

RedisConfigurationProvider.cs

  1. public sealed class RedisConfigurationProvider : ConfigurationProvider, IAsyncDisposable
  2. {
  3. private readonly ConnectionMultiplexer _connection;
  4. private readonly IDatabase _database;
  5. private readonly string _key;
  6. public RedisConfigurationProvider(RedisConfigurationSource source)
  7. {
  8. _key = source.Key;
  9. _connection = ConnectionMultiplexer.Connect(source.ConnectionString);
  10. _database = _connection.GetDatabase(source.Database);
  11. }
  12. /// <inheritdoc />
  13. public override void Load()
  14. {
  15. Data = _connection.HashGetAll(_key).ToDictionary(x => x.Name.ToString(), x => ReadRedisValue(x.Value);
  16. }
  17. private static string ReadRedisValue(RedisValue value)
  18. {
  19. if (value.IsNull)
  20. {
  21. return null;
  22. }
  23. return value.IsNullOrEmpty ? string.Empty : value.ToString();
  24. }
  25. /// <inheritdoc />
  26. public async ValueTask DisposeAsync()
  27. {
  28. await _connection.CloseAsync();
  29. await _connection.DisposeAsync();
  30. }
  31. }

RedisConfigurationSource.cs

  1. public sealed class RedisConfigurationSource : IConfigurationSource
  2. {
  3. /// <summary>
  4. /// The Redis connection string.
  5. /// </summary>
  6. [DisallowNull]
  7. public string ConnectionString { get; set; }
  8. /// <summary>
  9. /// Gets or sets the Redis database ID.
  10. /// </summary>
  11. public int Database { get; set; } = -1;
  12. /// <summary>
  13. /// Gets or sets the Redis key this source will read from.
  14. /// </summary>
  15. /// <remarks>
  16. /// The key is expected to be a hash.
  17. /// </remarks>
  18. public string Key { get; set; } = "appsettings";
  19. /// <inheritdoc />
  20. public IConfigurationProvider Build(IConfigurationBuilder builder)
  21. {
  22. return new RedisConfigurationProvider(this);
  23. }
  24. }

关键代码就这些,看上去似乎很简单……事实上确实很简单。

添加配置源

添加配置源的方法也很简单

  1. // Program.cs
  2. var builder = WebApplication.CreateBuilder(args);
  3. builder.Configuration.Add(new RedisConfigurationSource
  4. {
  5. ConnectionString = "localhost:6379",
  6. Key = "appsettings.dev"
  7. });

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

配置

  1. // Program.cs
  2. var builder = WebApplication.CreateBuilder(args);
  3. 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

  1. redis-cli config set notify-keyspace-events AKE

方法二:docker参数

  1. docker run -d --name redisname -p 6379:6379 redis --notify-keyspace-events AKE

方法三:配置文件

找到并打开打开redis.conf,在末尾加上

  1. notify-keyspace-events AKE

注意事项

  1. Redis本身自带持久化策略,但是有的企业/团队没有开启或者是特意关闭了持久化,因此需要谨慎使用此方案。
  2. 强烈建议将存储配置数据的key设置为永不过期(TTL设置为-1),避免key过期带来一些不必要的麻烦。

导入appsettings.json到Redis

微软.NET库提供了一个内部类JsonConfigurationFileParser用于将json格式的配置转换为Dictionary<string, string>

  1. namespace Microsoft.Extensions.Configuration.Json
  2. {
  3. internal sealed class JsonConfigurationFileParser
  4. {
  5. private JsonConfigurationFileParser() { }
  6. private readonly Dictionary<string, string?> _data = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
  7. private readonly Stack<string> _paths = new Stack<string>();
  8. public static IDictionary<string, string?> Parse(Stream input)
  9. => new JsonConfigurationFileParser().ParseStream(input);
  10. private Dictionary<string, string?> ParseStream(Stream input)
  11. {
  12. var jsonDocumentOptions = new JsonDocumentOptions
  13. {
  14. CommentHandling = JsonCommentHandling.Skip,
  15. AllowTrailingCommas = true,
  16. };
  17. using (var reader = new StreamReader(input))
  18. using (JsonDocument doc = JsonDocument.Parse(reader.ReadToEnd(), jsonDocumentOptions))
  19. {
  20. if (doc.RootElement.ValueKind != JsonValueKind.Object)
  21. {
  22. throw new FormatException(SR.Format(SR.Error_InvalidTopLevelJSONElement, doc.RootElement.ValueKind));
  23. }
  24. VisitObjectElement(doc.RootElement);
  25. }
  26. return _data;
  27. }
  28. private void VisitObjectElement(JsonElement element)
  29. {
  30. var isEmpty = true;
  31. foreach (JsonProperty property in element.EnumerateObject())
  32. {
  33. isEmpty = false;
  34. EnterContext(property.Name);
  35. VisitValue(property.Value);
  36. ExitContext();
  37. }
  38. SetNullIfElementIsEmpty(isEmpty);
  39. }
  40. private void VisitArrayElement(JsonElement element)
  41. {
  42. int index = 0;
  43. foreach (JsonElement arrayElement in element.EnumerateArray())
  44. {
  45. EnterContext(index.ToString());
  46. VisitValue(arrayElement);
  47. ExitContext();
  48. index++;
  49. }
  50. SetNullIfElementIsEmpty(isEmpty: index == 0);
  51. }
  52. private void SetNullIfElementIsEmpty(bool isEmpty)
  53. {
  54. if (isEmpty && _paths.Count > 0)
  55. {
  56. _data[_paths.Peek()] = null;
  57. }
  58. }
  59. private void VisitValue(JsonElement value)
  60. {
  61. Debug.Assert(_paths.Count > 0);
  62. switch (value.ValueKind)
  63. {
  64. case JsonValueKind.Object:
  65. VisitObjectElement(value);
  66. break;
  67. case JsonValueKind.Array:
  68. VisitArrayElement(value);
  69. break;
  70. case JsonValueKind.Number:
  71. case JsonValueKind.String:
  72. case JsonValueKind.True:
  73. case JsonValueKind.False:
  74. case JsonValueKind.Null:
  75. string key = _paths.Peek();
  76. if (_data.ContainsKey(key))
  77. {
  78. throw new FormatException(SR.Format(SR.Error_KeyIsDuplicated, key));
  79. }
  80. _data[key] = value.ToString();
  81. break;
  82. default:
  83. throw new FormatException(SR.Format(SR.Error_UnsupportedJSONToken, value.ValueKind));
  84. }
  85. }
  86. private void EnterContext(string context) =>
  87. _paths.Push(_paths.Count > 0 ?
  88. _paths.Peek() + ConfigurationPath.KeyDelimiter + context :
  89. context);
  90. private void ExitContext() => _paths.Pop();
  91. }
  92. }

点关注,不迷路。

如果您喜欢这篇文章,请不要忘记点赞、关注、转发,谢谢!如果您有任何高见,欢迎在评论区留言讨论……

公众号

原文链接:https://www.cnblogs.com/zhaorong/p/aspnet-configuration-redis.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号