监控 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 |
| A | A是特殊的,代表下面所有的参数的总和,是"g$lshztxed"的别名(除去mnKE的全部) |
| x | key 过期事件 |
| e | Redis内存满了,被内存淘汰的事件 |
| g | 通用命令 |
| $ | String commands |
| s | Set commands |
| h | Hash commands |
| z | Sorted set commands |
| t | Stream commands |
| d | Module key type events |
Keyspace Notifications 快速入门
首先开启该功能,然后客户端只需要监听对应的频道即可。
频道分为两类:__keyspace@<db>__:xxx 和 __keyevent@<db>__:xxx
详细可以参考下面的例子:
一个客户端执行:
另一个客户端执行:
第一个客户端收到消息如下:
可以看到,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 的事件被监听。
2、Key-event notification:关注事件
Key-event notification 更关注事件或者某个具体的命令
psubscribe '__keyevent@*__:[event]'
# 关注过期事件发生
psubscribe '__keyevent@*__:expired'
下面的例子中,两个key的过期事件均被监听到,没有set事件。
命令对应的事件
举个例子,过期事件对应为"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 事件的通知可能会有分钟级的延迟。