解决Redis Cluster模式下键空间通知Keyspace notification失效的问题(python实现)

背景

在做一个支付订单的CASE,需要对订单进行限定时间内支付,到期未完成支付则该订单失效,商品退库处理。

方案

这种案例很适合使用redis的keyspace notification键空间通知功能

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

可以通过对redis的redis.conf文件中配置notify-keyspace-events参数可以指定服务器发送哪种类型的通知。下面对于一些参数的描述。默认情况下此功能是关闭的。

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

所以当你配置文件中配置为AKE时就表示发送所有类型的通知。

也可以直接通过命令行设置:

redis-cli config set notify-keyspace-events KEA

实现1(redis+python)

先来看redis普通模式(单机/主从),引用网络上的一段实例代码1

# subcribe.py
from redis import StrictRedis

# 连接redis数据库
redis = StrictRedis(host='localhost', port=6379, decode_responses=True)

# 创建pubsub对象,该对象订阅一个频道并侦听新消息:
pubsub = redis.pubsub()

# 定义触发事件
def event_handler(msg):
    print('Handler', msg)
    print(msg['data'])
    order_id = str(msg['data'])

    # 获取订单对象
    order = OrderInfo.objects.get(order_id=order_id)

    # 判断用户是否已经付款
    if str(order.status) == "1":

        # 取消订单,更改订单状态
        OrderInfo.objects.filter(order_id=order_id).update(status="6")

        # 获取订单中的所有商品
        goods = order.skus.all()
		
		# 遍历商品
        for good in goods:
            
            # 获取订单中的商品数量
            count = good.count
            print(count)

            # 获取sku商品
            sku = good.sku

            # 将库存重新增加到sku的stock中去
            sku.stock += count
            
            # 从销量中减去已经取消的数量
            sku.sales -= count
            sku.save()
            
#订阅redis键空间通知
pubsub.psubscribe(**{'__keyevent@0__:expired': event_handler})

# 死循环,不停的接收订阅的通知
while True:
    message = pubsub.get_message()
    if message:
        print(message)
    else:
        time.sleep(0.01)

这段代码用于订阅key过期事件,当redis中有key到期时便会自动触发事件,客户端subcribe.py就可以捕获到这个事件

实现2(redis cluster + python)

自redis 3.0之后已支持redis cluster,生产上我们往往也都往redis cluster模式上转,所以申请到的可能就是集群连接串,即startup nodes,我们的示例代码如下:
我们用到了redis-py-cluster模块,需要安装它

pip install redis-py-cluster

和普通模式唯一的区别就是连接方式不一样,其他代码一样:

# subcribe-cluster.py
from rediscluster import StrictRedisCluster

# 连接redis数据库
class RedisConfig:
    REDIS_NODES = [{'host': '192.2.211.248', 'port': 7007},
                   {'host': '192.2.217.209', 'port': 7007}, 
                   {'host': '192.2.218.196', 'port': 7007},
                   {'host': '192.2.219.107', 'port': 7007}, 
                   {'host': '192.2.219.125', 'port': 7007},
                   {'host': '192.2.219.126', 'port': 7007},
                   ]
    REDIS_PASSWORD = os.environ.get('REDIS_PASSWORD')
    REDIS_EXPIRE = 120

redis_nodes = RedisConfig.REDIS_NODES
redis = StrictRedisCluster(startup_nodes=redis_nodes, password=RedisConfig.REDIS_PASSWORD, decode_responses=True)

# 创建pubsub对象,该对象订阅一个频道并侦听新消息:
pubsub = redis.pubsub()

问题及分析

但是在集群模式下你会发现一个问题,就是客户端可能接收不到所有的key过期事件,这就很尴尬了。网上搜了一把,发现问题,
根据github issue #53102里面的描述:

Note: keyspace notifications are node-specific, unlike regular pub/sub which is broadcast from all nodes. So: you (or the library you are using) would need to attach to all nodes to reliably get keyspace notifications from cluster.

也就是说键空间通知是指定node的(自身),不像常规的pub/sub是广播到所有节点的,所以我们需要连接集群中所有的可用节点去获取键空间通知

as keyspace notifications are not broadcasted in the cluster you’ll have to:
1.Open a connection to each of the cluster’s nodes
2.In each connection, subscribe to keyspace notifications from that node

换句话说,Redis Cluster集群中key采用的是分片存储,不同的key通过哈希计算放到不同的slot槽中,即可能是不同的node节点,而keyspace notification只在自己所在的node上发布,并没有发布到集群当中,我们redis-py-cluster客户端订阅监听的时候只监听随机的node(即每次建立连接的node是随机的),那么就有可能有些key过期没有被监听到,这就导致说我们收不到这个过期事件。

Where Pub/Sub messages are generally sent across the cluster, the keyspace notifications are only sent locally. Broadcasting such events cluster-wide could become very bandwidth intensive. However, to simulate cluster-wide behaviour, clients can subscribe to all the master nodes and merge the received events. In this approach the clients should check the cluster configuration from time to time to make sure to connect to other masters added in possible reconfiguration.

