功能概览

键空间通知使得客户端可以通过订阅频道或模式, 来接收那些以某种方式改动了 Redis 数据集的事件。
以下是一些键空间通知发送的事件的例子:

事件通过 Redis 的订阅与发布功能(pub/sub)来进行分发, 因此所有支持订阅与发布功能的客户端都可以在无须做任何修改的情况下, 直接使用键空间通知功能。
因为 Redis 目前的订阅与发布功能采取的是发送即忘(fire and forget)策略, 所以如果你的程序需要可靠事件通知(reliable notification of events), 那么目前的键空间通知可能并不适合你: 当订阅事件的客户端断线时, 它会丢失所有在断线期间分发给它的事件。
未来将会支持更可靠的事件分发, 这种支持可能会通过让订阅与发布功能本身变得更可靠来实现, 也可能会在 Lua 脚本中对消息(message)的订阅与发布进行监听, 从而实现类似将事件推入到列表这样的操作。

事件的类型

对于每个修改数据库的操作,键空间通知都会发送两种不同类型的事件。
比如说,对 0 号数据库的键 mykey 执行 DEL key [key …] 命令时, 系统将分发两条消息, 相当于执行以下两个 PUBLISH channel message 命令:

  1. PUBLISH __keyspace@0__:mykey del
  2. PUBLISH __keyevent@0__:del mykey

订阅第一个频道 __keyspace@0__:mykey 可以接收 0 号数据库中所有修改键 mykey 的事件, 而订阅第二个频道 __keyevent@0__:del 则可以接收 0 号数据库中所有执行 del 命令的键。
keyspace 为前缀的频道被称为键空间通知(key-space notification), 而以 keyevent 为前缀的频道则被称为键事件通知(key-event notification)
del mykey 命令执行时:

  • 键空间频道的订阅者将接收到被执行的事件的名字,在这个例子中,就是 del
  • 键事件频道的订阅者将接收到被执行事件的键的名字,在这个例子中,就是 mykey

    配置

    因为开启键空间通知功能需要消耗一些 CPU , 所以在默认配置下, 该功能处于关闭状态。
    可以通过修改 redis.conf 文件, 或者直接使用 CONFIG SET 命令来开启或关闭键空间通知功能:

  • notify-keyspace-events 选项的参数为空字符串时,功能关闭。

  • 另一方面,当参数不是空字符串时,功能开启。

notify-keyspace-events 的参数可以是以下字符的任意组合, 它指定了服务器该发送哪些类型的通知:

字符 发送的通知
K 键空间通知,所有通知以 __keyspace@<db>__ 为前缀
E 键事件通知,所有通知以 __keyevent@<db>__ 为前缀
g DELEXPIRERENAME 等类型无关的通用命令的通知
$ 字符串命令的通知
l 列表命令的通知
s 集合命令的通知
h 哈希命令的通知
z 有序集合命令的通知
x 过期事件:每当有过期键被删除时发送
e 驱逐(evict)事件:每当有键因为 maxmemory 政策而被删除时发送
A 参数 g$lshzxe 的别名

输入的参数中至少要有一个 K 或者 E , 否则的话, 不管其余的参数是什么, 都不会有任何通知被分发。
举个例子, 如果只想订阅键空间中和列表相关的通知, 那么参数就应该设为 Kl , 诸如此类。
将参数设为字符串 "AKE" 表示发送所有类型的通知。

命令产生的通知

以下列表记录了不同命令所产生的不同通知:

如果对命令所产生的通知有疑问, 最好还是使用以下命令, 自己来验证一下:

  1. $ redis-cli config set notify-keyspace-events KEA
  2. $ redis-cli --csv psubscribe '__key*__:*'
  3. Reading messages... (press Ctrl-C to quit)
  4. "psubscribe","__key*__:*",1

然后, 只要在其他终端里用 Redis 客户端发送命令, 就可以看到产生的通知了:
“pmessage”,”key*:“,”keyspace@0:foo”,”set”
“pmessage”,”__key
:*”,”keyevent@0__:set”,”foo”

过期通知的发送时间

Redis 使用以下两种方式删除过期的键:

  • 当一个键被访问时,程序会对这个键进行检查,如果键已经过期,那么该键将被删除。
  • 底层系统会在后台渐进地查找并删除那些过期的键,从而处理那些已经过期、但是不会被访问到的键。

