Redis keyspace 通知

介绍

Redis是内存中的数据结构存储,用于缓存、高速数据摄取、处理消息队列、分布式锁定等等。

与其他内存存储相比,使用Redis的优势在于它提供了持久性和数据结构,比如列表、集合、排序集合和散列。

在这篇文章中,介绍一个Redis keyspace通知的简短概述。并演示如何配置Redis来接收它们。并展示如何在python中订阅Redis通知

在开始之前,请安装并启动Redis服务器,如下所述:https://redis.io/topics/quickstart

启用通知

默认情况下,redis的通知事件是关闭的,在终端执行以下命令开启:

$ redis-cli config set notify-keyspace-events KEA
OK  

KEA字符串表示启用了所有可能的事件。要查看每个字符的含义,请查看文档

CLI 可以在特殊模式下工作,允许您订阅一个通道以接收消息。

现在检查事件是否起作用:

# 用于检查事件
# psubscribe '*'表示我们想订阅所有带有模式*的事件

$ redis-cli --csv psubscribe '*'     
Reading messages... (press Ctrl-C to quit)  
"psubscribe","*",1

开启一个新的终端,设置一个值

127.0.0.1:6379> set key1 value1  
OK  

在上一个终端,会看到:

$ redis-cli --csv psubscribe '*'     
Reading messages... (press Ctrl-C to quit)  
"psubscribe","*",1
"pmessage","*","__keyspace@0__:key1","set"
"pmessage","*","__keyevent@0__:set","key1

发现通知是工作中的

复述

Redis的键盘空间通知从2.8.0版起就可以使用了。对于更改任何Redis键的每个操作,可以配置Redis将消息发布到发布/订阅。然后可以订阅这些通知。值得一提的是,事件仅在确实修改了键的情况下才生成。例如,删除不存在的键将不会生成事件。

以上收到三个事件:

"psubscribe","*",1
"pmessage","*","__keyspace@0__:key1","set"
"pmessage","*","__keyevent@0__:set","key1
  • 第一个事件意味着已经成功订阅了reply中作为第二个元素给出的通道。1 表示目前订阅的频道数量。
  • 第二个事件是键空间通知。在keyspace通道中,接收事件集的名称作为消息。
  • 第三个事件是键-事件通知。在keyevent通道中,接收到key key1的名称作为消息。

Redis Pub/Sub

事件是通过Redis的Pub/Sub层交付的。

为了订阅channel channel1和channel2,客户端发出带有通道名称的subscribe命令

在Python中订阅通知

第一步,需要python操作redis的包

$ pip install redis

事件循环,请看以下代码:

import time  
from redis import StrictRedis

redis = StrictRedis(host='localhost', port=6379)

# redis 发布订阅
pubsub = redis.pubsub()  
# 监听通知
pubsub.psubscribe('__keyspace@0__:*')

# 开始消息循环
print('Starting message loop')  
while True:  
   # 获取消息
    message = pubsub.get_message()
    if message:
        print(message)
    else:
        time.sleep(0.01)

分析:

这是创建 `redis` 连接的代码

redis = StrictRedis(host='localhost', port=6379) 

默认情况下,所有响应都以字节的形式返回。用户负责解码。如果客户机的所有字符串响应都应该被解码,那么用户可以指定decode_responses=True to StrictRedis。在这种情况下,任何返回字符串类型的Redis命令都将用指定的编码进行解码。

接下来,创建一个订阅频道并侦听新消息的pubsub对象:

pubsub = redis.pubsub()  
pubsub.psubscribe('__keyspace@0__:*')  

继而通过一个无限循环来等待事件

while True:  
    message = pubsub.get_message()
...

如果有数据,get_message()将读取并返回它。如果没有数据,该方法将不返回任何数据。

从pubsub实例中读取的每条消息都是一个字典,其中包含以下键:

  • type  以下之一:订阅,取消订阅,psubscribe, punsubscribe, message, pmessage
  • channel  订阅消息的通道或消息发布到的通道
  • pattern  与已发布消息的通道匹配的模式(除pmessage类型外,在所有情况下都不匹配)
  • data  消息数据

现在启动 `python` 脚本,在终端中设置一个key值

127.0.0.1:6379> set mykey myvalue  
OK  

将看到脚本以下输出

$ python subscribe.py
Starting message loop  
{'type': 'psubscribe', 'data': 1, 'channel': b'__keyspace@0__:*', 'pattern': None}
{'type': 'pmessage', 'data': b'set', 'channel': b'__keyspace@0__:mykey', 'pattern': b'__keyspace@0__:*'}

回调注册