即集群本身的pub/sub是节点之间交叉广播的,但是键空间通知只支持本地

继续翻,发现了antirez大神(Redis的作者)亲口的答复3
在这里插入图片描述

Hello. I’m not sure we are able to provide non-local events in Redis Cluster. This would require to broadcast the events cluster-wide, which is quite bandwidth intensive… Isn’t it better that the client just subscribes to all the master nodes instead, merging all the received events? However one problem with this is that from time to time it should check the cluster configuration to make sure to connect to other masters added during the live of the cluster.

也就是说因集群模式点对点之间网络带宽的压力,不考虑将键空间通知加入到集群广播中来,更建议是客户端直接连接节点获取键空间通知,但是有个问题就是需要客户端随时检查集群配置,以获取新加入的master节点

在这里插入图片描述
图片来源网络,引用见文末4

解决办法

上面问题分析中已经给出了答案了,那就是主动去各个节点上获取键空间通知,这里再直接引用原jedis实现的文章的描述:
既然我们知道了在集群条件下,每次监听只会随机取一个端口进行监听。那么我们就自己写监听机制,监听集群条件下的所有主机的端口就行了。
思路如下:

  1. 程序启动时,获得集群的配置信息
  2. 根据集群配置的Master数配置相同的RedisMessageListenerContainer进行监听

原文是用jedis实现的,我们这里改用python实现如下,同时我们不用while循环,改用多线程5的方式实现:

# subscribe-cluster.py
# -*- coding:utf-8 -*-
"""
REDIS subcripe
"""
import time
from rediscluster import StrictRedisCluster
from redis.client import PubSub


def event_handler(msg):
    data_list = str(msg['data']).split(':')
    print('Handler', msg)
    # TODO your business
    #thread.stop()


if __name__ == "__main__":
    print('start subscribe...')
    redisconn = StrictRedisCluster(startup_nodes=redis_nodes, password=RedisConfig.REDIS_PASSWORD,decode_responses=True)
    pools = redisconn.get_pool()
    nodes = redisconn.cluster_nodes()
    #nodes_master = []
    #for node in nodes:
    #    if 'master' in list(node['flags']):
    #        nodes_master.append(
    #            {'host': node['host'], 'port': node['port'], 'name': '{0}:{1}'.format(node['host'], node['port'])})
    for node in nodes:
        print('start listening master node:{}'.format(node))
        r = pools.get_connection_by_node(node)
        newPubSub = PubSub(connection_pool=pools)
        newPubSub.connection = r
        newPubSub.subscribe(**{'__keyevent@0__:expired': event_handler})
        thread = newPubSub.run_in_thread(sleep_time=0.01)

其中thread.stop()根据实际情况来决定是否停止线程,如果你的subscribe.py是单独运行的文件进程,则不能停止该线程,特别是单独跑在容器里面的时候,否则处理完一条事件后就停止了,当然你也可以直接使用Linux自身的nohub命令去运行这个python程序即可

这个程序我们我们单独运行即可,相当于我们建立了6个连接来监听所有主从redis服务器发送的消息,大体如下图所示。

但是需要注意的就是程序是单独运行的,所以连接建立后程序的nodes是固定的,只遍历了一次,也就是如果有新的master加入到集群中,是无法监测到的,也就是上文中antirez大神提到的问题,你需要随时检查集群节点以确保新加入的集群能够被监听到,因手上redis集群维护不在我这边,我没法测试,所以暂时没在程序中添加此功能,后续有时间自己搭个集群测试下,大家有兴趣的也可以自行补充测试。
在这里插入图片描述

小贴士:模式能匹配通配符,例如__keyspace@0__:blog*表示只接收blog开头的key值的信息,其他key值信息不接收 *

Redis官网提醒

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

所以单单靠这个来做订单限时支付的提醒并不是很靠谱,可以加入plan B,本身我们采用键空间通知的目的是为了减轻定时扫盘的压力,既然我们有了键空间通知,那么我们的plan B就可以继续扫盘,不过时间上可以不用太紧张,如每分钟扫一次盘。当然大规模、要求高的可能需要另寻方案解决,如采用靠谱的延时队列来实现。

参考文章:


  1. Python利用Redis键空间回调函数,30分钟未支付取消订单,作者:Enzoooooa ↩︎

  2. redis cluster,config set notify-keyspace-events Ex, but can’t Trigger redis expired event #5310 ↩︎

  3. BUG about keyevent notification in cluster mode #2541 ↩︎

  4. 解决Redis集群条件下键空间通知服务器接收不到消息的问题,作者:薄情眉 ↩︎

  5. python中的Redis键空间通知(过期回调),作者:fu-yong ↩︎

发布了25 篇原创文章 · 获赞 24 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/arnolan/article/details/102065525