当过期键被以上两个程序的任意一个发现、 并且将键从数据库中删除时, Redis 会产生一个 expired 通知。
Redis 并不保证生存时间(TTL)变为 0 的键会立即被删除: 如果程序没有访问这个过期键, 或者带有生存时间的键非常多的话, 那么在键的生存时间变为 0 , 直到键真正被删除这中间, 可能会有一段比较显著的时间间隔。
因此, Redis 产生 expired 通知的时间为过期键被删除的时候, 而不是键的生存时间变为 0 的时候。

StackExchange.Redis事例

Redis设置

notify-keyspace-events 的参数可以是以下字符的任意组合, 它指定了服务器该发送哪些类型的通知:

字符 发送的通知
K 键空间通知,所有通知以 __keyspace@<db>__ 为前缀
E 键事件通知,所有通知以 __keyevent@<db>__ 为前缀
g DELEXPIRERENAME 等类型无关的通用命令的通知
$ 字符串命令的通知
l 列表命令的通知
s 集合命令的通知
h 哈希命令的通知
z 有序集合命令的通知
x 过期事件:每当有过期键被删除时发送
e 驱逐(evict)事件:每当有键因为 maxmemory 政策而被删除时发送
A 参数 g$lshzxe 的别名

notify-keyspace-events 设置为”gxeE”

  1. localhost@6379:0>CONFIG SET 'notify-keyspace-events' "gxeE"
  2. localhost@6379:0>CONFIG GET 'notify-keyspace-events'

image.png

阿里云Redis配置

note_attach.png
image.png

