redis-键空间通知

需求:redis中缓存了一些状态量,业务需要时刻关注状态量变化

方案一:轮询检查(各方面性能太差)

方案二:redis提供的键空间通知机制(redis主动推送,优选)

1、发布与订阅

SUBSCRIBE /UNSUBSCRIBE/PUBLISH 三个命令实现了发布与订阅信息泛型(Publish/Subscribe messaging paradigm),在这个实现中,发送者(发送信息的客户端)不是将信息直接发送给特定的接收者(接收信息的客户端),而是将信息发送给频道(channel),然后由频道将信息转发给所有对这个频道感兴趣的订阅者。

发送者无须知道任何关于订阅者的信息,而订阅者也无须知道是那个客户端给它发送信息,它只要关注自己感兴趣的频道即可。

对发布者和订阅者进行解构,可以极大地提高系统的扩展性,并得到一个更动态的网络拓扑。

比如说,要订阅频道foobar,客户端可以使用频道名字作为参数来调用 SUBSCRIBE 命令:

1
SUBSCRIBE foo bar

当有客户端发送信息到这些频道时,Redis 会将传入的信息推送到所有订阅这些频道的客户端里面。

正在订阅频道的客户端不应该发送除 SUBSCRIBEUNSUBSCRIBE 之外的其他命令。 其中,SUBSCRIBE 可以用于订阅更多频道,而 UNSUBSCRIBE 则可以用于退订已订阅的一个或多个频道。

SUBSCRIBEUNSUBSCRIBE的执行结果会以信息的形式返回,客户端可以通过分析所接收信息的第一个元素,从而判断所收到的内容是一条真正的信息,还是 SUBSCRIBEUNSUBSCRIBE 命令的操作结果。

1.1 信息格式

频道转发的每条信息都是一条带有三个元素的多条批量回复。

  • 第一个元素标识了信息的类型,有以下三种类型:

    • subscribe: 表示当前客户端成功地订阅了信息第二个元素所指示的频道,而此时信息的第三个元素则记录了目前客户端已订阅频道的总数。
    • unsubscribe: 表示当前客户端成功地退订了信息第二个元素所指示的频道,而此时信息的第三个元素记录了客户端目前仍在订阅的频道数量。 当客户端订阅的频道数量降为0时,客户端不再订阅任何频道,它可以像往常一样,执行任何 Redis 命令。
    • message: 表示这条信息是由某个客户端执行 PUBLISH 命令所发送的,真正的信息。
  • 第二个元素是信息来源的频道。

  • 第三个元素则是信息的内容。

1.2 订阅模式

Redis 的发布与订阅实现支持模式匹配: 客户端可以订阅一个带*号的模式,如果某些频道的名字和这个模式匹配,那么当有信息发送给这个/这些频道的时候,客户端也会收到这个/这些频道的信息。

比如说,执行命令

1
PSUBSCRIBE news.*

的客户端将收到来自news.art.figurativenews.music.jazz等频道的信息。

客户端订阅的模式里面可以包含多个 glob 风格的通配符,比如*?[...],等等。

执行命令

1
PUNSUBSCRIBE news.*

将退订news.*模式,其他已订阅的模式不会被影响。

通过订阅模式接收到的信息,和通过订阅频道接收到的信息,两者的格式不太一样:

通过订阅模式而接收到的信息的类型为pmessage: 这代表有某个客户端通过 PUBLISH 向某个频道发送了信息,而这个频道刚好匹配了当前客户端所订阅的某个模式。 信息的第二个元素记录了被匹配的模式,第三个元素记录了被匹配的频道的名字,最后一个元素则记录了信息的实际内容。

客户端处理 PSUBSCRIBEPUNSUBSCRIBE 返回值的方式,和客户端处理 SUBSCRIBEUNSUBSCRIBE 的方式类似: 通过对信息的第一个元素进行分析,客户端可以判断接收到的信息是一个真正的信息,还是 PSUBSCRIBEPUNSUBSCRIBE 命令的返回值。

2、发布什么

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

以下是一些键空间通知发送的事件的例子:

  • 所有修改键的命令。
  • 所有接收到 LPUSH 命令的键。
  • 0号数据库中所有已过期的键。

事件通过 Redis 的订阅与发布功能(pub/sub)来进行分发,因此所有支持订阅与发布功能的客户端都可以在无须做任何修改的情况下,直接使用键空间通知功能。

因为 Redis 目前的订阅与发布功能采取的是发送即忘策略,所以如果你的程序需要可靠事件通知,那么目前的键空间通知可能并不适合你: 当订阅事件的客户端断线时,它会丢失所有在断线期间分发给它的事件。

未来将会支持更可靠的事件分发,这种支持可能会通过让订阅与发布功能本身变得更可靠来实现,也可能会在 Lua 脚本中对消息的订阅与发布进行监听,从而实现类似将事件推入到列表这样的操作。

2.1 通知类型

对于每个修改数据库的操作,键空间通知都会发送两种不同类型的事件。

比如说,对0号数据库的键mykey执行 DEL 命令时,系统将分发两条消息,相当于执行以下两个 PUBLISH 命令:

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

订阅第一个频道__keyspace@0__:mykey可以接收0号数据库中所有修改键mykey的事件,而订阅第二个频道__keyevent@0__:del则可以接收0号数据库中所有执行del命令的键。