注册回调函数来处理已发布的消息。消息处理程序接受一个参数,即消息。要使用消息处理程序订阅通道或模式,请将通道或模式名称作为关键字参数传递,其值为回调函数。当使用消息处理程序在通道或模式上读取消息时,将创建消息字典并将其传递给消息处理程序。在这种情况下,get_message()返回一个None值,因为消息已经被处理

import time  
from redis import StrictRedis

redis = StrictRedis(host='localhost', port=6379)

pubsub = redis.pubsub()

def event_handler(msg):  
    print('Handler', msg)

pubsub.psubscribe(**{'__keyspace@0__:*': event_handler})

print('Starting message loop')  
while True:  
    message = pubsub.get_message()
    if message:
        print(message)
    else:
        time.sleep(0.01)
View Code

事件循环在单独的线程中

选择是在单独的线程中运行事件循环

import time  
from redis import StrictRedis

redis = StrictRedis(host='localhost', port=6379)

def event_handler(msg):  
    print(msg)
    thread.stop()  

pubsub = redis.pubsub()  
pubsub.psubscribe(**{'__keyevent@0__:expired': event_handler})  
thread = pubsub.run_in_thread(sleep_time=0.01)
View Code

总结

Redis的一个常见用例是,应用程序需要能够响应存储在特定键或键中的值可能发生的更改。由于有了键盘空间通知和发布/订阅,可以对Redis数据中的变化做出响应。通知很容易使用,而事件处理器可能在地理位置上分布。

最大的缺点是,Pub/Sub的实现要求发布者和订阅方始终处于启动状态。用户在停止或连接丢失时丢失数据。

参考文献

 

基于Redis的发布订阅模式对键/空间事件进行监听

因为开启键/空间事件通知功能需要消耗redis服务器一定的CPU,所以redis的键/空间事件通知是默认没有开发和进行配置的,要开启这个功能,则可以在配置文件redis.conf中找到配置项notify-keyspace-events进行设置并保存,并重启redis即可。

发布订阅模式,是编程领域里面的传统的设计模式来的。

sudo vim /etc/redis/redis.conf
# 在配置项原来的空字符串中添加"KEA",大概在1905行附近
# notify-keyspace-events ""
notify-keyspace-events "KEA"

# 保存退出:wq
# 重启redis
service redis-server restart

notify-keyspace-events,订阅通知的频道/模式选项表。

所谓的键空间(或空间事件,keyspace)就是redis保存的数据的空间(当数据的值发生了变化,空间肯定也变化了),就是所谓的变量名

所谓的键事件(events)就是redis中提供给开发者用于修改数据的操作,例如:set 设置/更新,del 删除,get 读取数据等等

如下:

参数发送的通知补充
K 键空间通知,以 __keyspace@db__ 为前缀,针对Key的 需要监控指定key的值变化时,可设置keyspace
db表示redis数据库编号,0~15
注意前缀的左右两边是2个英文下划线。
E 键事件通知,以 __keyevent@db__ 为前缀,针对event的 需要监控对任意key进行指定命名时,可设置keyevent
db同上。
g DEL 、 EXPIRE 、 RENAME 等类型无关的通用命令的通知  
$ 字符串命令的通知  
l 列表命令的通知  
s 集合命令的通知  
h 哈希命令的通知  
z 有序集合命令的通知  
x 过期事件:每当有过期键被删除时发送  
e 驱逐(evict)事件:每当有键因为 maxmemory 政策而被删除时发送  
A 除了上面KE2个参数以外,包括g$lshzxe等参数的集合,all的简写。  
订阅通知的基本语法

redis中的订阅通知是通过频道channel或叫模式patterns来进行分发不同区间的。一般语法如下:

# 1. 订阅全频道或全模式的任意通知, 就1个星号
*
# 2. 只订阅键空间和键事件的2种通知,2个星号,使用英文冒号隔开
*:*
# 3. 指定库的键空间事件通知订阅,<db>是一个变量,表示数据库下标,0~15都可以
*@<db>__:*
# 例如:订阅0号库的键空间事件通知,则是
*@0__:*

# 4. 只监听键空间的通知
__keyspace@*:* # 此处表示订阅任意库的任意键的键空间通知
# 例如,订阅2号库下的任意键空间通知,则是
__keyspace@2__:*
# 例如:订阅2号库下的uname键空间通知,则是
__keyspace@2__:uname # 当2号库的uname发生任何变化时,都会得到通知信息

# 5. 只监听键事件通知
__keyevent@*:* # 此处表示订阅任意库的任意键的键事件通知
# 例如:订阅1号库下的任意键事件通知,则是
__keyevent@1__:*
# 例如:
__keyevent@1__:del # 当1号库下有任意键被del了,都会得到通知消息
__keyevent@15__:set # 当15号库下有任意键被设置了,都会得到通知消息
__keyevent@3__:expired # 当3号库下有任意键过期了,都会得到通知消息