C#代码

  1. /// <summary>
  2. /// Gets the servers.
  3. /// </summary>
  4. /// <value>The servers.</value>
  5. public IEnumerable<IServer> Servers
  6. {
  7. get
  8. {
  9. var endpoints = redisConn.GetEndPoints();
  10. foreach (var endpoint in endpoints)
  11. {
  12. var server = redisConn.GetServer(endpoint);
  13. yield return server;
  14. }
  15. }
  16. }
  17. /// <summary>
  18. /// Gets the configuration.
  19. /// </summary>
  20. /// <param name="key">The key.</param>
  21. /// <returns>Dictionary&lt;System.Net.EndPoint, System.String&gt;.</returns>
  22. private Dictionary<System.Net.EndPoint, string> GetConfiguration(string key)
  23. {
  24. var result = new Dictionary<System.Net.EndPoint, string>();
  25. foreach (var server in Servers)
  26. {
  27. var values = server.ConfigGet(key).ToDictionary(k => k.Key, v => v.Value);
  28. if (values.ContainsKey(key))
  29. {
  30. var value = values.FirstOrDefault(p => p.Key == key);
  31. result.Add(server.EndPoint, value.Value);
  32. }
  33. }
  34. return result;
  35. }
  36. /// <summary>
  37. /// Initializes the keyspace notifications.
  38. /// </summary>
  39. private void InitKeyspaceNotifications()
  40. {
  41. if (!KeyspaceNotificationsEnabled)
  42. {
  43. return;
  44. }
  45. // notify-keyspace-events needs to be set to "Exe" at least! Otherwise we will not receive any events.
  46. // this must be configured per server and should probably not be done automagically as this needs admin rights!
  47. // Let's try to check at least if those settings are configured (the check also works only if useAdmin is set to true though).
  48. try
  49. {
  50. var configurations = GetConfiguration("notify-keyspace-events");
  51. foreach (var cfg in configurations)
  52. {
  53. if (!cfg.Value.Contains("E"))
  54. {
  55. SysLogHelper.Error("InitKeyspaceNotifications", $"Server { cfg.Key} is missing configuration value 'E' in notify-keyspace-events to enable keyevents.");
  56. }
  57. if (!(cfg.Value.Contains("A") || (cfg.Value.Contains("x") && cfg.Value.Contains("e"))))
  58. {
  59. SysLogHelper.Error("InitKeyspaceNotifications", $"Server { cfg.Key} is missing configuration value 'A' or 'x' and 'e' in notify-keyspace-events to enable keyevents for expired and evicted keys.");
  60. }
  61. }
  62. }
  63. catch (Exception ex)
  64. {
  65. SysLogHelper.Error(ex, strAddition: "Could not read configuration from redis to validate notify-keyspace-events. Most likely useAdmin is not set to true.");
  66. }
  67. SubscribeKeyspaceNotifications();
  68. }
  69. private const string Base64Prefix = "base64\0";
  70. /// <summary>
  71. /// Parses the key.
  72. /// </summary>
  73. /// <param name="value">The value.</param>
  74. /// <returns>Tuple&lt;System.String, System.String&gt;.</returns>
  75. private Tuple<string, string> ParseKey(string value)
  76. {
  77. if (value == null)
  78. {
  79. return Tuple.Create<string, string>(null, null);
  80. }
  81. var sepIndex = value.IndexOf(':');
  82. var hasRegion = sepIndex > 0;
  83. var key = value;
  84. string region = null;
  85. if (hasRegion)
  86. {
  87. region = value.Substring(0, sepIndex);
  88. key = value.Substring(sepIndex + 1);
  89. if (region.StartsWith(Base64Prefix))
  90. {
  91. region = region.Substring(Base64Prefix.Length);
  92. region = Encoding.UTF8.GetString(Convert.FromBase64String(region));
  93. }
  94. }
  95. if (key.StartsWith(Base64Prefix))
  96. {
  97. key = key.Substring(Base64Prefix.Length);
  98. key = Encoding.UTF8.GetString(Convert.FromBase64String(key));
  99. }
  100. return Tuple.Create(key, region);
  101. }
  102. /// <summary>
  103. /// Can be used to signal a remove event to the <see cref="ICacheManager{TCacheValue}"/> in case the underlying cache supports this and the implementation
  104. /// can react on evictions and expiration of cache items.
  105. /// </summary>
  106. /// <param name="key">The cache key.</param>
  107. /// <param name="region">The cache region. Can be null.</param>
  108. /// <param name="reason">The reason.</param>
  109. /// <param name="value">The original cache value. The value might be null if the underlying cache system doesn't support returning the value on eviction.</param>
  110. /// <exception cref="ArgumentNullException">If <paramref name="key"/> is null.</exception>
  111. protected void TriggerCacheSpecificRemove(string key, string region, CacheEntryRemovedReason reason, object value)
  112. {
  113. SysLogHelper.Debug("TriggerCacheSpecificRemove", $"triggered remove '{region}:{key}' because '{reason}'");
  114. }
  115. /// <summary>
  116. /// Subscribes the keyspace notifications.
  117. /// </summary>
  118. private void SubscribeKeyspaceNotifications()
  119. {
  120. try
  121. {
  122. var subscriber = redisConn.GetSubscriber();
  123. subscriber.Subscribe(
  124. $"__keyevent@{Db}__:expired",
  125. (channel, key) =>
  126. {
  127. var tupple = ParseKey(key);
  128. SysLogHelper.Debug("SubscribeKeyspaceNotifications", $"Got expired event for key '{tupple.Item2}:{tupple.Item1}'");
  129. // we cannot return the original value here because we don't have it
  130. TriggerCacheSpecificRemove(tupple.Item1, tupple.Item2, CacheEntryRemovedReason.Expired, null);
  131. });
  132. subscriber.Subscribe(
  133. $"__keyevent@{Db}__:evicted",
  134. (channel, key) =>
  135. {
  136. var tupple = ParseKey(key);
  137. SysLogHelper.Debug("SubscribeKeyspaceNotifications", $"Got evicted event for key '{tupple.Item2}:{ tupple.Item1}'");
  138. // we cannot return the original value here because we don't have it
  139. TriggerCacheSpecificRemove(tupple.Item1, tupple.Item2, CacheEntryRemovedReason.Evicted, null);
  140. });
  141. subscriber.Subscribe(
  142. $"__keyevent@{Db}__:del",
  143. (channel, key) =>
  144. {
  145. var tupple = ParseKey(key);
  146. SysLogHelper.Debug("SubscribeKeyspaceNotifications", $"Got del event for key '{tupple.Item2}:{tupple.Item1}'");
  147. // we cannot return the original value here because we don't have it
  148. TriggerCacheSpecificRemove(tupple.Item1, tupple.Item2, CacheEntryRemovedReason.Removed, null);
  149. });
  150. }
  151. catch (Exception ex)
  152. {
  153. SysLogHelper.Error(ex);
  154. }
  155. }

image.png
image.png

原文链接

http://redisdoc.com/topic/notification.html http://redis.io/topics/notifications http://redisguide.com/