bigywater.tech
Redis · 51

Keyspace Notification

Keyspace Notification 让你订阅「某个 key 被改了」「某个 key 过期了」这类事件。最常见的用法是用 expired 事件做「到期触发」,比如订单超时关闭。但 expired 事件到底有多准时?这篇量了一个多数人没量过的场景:数据库里有大量 TTL 很长的 key 时,短 TTL key 的过期事件要等多久。

默认关闭
notify-keyspace-events 默认是空字符串
5.3 ms
空闲实例上 expired 事件比 TTL 晚的中位数(最大 156 ms)
1/30
池子里有 30 万个长 TTL key 时,30 个 PX 300 的 key 在 20 秒内收到 expired 事件的个数(1000 个时是 22/30)

默认关闭,KEA 打开之后

实测 · 本机执行输出
> CONFIG GET notify-keyspace-events
1) "notify-keyspace-events"
2) ""
订阅 __key*__:* 后 SET k v,收到: []
实测 · 本机执行输出
> CONFIG SET notify-keyspace-events KEA
"OK"
> SET k v
    __keyspace@0__:k → set
    __keyevent@0__:set → k
> LPUSH l a
    __keyspace@0__:l → lpush
    __keyevent@0__:lpush → l
> EXPIRE k 100
    __keyspace@0__:k → expire
    __keyevent@0__:expire → k
> RENAME k k2
    __keyspace@0__:k → rename_from
    __keyevent@0__:rename_from → k
    __keyspace@0__:k2 → rename_to
    __keyevent@0__:rename_to → k2
> DEL k2
    __keyspace@0__:k2 → del
    __keyevent@0__:del → k2

K 是 keyspace 频道(__keyspace@0__:<key>,内容是事件名),E 是 keyevent 频道(__keyevent@0__:<事件>,内容是 key 名),A 表示所有事件类型。所以一条命令会推两条消息。RENAME 会产生 rename_from 和 rename_to 两个事件。

expired 事件:空闲实例上的延迟

实测 · 本机执行输出
> CONFIG SET notify-keyspace-events Ex
"OK"
延迟(事件到达时间 − 300 ms):最小 2.9  中位 5.3  最大 155.8 ms

每次 SET key v PX 300,然后等 expired 事件,量它比 300 ms 晚多少。30 次:最小 2.9 ms、中位 5.3 ms、最大 156 ms。空闲时事件几乎准点,但最大值说明它不是一个有保证的上限。

大量长 TTL key 时:事件可能很久不来

每组同时设 30 个 PX 300 的 key,最多等 20 秒
实测 · 本机执行输出
 长 TTL key 数    20 秒内收到    最早(ms)    中位(ms)    最晚(ms)
           0       30/30         4         4         4
        1000       22/30       199      8761     18239
       10000       13/30      1367     11964     17975
      100000        3/30      1430      7950     15390
      300000        1/30      6930      6930      6930

先往库里灌不同数量、TTL 为 1 小时的 key,再同时设 30 个 300 ms 的 key,看 20 秒内能收到几个 expired 事件。库里没有长 TTL key 时 30/30 都在 4 ms 到;1000 个长 TTL key 时只有 22/30,中位延迟约 8.8 秒;10 万个以上时,20 秒内一个到不了或只到寥寥几个。我把这个实验跑了三次:三次里 1000 个长 TTL key 都是 22–24/30,10000 个是 12–15/30,10 万个是 0–3/30,30 万个是 0–1/30——具体个数每次不同,但趋势一致。上面打印的是最后一次。

这意味着:expired 事件不能当作准点的定时器。库里有大量长 TTL key 时,一个刚过期的 key 可能很久之后才被后台清理发现。为什么?我推测是后台过期清理采用随机抽样(每次只看一小批带 TTL 的 key),池子越大越难抽中那几个过期的——这是我对这个现象的解释,没有读源码验证。

惰性删除:访问会立刻触发

实测 · 本机执行输出
过期 0.4 秒后,GET lazy → None;expired 事件在 GET 之后 0.0 ms 内到达
另一个 key:PX 100,不去访问,(池子里有 300002 个 key,hz=1)等 8 秒:8 秒内没有收到 expired 事件

把 hz 调到 1 让后台清理变慢。一个已过期的 key,被 GET 访问时,返回 nil、同时 expired 事件在 0.05 ms 内到达;而另一个同样过期、但没人访问的 key,在 30 万 key 的库里 8 秒内一直没有事件。所以「读一下」比「等通知」更可靠——但你得知道要读哪个 key。

发后即忘,以及开销

实测 · 本机执行输出
重新订阅后收到: 什么都没有

Keyspace Notification 基于 Pub/Sub(见第 49 篇),订阅者不在时事件就丢了,没有补发。

实测 · 本机执行输出
关闭通知:735k/s   KEA 但无订阅者:606k/s   KEA 且有 1 个订阅 set 事件的订阅者:554k/s

开启 KEA 之后管道写入吞吐从 735k/s 降到 606k/s(无订阅者),有一个订阅者时 554k/s。只开需要的事件类型(比如只开 Ex)可以少产生通知——这一点我没有另外测量。

分步演示:通知与过期

Keyspace Notification
本步的数据

    本文用到的工具与边界

    • 本机 macOS 27 / Redis 8.10.2 独立实例(本机回环端口,save 关闭),redis-cli 风格输出由自写的 RESP 客户端打印;计时用 time.perf_counter,数字每次运行会有小幅抖动。第 4 节的 expired 事件实验用一个订阅者连接接收 __keyevent@0__:expired;每组 30 个 key;20 秒的观察窗口是固定值
    • 没有验证:hz 与 active-expire-effort 调大后事件延迟能否改善、只开 Ex 时的开销、集群下的事件、Redis 8 的哈希字段过期事件(hexpired)
    京ICP备2026067502号-1京公网安备11010502063487号