keyspace为前缀的频道被称为键空间通知,而以keyevent为前缀的频道则被称为键事件通知

del mykey命令执行时:

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

2.2 配置

因为开启键空间通知功能需要消耗一些 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"表示发送所有类型的通知。

2.3 过期通知的发送时间

我们已经了解了redis的键过期机制为 定期删除 + 惰性删除:

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

当过期键被以上两个程序的任意一个发现、 并且将键从数据库中删除时,Redis 会产生一个expired通知。

Redis 并不保证生存时间(TTL)变为0的键会立即被删除: 如果程序没有访问这个过期键,或者带有生存时间的键非常多的话,那么在键的生存时间变为0,直到键真正被删除这中间,可能会有一段比较显著的时间间隔。

因此,Redis 产生expired通知的时间为过期键被删除的时候,而不是键的生存时间变为0的时候。命令产生的通知

附录:

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

  • DEL 命令为每个被删除的键产生一个del通知。
  • RENAME 产生两个通知:为来源键(source key)产生一个rename_from通知,并为目标键(destination key)产生一个rename_to通知。
  • EXPIREEXPIREAT 在键被正确设置过期时间时产生一个expire通知。当 EXPIREAT 设置的时间已经过期,或者 EXPIRE 传入的时间为负数值时,键被删除,并产生一个del通知。
  • 每当一个键因为过期而被删除时,产生一个expired通知。
  • SORT 在命令带有STORE参数时产生一个sortstore事件。如果STORE指示的用于保存排序结果的键已经存在,那么程序还会发送一个del事件。
  • SET 以及它的所有变种(SETEXSETNXGETSET)都产生set通知。其中 SETEX 还会产生expire通知。
  • MSET 为每个键产生一个set通知。
  • SETRANGE 产生一个setrange通知。
  • INCRDECRINCRBYDECRBY 都产生incrby通知。
  • INCRBYFLOAT 产生incrbyfloat通知。
  • APPEND 产生append通知。
  • LPUSHLPUSHX 都产生单个lpush通知,即使有多个输入元素时,也是如此。
  • RPUSHRPUSHX 都产生单个rpush通知,即使有多个输入元素时,也是如此。
  • RPOP 产生rpop通知。如果被弹出的元素是列表的最后一个元素,那么还会产生一个del通知。
  • LPOP 产生lpop通知。如果被弹出的元素是列表的最后一个元素,那么还会产生一个del通知。
  • LINSERT 产生一个linsert通知。
  • LSET 产生一个lset通知。
  • LTRIM 产生一个ltrim通知。如果 LTRIM 执行之后,列表键被清空,那么还会产生一个del通知。
  • RPOPLPUSHBRPOPLPUSH 产生一个rpop通知,以及一个lpush通知。两个命令都会保证rpop的通知在lpush的通知之前分发。如果从键弹出元素之后,被弹出的列表键被清空,那么还会产生一个del通知。
  • HSETHSETNXHMSET 都只产生一个hset通知。
  • HINCRBY 产生一个hincrby通知。
  • HINCRBYFLOAT 产生一个hincrbyfloat通知。
  • HDEL 产生一个hdel通知。如果执行 HDEL 之后,哈希键被清空,那么还会产生一个del通知。
  • SADD 产生一个sadd通知,即使有多个输入元素时,也是如此。
  • SREM 产生一个srem通知,如果执行 SREM 之后,集合键被清空,那么还会产生一个del通知。
  • SMOVE 为来源键(source key)产生一个srem通知,并为目标键(destination key)产生一个sadd事件。
  • SPOP 产生一个spop事件。如果执行 SPOP 之后,集合键被清空,那么还会产生一个del通知。
  • SINTERSTORESUNIONSTORESDIFFSTORE 分别产生sinterstoresunionostoresdiffstore三种通知。如果用于保存结果的键已经存在,那么还会产生一个del通知。
  • ZINCRBY 产生一个zincr通知。(译注:非对称,请注意。)
  • ZADD 产生一个zadd通知,即使有多个输入元素时,也是如此。
  • ZREM 产生一个zrem通知,即使有多个输入元素时,也是如此。如果执行 ZREM 之后,有序集合键被清空,那么还会产生一个del通知。
  • ZREMRANGEBYSCORE 产生一个zrembyscore通知。(译注:非对称,请注意。)如果用于保存结果的键已经存在,那么还会产生一个del通知。
  • ZREMRANGEBYRANK 产生一个zrembyrank通知。(译注:非对称,请注意。)如果用于保存结果的键已经存在,那么还会产生一个del通知。
  • ZINTERSTOREZUNIONSTORE 分别产生zinterstorezunionstore两种通知。如果用于保存结果的键已经存在,那么还会产生一个del通知。
  • 每当一个键因为maxmemory政策而被删除以回收内存时,产生一个evicted通知。

所有命令都只在键真的被改动了之后,才会产生通知。

比如说,当 SREM 试图删除不存在于集合的元素时,删除操作会执行失败,因为没有真正的改动键,所以这一操作不会发送通知。

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

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

然后,只要在其他终端里用 Redis 客户端发送命令,就可以看到产生的通知了:

1
2
3
"pmessage","__key*__:*","__keyspace@0__:foo","set"
"pmessage","__key*__:*","__keyevent@0__:set","foo"
...