【Redis从入门到精通】第28篇:数据库通知——Redis的事件订阅机制
上一篇【第27篇】过期键的骨牌效应——AOF/RDB/复制中的过期处理
下一篇【第29篇】RDB持久化——Redis的快照是怎么拍的
Redis里的键生老病死,你能不能第一时间知道?键空间通知就是Redis的"事件播报员"——但它可不像新闻联播那样准时。
引言:为什么需要通知?
想象这样一个场景:用户下单后30分钟未支付,订单自动取消。你怎么知道30分钟到了?
方案一:写个定时任务,每秒扫描一遍数据库?太蠢了,数据库会被你问崩溃。
方案二:在程序里用Timer或DelayQueue?重启就没了,分布式环境下更没法用。
方案三:让Redis在键过期的时候通知我?这个思路不错——这就是键空间通知的由来。
键空间通知是什么
Redis键空间通知(Keyspace Notifications)是Redis 2.8版本引入的功能,允许客户端通过Pub/Sub机制订阅Redis中键的变更事件。
它本质上是一种"观察者模式"的实现:当Redis中发生特定事件(如键过期、键被删除、List入队等)时,Redis会通过Pub/Sub频道发布通知,订阅了该频道的客户端就能收到消息。
两种通知类型
Redis提供两种维度的通知:
1. Keyspace通知(键空间通知):订阅某个键上发生的所有事件
频道格式:__keyspace@<db>__:<key> 示例:__keyspace@0__:mykey 收到消息内容:事件名称(如 "set", "expired", "del")2. Keyevent通知(键事件通知):订阅某种事件涉及的所有键
频道格式:__keyevent@<db>__:<event> 示例:__keyevent@0__:expired 收到消息内容:触发事件的键名(如 "mykey", "session:abc")两种通知的区别:
┌──────────────────────────────────────────────────────────┐ │ 事件发生 │ │ SET mykey "hello" │ │ │ │ ┌─────────────────────────┐ ┌────────────────────────┐ │ │ │ Keyspace通知 │ │ Keyevent通知 │ │ │ │ │ │ │ │ │ │ 频道: │ │ 频道: │ │ │ │ __keyspace@0__:mykey │ │ __keyevent@0__:set │ │ │ │ │ │ │ │ │ │ 消息: │ │ 消息: │ │ │ │ "set" │ │ "mykey" │ │ │ │ │ │ │ │ │ │ 视角:关注"mykey这个键 │ │ 视角:关注"set这个事件 │ │ │ │ 发生了什么" │ │ 影响了哪些键" │ │ │ └─────────────────────────┘ └────────────────────────┘ │ └──────────────────────────────────────────────────────────┘| 维度 | Keyspace通知 | Keyevent通知 |
|---|---|---|
| 订阅目标 | 特定键 | 特定事件类型 |
| 频道格式 | __keyspace@db__:key | __keyevent@db__:event |
| 消息内容 | 事件名称 | 键名 |
| 适用场景 | 监控某个key的状态变化 | 监控某类事件的所有触发 |
| 类比 | 关注某人的微博 | 关注某个话题的微博 |
notify-keyspace-events配置
键空间通知默认是关闭的!因为它会消耗一定的CPU资源(每次事件都需要发送Pub/Sub消息)。你需要通过notify-keyspace-events参数来开启。
配置参数详解
notify-keyspace-events的值由多个字母组合而成,每个字母代表一类事件:
| 字母 | 含义 | 事件类型 |
|---|---|---|
| K | Keyspace通知 | __keyspace@db__:key频道 |
| E | Keyevent通知 | __keyevent@db__:event频道 |
| g | 通用命令 | DEL, EXPIRE, RENAME等 |
| $ | String命令 | SET, INCR, APPEND等 |
| l | List命令 | LPUSH, RPUSH, LPOP, RPOP等 |
| z | Sorted Set命令 | ZADD, ZINCRBY, ZREM等 |
| x | 过期事件 | 键过期时触发 |
| e | 驱逐事件 | 键被maxmemory-policy淘汰时触发 |
| t | Stream命令 | XADD, XTRIM等 |
| m | Key-miss事件 | 访问不存在的键时触发(Redis 7.0+) |
| A | 等价于"g$lzxe" | 别名,含所有事件 |
关键规则:
- 至少需要
K或E中的一个,否则不会发送任何通知 K和E可以同时启用- 不指定具体事件类型字母时,即使启用了
K/E也不会有通知 A是g$lzxet的简写,不包含m
常见配置组合
# 最常用:开启所有键空间和键事件通知CONFIG SET notify-keyspace-events"KEA"# 只关注过期事件(延迟任务场景)CONFIG SET notify-keyspace-events"Kx"# 只关注过期和驱逐事件CONFIG SET notify-keyspace-events"Kxe"# 关闭通知CONFIG SET notify-keyspace-events""踩坑提示:
notify-keyspace-events的值中,K或E是"开关",后面的字母是"筛选器"。如果你只写了Kx,那么只有Keyspace格式的过期事件通知。如果你想要两种格式都收到过期事件,需要写KEx。
实战:订阅过期事件
下面是一个完整的过期事件订阅演示。
步骤1:开启通知
127.0.0.1:6379>CONFIG SET notify-keyspace-events"KEA"OK步骤2:终端A——订阅过期事件
# 订阅0号数据库的所有过期事件127.0.0.1:6379>SUBSCRIBE __keyevent@0__:expired Reading messages...(press Ctrl-C to quit)1)"subscribe"2)"__keyevent@0__:expired"3)(integer)1步骤3:终端B——设置带TTL的键
127.0.0.1:6379>SET order:10086"pending"EX5OK步骤4:5秒后,终端A收到通知
1)"message"2)"__keyevent@0__:expired"# 频道3)"order:10086"# 过期的键名同时订阅Keyspace通知
如果你同时订阅了__keyspace@0__:order:10086,在键过期时也会收到:
1)"message"2)"__keyspace@0__:order:10086"# 频道3)"expired"# 事件名称实际应用场景
场景一:监听key过期实现延迟任务
最常见的场景——订单超时自动取消:
┌──────────┐ SET order:10086 EX 300 ┌──────────┐ │ 业务系统 │ ───────────────────────────► │ Redis │ │ │ │ │ │ │ │ 5分钟后 │ │ │ │ 键过期 │ │ │ expired通知 │ │ │ │ ◄──────────────────────────── │ │ │ │ │ │ │ 取消订单 │ │ │ │ 释放库存 │ │ │ └──────────┘ └──────────┘代码示例(Python伪代码):
importredisimportthreading r=redis.Redis()deforder_timeout_handler():"""监听订单过期事件"""pubsub=r.pubsub()pubsub.subscribe('__keyevent@0__:expired')formessageinpubsub.listen():ifmessage['type']=='message':key=message['data']ifkey.startswith(b'order:'):order_id=key.decode().split(':')[1]cancel_order(order_id)# 取消订单release_stock(order_id)# 释放库存# 创建订单时设置TTLdefcreate_order(order_id):r.set(f'order:{order_id}','pending',ex=300)# 5分钟超时场景二:监听key修改实现数据变更通知
当缓存数据被更新时,通知其他服务刷新本地缓存:
# 服务A更新了配置SET config:app"new_config_value"# 服务B订阅了该key的变化SUBSCRIBE __keyspace@0__:config:app# 收到 "set" 事件后,刷新本地缓存场景三:监听List入队事件触发消费
# 生产者入队LPUSH task_queue"task_data"# 消费者订阅List的push事件SUBSCRIBE __keyspace@0__:task_queue# 收到 "lpush" 事件后,开始消费踩坑提示:这种方式在并发场景下可能重复消费——如果多个消费者都订阅了同一个事件,它们会同时收到通知。需要额外的锁或分配机制来保证任务不被重复处理。
过期通知的全流程
下面是过期通知从产生到消费者收到的完整流程:
┌──────────────────────────────────────────────────────────────┐ │ 过期通知全流程 │ │ │ │ 1. 键过期 │ │ ┌──────────┐ │ │ │ TTL = 0 │ (注意:TTL=0不代表立刻触发通知!) │ │ └────┬─────┘ │ │ │ │ │ ▼ │ │ 2. 被删除(惰性/定期) │ │ ┌──────────────────┐ │ │ │ expireIfNeeded() │ 或 activeExpireCycle() │ │ │ 删除过期键 │ │ │ └────┬─────────────┘ │ │ │ │ │ ▼ │ │ 3. 检查通知配置 │ │ ┌──────────────────────────┐ │ │ │ notify-keyspace-events │ │ │ │ 是否包含K/E和x? │ │ │ └────┬─────────┬───────────┘ │ │ │是 │否 │ │ ▼ ▼ │ │ 4. 发送通知 4. 不发送 │ │ ┌──────────┐ │ │ │ PUBLISH │ │ │ │ __keyspace│ │ │ │ __keyevent│ │ │ └────┬─────┘ │ │ │ │ │ ▼ │ │ 5. 消费者收到消息 │ │ ┌──────────────────┐ │ │ │ SUBSCRIBE客户端 │ │ │ │ 收到expired通知 │ │ │ └──────────────────┘ │ │ │ └──────────────────────────────────────────────────────────────┘键空间通知的局限性
键空间通知很方便,但它有几条重要的局限性,你在使用之前必须了解:
局限一:不保证100%送达
Redis的Pub/Sub是fire-and-forget(发完就忘)模式。如果消费者断线了,或者Redis重启了,通知就丢了,没有重试机制。
正常情况: Redis ──expired通知──► 消费者 ✓ 异常情况: Redis ──expired通知──► 消费者离线 ✗ (通知丢失,无法找回) Redis 重启 ── 所有未消费的通知丢失 ✗局限二:过期通知的时机不是TTL=0
这是一个非常重要的认知:键的过期通知不是在TTL减到0的那一刻发出的,而是在键被实际删除的时候发出的。
由于Redis使用惰性删除+定期删除策略,一个键的TTL变为0后:
- 如果没有人访问这个键,它可能在TTL=0后的一小段时间内还存在于内存中
- 直到惰性删除或定期删除真正删除了这个键,过期通知才会发出
- 这个延迟通常在0-100毫秒之间,但也可能更长(如果定期删除的那一轮没有抽到这个键)
# 设置1秒过期SET key"value"EX1# 1秒后TTL=0,但键可能还在内存中# 等到惰性/定期删除真正删除时,通知才发出# 可能是1.001秒后,也可能是1.1秒后局限三:不适用于高频事件
如果你的Redis实例每秒有成千上万的键过期,每个过期都会触发通知,这会给Pub/Sub系统和网络带来很大压力。
局限四:不支持集群模式的全局通知
在Redis Cluster中,键空间通知只在当前节点有效。如果你想订阅所有节点的过期事件,需要连接到每个节点分别订阅。
延迟任务方案的可靠性分析
基于键空间通知的延迟任务是最常见的应用场景,但它真的可靠吗?我们来做一个对比:
| 维度 | 键空间通知 | Redis Stream | 专业MQ(RabbitMQ/Kafka) |
|---|---|---|---|
| 可靠性 | 低(消息可能丢失) | 高(持久化+消费者组) | 高(持久化+确认机制) |
| 延迟精度 | 低(取决于删除时机) | 中(轮询或阻塞读取) | 高(定时投递) |
| 消费者扩展 | 困难(广播模式,重复消费) | 容易(消费者组) | 容易(分区/队列) |
| 断线恢复 | 无(消息丢失) | 有(未确认消息重分配) | 有(确认+重试) |
| 实现复杂度 | 低 | 中 | 高 |
| 运维成本 | 低(Redis自带) | 中(Redis自带) | 高(额外组件) |
| 适用规模 | 小型/非关键业务 | 中型/可接受偶尔丢失 | 大型/关键业务 |
推荐方案
┌──────────────────────────────────────────┐ │ 延迟任务方案选择决策树 │ │ │ │ 数据丢失是否可接受? │ │ │ │ │ 是 │ 否 │ │ │ │ │ │ ▼ ▼ │ │ 键空间通知 需要精确延迟? │ │ (简单) │ │ │ │ 否 │ 是 │ │ │ │ │ │ │ ▼ ▼ │ │ Redis Stream 专业MQ │ │ (中规中矩) (重量级) │ └──────────────────────────────────────────┘实际建议:
- 非关键业务(如日志清理、非核心缓存更新):键空间通知足够
- 中等关键业务(如订单超时提醒——超时几秒无所谓):Redis Stream + 定时扫描兜底
- 关键业务(如支付超时、库存锁定):专业MQ(RocketMQ延迟消息、RabbitMQ死信队列)
Redis Stream实现延迟任务的思路
如果你觉得键空间通知不够可靠,但又不想引入专业MQ,可以用Redis Stream实现一个简单的延迟队列:
# 生产者:写入延迟任务(按执行时间排序)XADD delay_queue * execute_at1687700060task_data"cancel_order:10086"# 消费者:定时轮询# 每秒执行一次:XRANGE delay_queue - + COUNT100# 过滤 execute_at <= now 的任务# 处理后 XACK 或 XDEL这种方案比键空间通知更可靠,因为Stream支持持久化和消费者组。
配置最佳实践
# 1. 只开启你需要的事件类型,不要用"A"全开# 如果只需要过期通知:CONFIG SET notify-keyspace-events"KEx"# 2. 在redis.conf中持久化配置notify-keyspace-events"KEx"# 3. 监控Pub/Sub的输出缓冲区# 通知量大时可能导致输出缓冲区暴涨CLIENT LIST# 关注 omem 字段(输出缓冲区内存)# 4. 设置合理的输出缓冲区上限CONFIG SET client-output-buffer-limit"normal 0 0 0 pubsub 32mb 8mb 60"踩坑提示:如果通知消费者处理速度跟不上生产速度,Redis的输出缓冲区会暴涨,最终可能触发
client-output-buffer-limit导致消费者被断开。这在高频过期场景下尤其需要注意。
总结
键空间通知是Redis提供的一个轻量级事件订阅机制:
- 两种通知维度:Keyspace(关注键)和Keyevent(关注事件),可以根据需求选择
- 配置灵活:通过
notify-keyspace-events参数精确控制需要哪些事件 - 实战简单:几行代码就能实现延迟任务、变更通知等功能
- 但有限制:不保证送达、过期通知有延迟、不支持集群全局通知
- 方案选择:非关键业务用通知,关键业务用Stream或专业MQ
理解了通知的局限性,你就能做出合理的架构决策——用最简单的方案解决问题,而不是最复杂的。下一篇,我们将从通知回到持久化的主线,深入RDB快照的实现原理。
上一篇【第27篇】过期键的骨牌效应——AOF/RDB/复制中的过期处理
下一篇【第29篇】RDB持久化——Redis的快照是怎么拍的