了解了订阅通知的语法以后,在编写代码使用发布订阅之前,我们可以先打开2个终端进行再使用一下。

终端1,先订阅0号库中的键空间通知,__keyspace@0__

# 不要进入redis-cli,下面2句命令不能同时操作,必须一个个进行测试使用。
redis-cli --csv psubscribe '__keyevent@0__:set' # 订阅对0号库下set命令操作的键事件通知
# redis-cli --csv psubscribe '__keyspace@2__:uname' # 订阅对2号库下uname操作的键空间通知

终端2,输入命令,让redis发布通知:

# 需要进入redis-cli,输入第1条命令
select 0 # 切换到0号库
set name xioaming
set age 16
"""
可以在终端1查看效果,订阅键事件通知的消息格式如下:
"pmessage","__keyevent@0__:set","__keyevent@0__:set","name"
"pmessage","__keyevent@0__:set","__keyevent@0__:set","age"
"""

# 接下来,终端1可以使用第2条命令
select 2 # 切换到2号库
setex uname 300 xiaohong
"""
可以在终端1查看效果,订阅键空间通知的消息格式如下:
"pmessage","__keyspace@2__:uname","__keyspace@2__:uname","set"
"pmessage","__keyspace@2__:uname","__keyspace@2__:uname","expire"  # 监听是否使用expire命令
"pmessage","__keyspace@2__:uname","__keyspace@2__:uname","expired"  # 监听是否uname变量是否过期了
"""

python中使用键空间事件通知

接下来,我们需要在项目初始化的时候,对redis的事件进行订阅监听,由redis模块是有对键空间事件进行封装的操作对象(pubsub),我们可以使用pubsub对象提供的操作进行键空间事件的订阅,而且pubsub还提供了一个独立线程帮我们单独执行避免出现阻塞。

当前,我们的宠物信息保存在2号库,果树保存在3号库。

utils/redisutils.py,代码:

from flask import Flask
from redis import Redis
from typing import Callable, Dict

class RedisUtil(object):
    """redis的工具类"""
    def __init__(self, app: Flask=None, redis: Redis=None):
        if app is not None:
            self.init_app(app, redis)

    def init_app(self, app: Flask, redis: Redis):
        self.app = app
        self.redis = redis
        # 把redis提供的发布订阅进行初始化,创建发布订阅对象
        self.pubsub = self.redis.pubsub()
        # 注册发布订阅任务
        self.pubsub_init()  # 这里自定义一个订阅通知的方法
        # 让pubsub提供的独立启动
        self.thread = self.pubsub.run_in_thread(sleep_time=1)

    def pubsub_init(self):
        """
        初始化配置订阅发布任务
        """
        # 监听宠物
        self.pubsub_tasks("event", 2, "expired", self.event_expired_response)
        self.pubsub_tasks("event", 2, "del", self.event_del_response)

        # 监听果树
        self.pubsub_tasks("event",3, "expired", self.event_expired_response)
        self.pubsub_tasks("event",3, "del", self.event_del_response)

        # 测试代码
        self.pubsub_tasks("space", 4, "message", self.space_message_response)

    def pubsub_tasks(self, channel: str, db: int, name: str, callback: Callable):
        """
        订阅发布键空间或键事件监听的任务方法
        :param channel: 模式/频道
        :param db: 数据库下标
        :param name: 键名称/命令名称
        :param callback: 订阅回调
        :return:
        """
        self.pubsub.psubscribe(**{f'__key{channel}@{db}__:{name}': callback})

    def event_expired_response(self, message: Dict):
        """
        过期事件的回调处理
        :param message: 发布结果
        {
            'type': 'pmessage',  # 表示接收到的是消息
            'pattern': b'__keyevent@2__:expired', # 模式格式,表示我们的订阅规则
            'channel': b'__keyevent@2__:expired', # 频道名称,表示匹配上的频道全称
            'data': b'数据变量名' # 消息正文
        }
        :return:
        """
        print("过期事件的回调处理", message)
        print(message["data"].decode())

    def event_del_response(self, message: Dict):
        """
        删除事件的回调处理
        """
        print("删除事件的回调处理", message)
        print(message["data"].decode())

    def space_message_response(self, message):
        """
        :param message: 发布结果
        {
            'type': 'pmessage',  # 表示接收到的是消息
            'pattern': b'__keyevent@2__:expired', # 模式格式,表示我们的订阅规则
            'channel': b'__keyevent@2__:expired', # 频道名称,表示匹配上的频道全称
            'data': b'操作命令' # 消息正文
        }
        :return:
        """
        print("空间事件的回调处理", message)
        print(message["data"].decode())

posted @ 2020-07-01 23:54  silencio。  阅读(1253)  评论(0)    收藏  举报