Redis Keyspace Notifications 机制详解

1,251 阅读4分钟

监控 key 变化:Keyspace Notifications

Keyspace Notifications,可以用于监控 Redis 内的 Key 和 Value的变化,包括 Key 过期事件。像监听过期 Key 的功能就是通过 Keyspace Notifications 实现的。

基本原理是:Pub/Sub。客户端通过订阅 Pub/Sub 频道,来感知事件的发生。

开启键空间通知功能

Keyspace Notifications 功能默认是关闭的,需要手动开启

# config set 或者 redis.conf 配置
notify-keyspace-events [参数](KEA)
# 禁用该功能 参数设置为空即可

参数详细选项

至少需要有 K 或者 E 中的一个

参数意义
K__keyspace@<db>__ 为前缀的 Keyspace events
E__keyevent@<db>__ 为前缀的 Keyevent events
m访问了不存在的 key
n产生了新 key
AA是特殊的,代表下面所有的参数的总和,是"g$lshztxed"的别名(除去mnKE的全部)
xkey 过期事件
eRedis内存满了,被内存淘汰的事件
g通用命令
$String commands
sSet commands
hHash commands
zSorted set commands
tStream commands
dModule key type events

Keyspace Notifications 快速入门

首先开启该功能,然后客户端只需要监听对应的频道即可。

频道分为两类:__keyspace@<db>__:xxx__keyevent@<db>__:xxx

详细可以参考下面的例子:

一个客户端执行:

image-20231225231656041.png

另一个客户端执行:

image-20231225231752867.png

第一个客户端收到消息如下:

image-20231225231813285.png

可以看到,setex 命令分为 set expire 两条命令,并且在10s以后产生了 expired 事件代表 key 过期

对于每个事件的含义如下:

1) 消息类型 pmessage 为模式匹配的消息
2) 匹配的模式
3) 事件名称
4) 命令 或者 key

客户端订阅频道:两种通知事件

大多数情况下,一条命令会生成两条通知事件,两种事件的类型不同。

键空间通知事件分为两类,K 和 E,下面说一下区别

1、Key-space notification:关注 key

Key-space notification 更关注 key,你可以通过如下命令来关注指定的 key

psubscribe '__keyspace@*__:[key name]'

下面的例子中,只有 Key 为 test 的事件被监听。

image-20231225233008020.png

2、Key-event notification:关注事件

Key-event notification 更关注事件或者某个具体的命令

psubscribe '__keyevent@*__:[event]'
# 关注过期事件发生
psubscribe '__keyevent@*__:expired'

下面的例子中,两个key的过期事件均被监听到,没有set事件。

image-20231225233417497.png

命令对应的事件

举个例子,过期事件对应为"expired",RENAME 命令对应有 rename_from 事件和 rename_to 事件

非常多,在此不一一列举,详细见官网:Events generated by different commands

Keyspace Notifications 源码分析

我们还是以过期事件为例,下面是懒删除找不到 key 时的处理:

    robj *lookupKey(redisDb *db, robj *key, int flags) {
        dictEntry *de = dictFind(db->dict,key->ptr);
        robj *val = NULL;
        if (de) {
            val = dictGetVal(de);
            // ...
            // 发现过期了
            if (expireIfNeeded(db, key, expire_flags)) {
                val = NULL;
            }
        }
        if (val) {
            // ... 
        } else {
            // notifyKeyspaceEvent 发出通知 对不存在的 key 进行了操作
            if (!(flags & (LOOKUP_NONOTIFY | LOOKUP_WRITE)))
                notifyKeyspaceEvent(NOTIFY_KEY_MISS, "keymiss", key, db->id);
        }
        return val;
    }

expireIfNeeded 方法中,调用如下方法:

    void deleteExpiredKeyAndPropagate(redisDb *db, robj *keyobj) {
        // 先删除 key
        mstime_t expire_latency;
        latencyStartMonitor(expire_latency);
        if (server.lazyfree_lazy_expire)
            dbAsyncDelete(db,keyobj);
        else
            dbSyncDelete(db,keyobj);
        latencyEndMonitor(expire_latency);
        latencyAddSampleIfNeeded("expire-del",expire_latency);
        // notifyKeyspaceEvent 发出通知 发生了 key 过期事件
        notifyKeyspaceEvent(NOTIFY_EXPIRED,"expired",keyobj,db->id);
        signalModifiedKey(NULL, db, keyobj);
        propagateDeletion(db,keyobj,server.lazyfree_lazy_expire);
        server.stat_expiredkeys++;
    }

所以不管是什么类型的事件,都会通过 notifyKeyspaceEvent 进行消息的发布,第一个参数 type 就代表了事件的类型。 notifyKeyspaceEvent 就是最核心的部分,源码如下:

    void notifyKeyspaceEvent(int type, char *event, robj *key, int dbid) {
        // sds 做 key 的拼接
        sds chan;
        robj *chanobj, *eventobj;
        int len = -1;
        char buf[24];
    ​
        eventobj = createStringObject(event,strlen(event));
    ​
        /* __keyspace@<db>__:<key> <event> notifications. */
        if (server.notify_keyspace_events & NOTIFY_KEYSPACE) {
            // 首先利用 sds 拼接 key 
            chan = sdsnewlen("__keyspace@",11);
            len = ll2string(buf,sizeof(buf),dbid);
            chan = sdscatlen(chan, buf, len);
            chan = sdscatlen(chan, "__:", 3);
            chan = sdscatsds(chan, key->ptr);
            chanobj = createObject(OBJ_STRING, chan);
            // pubsub 发布消息
            pubsubPublishMessage(chanobj, eventobj, 0);
            decrRefCount(chanobj);
        }
    ​
        /* __keyevent@<db>__:<event> <key> notifications. */
        if (server.notify_keyspace_events & NOTIFY_KEYEVENT) {
            chan = sdsnewlen("__keyevent@",11);
            if (len == -1) len = ll2string(buf,sizeof(buf),dbid);
            chan = sdscatlen(chan, buf, len);
            chan = sdscatlen(chan, "__:", 3);
            chan = sdscatsds(chan, eventobj->ptr);
            chanobj = createObject(OBJ_STRING, chan);
            pubsubPublishMessage(chanobj, key, 0);
            decrRefCount(chanobj);
        }
        decrRefCount(eventobj);
    }

代码逻辑很清晰,就是拼接 key 然后发布到对应的频道上。 pubsubPublishMessage 方法,就相当于执行了 publish 命令,在上文 Pub/Sub 源码解析 已经介绍过

Keyspace Notifications 的缺点

既然是基于 Pub/Sub 实现的,那 Pub/Sub 的所有缺点 Keyspace Notifications 也一并继承了。

1、不可靠性

pubsub的消息传递模型是至多一次,不会对消息做持久化,因此出现连接断开时,消息会丢失

2、集群模式的问题

在集群模式下,Keyspace Notifications 就与 Pub/Sub 不太相同了。

在 Redis 7.0 前,集群模式下的全局 Pub/Sub,会导致广播风暴问题;

Redis 7.0 推出了 Shared Pub/Sub 来解决这个问题。

而在集群模式下,Keyspace Notifications 不会进行广播,每个节点只会生成自己 Key 子集的事件。

因此,为了收到所有的事件,客户端需要订阅集群的所有节点。

3、过期 Key 事件的延迟

过期 Key 事件的通知,并非 TTL 到 0,而是当 Redis 发现过期并真正删除时才会通知,发现 Key 过期的时间取决于过期策略。因此,在 Key 的数量非常多时,过期 Key 事件的通知可能会有分钟级的延迟。