功能概览
键空间通知使得客户端可以通过订阅频道或模式, 来接收那些以某种方式改动了 Redis 数据集的事件。
以下是一些键空间通知发送的事件的例子:
- 所有修改键的命令。
- 所有接收到 LPUSH key value [value …] 命令的键。
0
号数据库中所有已过期的键。
事件通过 Redis 的订阅与发布功能(pub/sub)来进行分发, 因此所有支持订阅与发布功能的客户端都可以在无须做任何修改的情况下, 直接使用键空间通知功能。
因为 Redis 目前的订阅与发布功能采取的是发送即忘(fire and forget)策略, 所以如果你的程序需要可靠事件通知(reliable notification of events), 那么目前的键空间通知可能并不适合你: 当订阅事件的客户端断线时, 它会丢失所有在断线期间分发给它的事件。
未来将会支持更可靠的事件分发, 这种支持可能会通过让订阅与发布功能本身变得更可靠来实现, 也可能会在 Lua 脚本中对消息(message)的订阅与发布进行监听, 从而实现类似将事件推入到列表这样的操作。
事件的类型
对于每个修改数据库的操作,键空间通知都会发送两种不同类型的事件。
比如说,对 0
号数据库的键 mykey
执行 DEL key [key …] 命令时, 系统将分发两条消息, 相当于执行以下两个 PUBLISH channel message 命令:
PUBLISH __keyspace@0__:mykey del
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 |
DEL 、 EXPIRE 、 RENAME 等类型无关的通用命令的通知 |
$ |
字符串命令的通知 |
l |
列表命令的通知 |
s |
集合命令的通知 |
h |
哈希命令的通知 |
z |
有序集合命令的通知 |
x |
过期事件:每当有过期键被删除时发送 |
e |
驱逐(evict)事件:每当有键因为 maxmemory 政策而被删除时发送 |
A |
参数 g$lshzxe 的别名 |
输入的参数中至少要有一个 K
或者 E
, 否则的话, 不管其余的参数是什么, 都不会有任何通知被分发。
举个例子, 如果只想订阅键空间中和列表相关的通知, 那么参数就应该设为 Kl
, 诸如此类。
将参数设为字符串 "AKE"
表示发送所有类型的通知。
命令产生的通知
以下列表记录了不同命令所产生的不同通知:
- DEL key [key …] 命令为每个被删除的键产生一个
del
通知。 - RENAME key newkey 产生两个通知:为来源键(source key)产生一个
rename_from
通知,并为目标键(destination key)产生一个rename_to
通知。 - EXPIRE key seconds 和 EXPIREAT key timestamp 在键被正确设置过期时间时产生一个
expire
通知。当 EXPIREAT key timestamp 设置的时间已经过期,或者 EXPIRE key seconds 传入的时间为负数值时,键被删除,并产生一个del
通知。 - [SORT key [BY pattern] [LIMIT offset count] [GET pattern [GET pattern …]] [ASC | DESC] [ALPHA] STORE destination] 在命令带有
STORE
参数时产生一个sortstore
事件。如果STORE
指示的用于保存排序结果的键已经存在,那么程序还会发送一个del
事件。 - SET key value [EX seconds] [PX milliseconds] [NX|XX] 以及它的所有变种(SETEX key seconds value 、 SETNX key value 和 GETSET key value)都产生
set
通知。其中 SETEX key seconds value 还会产生expire
通知。 - MSET key value [key value …] 为每个键产生一个
set
通知。 - SETRANGE key offset value 产生一个
setrange
通知。 - INCR key 、 DECR key 、 INCRBY key increment 和 DECRBY key decrement 都产生
incrby
通知。 - INCRBYFLOAT key increment 产生
incrbyfloat
通知。 - APPEND key value 产生
append
通知。 - LPUSH key value [value …] 和 LPUSHX key value 都产生单个
lpush
通知,即使有多个输入元素时,也是如此。 - RPUSH key value [value …] 和 RPUSHX key value 都产生单个
rpush
通知,即使有多个输入元素时,也是如此。 - RPOP key 产生
rpop
通知。如果被弹出的元素是列表的最后一个元素,那么还会产生一个del
通知。 - LPOP key 产生
lpop
通知。如果被弹出的元素是列表的最后一个元素,那么还会产生一个del
通知。 - LINSERT key BEFORE|AFTER pivot value 产生一个
linsert
通知。 - LSET key index value 产生一个
lset
通知。 - LTRIM key start stop 产生一个
ltrim
通知。如果 LTRIM key start stop 执行之后,列表键被清空,那么还会产生一个del
通知。 - RPOPLPUSH source destination 和 BRPOPLPUSH source destination timeout 产生一个
rpop
通知,以及一个lpush
通知。两个命令都会保证rpop
的通知在lpush
的通知之前分发。如果从键弹出元素之后,被弹出的列表键被清空,那么还会产生一个del
通知。 - HSET hash field value 、 HSETNX hash field value 和 HMSET 都只产生一个
hset
通知。 - HINCRBY 产生一个
hincrby
通知。 - HINCRBYFLOAT 产生一个
hincrbyfloat
通知。 - HDEL 产生一个
hdel
通知。如果执行 HDEL 之后,哈希键被清空,那么还会产生一个del
通知。 - SADD key member [member …] 产生一个
sadd
通知,即使有多个输入元素时,也是如此。 - SREM key member [member …] 产生一个
srem
通知,如果执行 SREM key member [member …] 之后,集合键被清空,那么还会产生一个del
通知。 - SMOVE source destination member 为来源键(source key)产生一个
srem
通知,并为目标键(destination key)产生一个sadd
事件。 - SPOP key 产生一个
spop
事件。如果执行 SPOP key 之后,集合键被清空,那么还会产生一个del
通知。 - SINTERSTORE destination key [key …] 、 SUNIONSTORE destination key [key …] 和 SDIFFSTORE destination key [key …] 分别产生
sinterstore
、sunionostore
和sdiffstore
三种通知。如果用于保存结果的键已经存在,那么还会产生一个del
通知。 - ZINCRBY key increment member 产生一个
zincr
通知。(译注:非对称,请注意。) - ZADD key score member [[score member] [score member] …] 产生一个
zadd
通知,即使有多个输入元素时,也是如此。 - ZREM key member [member …] 产生一个
zrem
通知,即使有多个输入元素时,也是如此。如果执行 ZREM key member [member …] 之后,有序集合键被清空,那么还会产生一个del
通知。 - ZREMRANGEBYSCORE key min max 产生一个
zrembyscore
通知。(译注:非对称,请注意。)如果用于保存结果的键已经存在,那么还会产生一个del
通知。 - ZREMRANGEBYRANK key start stop 产生一个
zrembyrank
通知。(译注:非对称,请注意。)如果用于保存结果的键已经存在,那么还会产生一个del
通知。 - [ZINTERSTORE destination numkeys key [key …] [WEIGHTS weight [weight …]] AGGREGATE SUM|MIN|MAX] 和 [ZUNIONSTORE destination numkeys key [key …] [WEIGHTS weight [weight …]] AGGREGATE SUM|MIN|MAX] 分别产生
zinterstore
和zunionstore
两种通知。如果用于保存结果的键已经存在,那么还会产生一个del
通知。 - 每当一个键因为过期而被删除时,产生一个
expired
通知。 - 每当一个键因为
maxmemory
政策而被删除以回收内存时,产生一个evicted
通知。Note 所有命令都只在键真的被改动了之后,才会产生通知。 比如说,当 SREM key member [member …] 试图删除不存在于集合的元素时,删除操作会执行失败,因为没有真正的改动键,所以这一操作不会发送通知。
如果对命令所产生的通知有疑问, 最好还是使用以下命令, 自己来验证一下:
$ redis-cli config set notify-keyspace-events KEA
$ redis-cli --csv psubscribe '__key*__:*'
Reading messages... (press Ctrl-C to quit)
"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 |
DEL 、 EXPIRE 、 RENAME 等类型无关的通用命令的通知 |
$ |
字符串命令的通知 |
l |
列表命令的通知 |
s |
集合命令的通知 |
h |
哈希命令的通知 |
z |
有序集合命令的通知 |
x |
过期事件:每当有过期键被删除时发送 |
e |
驱逐(evict)事件:每当有键因为 maxmemory 政策而被删除时发送 |
A |
参数 g$lshzxe 的别名 |
notify-keyspace-events 设置为”gxeE”
localhost@6379:0>CONFIG SET 'notify-keyspace-events' "gxeE"
localhost@6379:0>CONFIG GET 'notify-keyspace-events'
阿里云Redis配置
C#代码
/// <summary>
/// Gets the servers.
/// </summary>
/// <value>The servers.</value>
public IEnumerable<IServer> Servers
{
get
{
var endpoints = redisConn.GetEndPoints();
foreach (var endpoint in endpoints)
{
var server = redisConn.GetServer(endpoint);
yield return server;
}
}
}
/// <summary>
/// Gets the configuration.
/// </summary>
/// <param name="key">The key.</param>
/// <returns>Dictionary<System.Net.EndPoint, System.String>.</returns>
private Dictionary<System.Net.EndPoint, string> GetConfiguration(string key)
{
var result = new Dictionary<System.Net.EndPoint, string>();
foreach (var server in Servers)
{
var values = server.ConfigGet(key).ToDictionary(k => k.Key, v => v.Value);
if (values.ContainsKey(key))
{
var value = values.FirstOrDefault(p => p.Key == key);
result.Add(server.EndPoint, value.Value);
}
}
return result;
}
/// <summary>
/// Initializes the keyspace notifications.
/// </summary>
private void InitKeyspaceNotifications()
{
if (!KeyspaceNotificationsEnabled)
{
return;
}
// notify-keyspace-events needs to be set to "Exe" at least! Otherwise we will not receive any events.
// this must be configured per server and should probably not be done automagically as this needs admin rights!
// Let's try to check at least if those settings are configured (the check also works only if useAdmin is set to true though).
try
{
var configurations = GetConfiguration("notify-keyspace-events");
foreach (var cfg in configurations)
{
if (!cfg.Value.Contains("E"))
{
SysLogHelper.Error("InitKeyspaceNotifications", $"Server { cfg.Key} is missing configuration value 'E' in notify-keyspace-events to enable keyevents.");
}
if (!(cfg.Value.Contains("A") || (cfg.Value.Contains("x") && cfg.Value.Contains("e"))))
{
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.");
}
}
}
catch (Exception ex)
{
SysLogHelper.Error(ex, strAddition: "Could not read configuration from redis to validate notify-keyspace-events. Most likely useAdmin is not set to true.");
}
SubscribeKeyspaceNotifications();
}
private const string Base64Prefix = "base64\0";
/// <summary>
/// Parses the key.
/// </summary>
/// <param name="value">The value.</param>
/// <returns>Tuple<System.String, System.String>.</returns>
private Tuple<string, string> ParseKey(string value)
{
if (value == null)
{
return Tuple.Create<string, string>(null, null);
}
var sepIndex = value.IndexOf(':');
var hasRegion = sepIndex > 0;
var key = value;
string region = null;
if (hasRegion)
{
region = value.Substring(0, sepIndex);
key = value.Substring(sepIndex + 1);
if (region.StartsWith(Base64Prefix))
{
region = region.Substring(Base64Prefix.Length);
region = Encoding.UTF8.GetString(Convert.FromBase64String(region));
}
}
if (key.StartsWith(Base64Prefix))
{
key = key.Substring(Base64Prefix.Length);
key = Encoding.UTF8.GetString(Convert.FromBase64String(key));
}
return Tuple.Create(key, region);
}
/// <summary>
/// Can be used to signal a remove event to the <see cref="ICacheManager{TCacheValue}"/> in case the underlying cache supports this and the implementation
/// can react on evictions and expiration of cache items.
/// </summary>
/// <param name="key">The cache key.</param>
/// <param name="region">The cache region. Can be null.</param>
/// <param name="reason">The reason.</param>
/// <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>
/// <exception cref="ArgumentNullException">If <paramref name="key"/> is null.</exception>
protected void TriggerCacheSpecificRemove(string key, string region, CacheEntryRemovedReason reason, object value)
{
SysLogHelper.Debug("TriggerCacheSpecificRemove", $"triggered remove '{region}:{key}' because '{reason}'");
}
/// <summary>
/// Subscribes the keyspace notifications.
/// </summary>
private void SubscribeKeyspaceNotifications()
{
try
{
var subscriber = redisConn.GetSubscriber();
subscriber.Subscribe(
$"__keyevent@{Db}__:expired",
(channel, key) =>
{
var tupple = ParseKey(key);
SysLogHelper.Debug("SubscribeKeyspaceNotifications", $"Got expired event for key '{tupple.Item2}:{tupple.Item1}'");
// we cannot return the original value here because we don't have it
TriggerCacheSpecificRemove(tupple.Item1, tupple.Item2, CacheEntryRemovedReason.Expired, null);
});
subscriber.Subscribe(
$"__keyevent@{Db}__:evicted",
(channel, key) =>
{
var tupple = ParseKey(key);
SysLogHelper.Debug("SubscribeKeyspaceNotifications", $"Got evicted event for key '{tupple.Item2}:{ tupple.Item1}'");
// we cannot return the original value here because we don't have it
TriggerCacheSpecificRemove(tupple.Item1, tupple.Item2, CacheEntryRemovedReason.Evicted, null);
});
subscriber.Subscribe(
$"__keyevent@{Db}__:del",
(channel, key) =>
{
var tupple = ParseKey(key);
SysLogHelper.Debug("SubscribeKeyspaceNotifications", $"Got del event for key '{tupple.Item2}:{tupple.Item1}'");
// we cannot return the original value here because we don't have it
TriggerCacheSpecificRemove(tupple.Item1, tupple.Item2, CacheEntryRemovedReason.Removed, null);
});
}
catch (Exception ex)
{
SysLogHelper.Error(ex);
}
}
原文链接
http://redisdoc.com/topic/notification.html http://redis.io/topics/notifications http://redisguide.com/