【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通知键空间通知订阅某个键上发生的所有事件频道格式__keyspacedb__:key 示例__keyspace0__:mykey 收到消息内容事件名称如 set, expired, del2. Keyevent通知键事件通知订阅某种事件涉及的所有键频道格式__keyeventdb__:event 示例__keyevent0__:expired 收到消息内容触发事件的键名如 mykey, session:abc两种通知的区别┌──────────────────────────────────────────────────────────┐ │ 事件发生 │ │ SET mykey hello │ │ │ │ ┌─────────────────────────┐ ┌────────────────────────┐ │ │ │ Keyspace通知 │ │ Keyevent通知 │ │ │ │ │ │ │ │ │ │ 频道 │ │ 频道 │ │ │ │ __keyspace0__:mykey │ │ __keyevent0__:set │ │ │ │ │ │ │ │ │ │ 消息 │ │ 消息 │ │ │ │ set │ │ mykey │ │ │ │ │ │ │ │ │ │ 视角关注mykey这个键 │ │ 视角关注set这个事件 │ │ │ │ 发生了什么 │ │ 影响了哪些键 │ │ │ └─────────────────────────┘ └────────────────────────┘ │ └──────────────────────────────────────────────────────────┘维度Keyspace通知Keyevent通知订阅目标特定键特定事件类型频道格式__keyspacedb__:key__keyeventdb__:event消息内容事件名称键名适用场景监控某个key的状态变化监控某类事件的所有触发类比关注某人的微博关注某个话题的微博notify-keyspace-events配置键空间通知默认是关闭的因为它会消耗一定的CPU资源每次事件都需要发送Pub/Sub消息。你需要通过notify-keyspace-events参数来开启。配置参数详解notify-keyspace-events的值由多个字母组合而成每个字母代表一类事件字母含义事件类型KKeyspace通知__keyspacedb__:key频道EKeyevent通知__keyeventdb__:event频道g通用命令DEL, EXPIRE, RENAME等$String命令SET, INCR, APPEND等lList命令LPUSH, RPUSH, LPOP, RPOP等zSorted Set命令ZADD, ZINCRBY, ZREM等x过期事件键过期时触发e驱逐事件键被maxmemory-policy淘汰时触发tStream命令XADD, XTRIM等mKey-miss事件访问不存在的键时触发Redis 7.0A等价于g$lzxe别名含所有事件关键规则至少需要K或E中的一个否则不会发送任何通知K和E可以同时启用不指定具体事件类型字母时即使启用了K/E也不会有通知A是g$lzxet的简写不包含m常见配置组合# 最常用开启所有键空间和键事件通知CONFIG SET notify-keyspace-eventsKEA# 只关注过期事件延迟任务场景CONFIG SET notify-keyspace-eventsKx# 只关注过期和驱逐事件CONFIG SET notify-keyspace-eventsKxe# 关闭通知CONFIG SET notify-keyspace-events踩坑提示notify-keyspace-events的值中K或E是开关后面的字母是筛选器。如果你只写了Kx那么只有Keyspace格式的过期事件通知。如果你想要两种格式都收到过期事件需要写KEx。实战订阅过期事件下面是一个完整的过期事件订阅演示。步骤1开启通知127.0.0.1:6379CONFIG SET notify-keyspace-eventsKEAOK步骤2终端A——订阅过期事件# 订阅0号数据库的所有过期事件127.0.0.1:6379SUBSCRIBE __keyevent0__:expired Reading messages...(press Ctrl-C to quit)1)subscribe2)__keyevent0__:expired3)(integer)1步骤3终端B——设置带TTL的键127.0.0.1:6379SET order:10086pendingEX5OK步骤45秒后终端A收到通知1)message2)__keyevent0__:expired# 频道3)order:10086# 过期的键名同时订阅Keyspace通知如果你同时订阅了__keyspace0__:order:10086在键过期时也会收到1)message2)__keyspace0__:order:10086# 频道3)expired# 事件名称实际应用场景场景一监听key过期实现延迟任务最常见的场景——订单超时自动取消┌──────────┐ SET order:10086 EX 300 ┌──────────┐ │ 业务系统 │ ───────────────────────────► │ Redis │ │ │ │ │ │ │ │ 5分钟后 │ │ │ │ 键过期 │ │ │ expired通知 │ │ │ │ ◄──────────────────────────── │ │ │ │ │ │ │ 取消订单 │ │ │ │ 释放库存 │ │ │ └──────────┘ └──────────┘代码示例Python伪代码importredisimportthreading rredis.Redis()deforder_timeout_handler():监听订单过期事件pubsubr.pubsub()pubsub.subscribe(__keyevent0__:expired)formessageinpubsub.listen():ifmessage[type]message:keymessage[data]ifkey.startswith(border:):order_idkey.decode().split(:)[1]cancel_order(order_id)# 取消订单release_stock(order_id)# 释放库存# 创建订单时设置TTLdefcreate_order(order_id):r.set(forder:{order_id},pending,ex300)# 5分钟超时场景二监听key修改实现数据变更通知当缓存数据被更新时通知其他服务刷新本地缓存# 服务A更新了配置SET config:appnew_config_value# 服务B订阅了该key的变化SUBSCRIBE __keyspace0__:config:app# 收到 set 事件后刷新本地缓存场景三监听List入队事件触发消费# 生产者入队LPUSH task_queuetask_data# 消费者订阅List的push事件SUBSCRIBE __keyspace0__:task_queue# 收到 lpush 事件后开始消费踩坑提示这种方式在并发场景下可能重复消费——如果多个消费者都订阅了同一个事件它们会同时收到通知。需要额外的锁或分配机制来保证任务不被重复处理。过期通知的全流程下面是过期通知从产生到消费者收到的完整流程┌──────────────────────────────────────────────────────────────┐ │ 过期通知全流程 │ │ │ │ 1. 键过期 │ │ ┌──────────┐ │ │ │ TTL 0 │ 注意TTL0不代表立刻触发通知 │ │ └────┬─────┘ │ │ │ │ │ ▼ │ │ 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 重启 ── 所有未消费的通知丢失 ✗局限二过期通知的时机不是TTL0这是一个非常重要的认知键的过期通知不是在TTL减到0的那一刻发出的而是在键被实际删除的时候发出的。由于Redis使用惰性删除定期删除策略一个键的TTL变为0后如果没有人访问这个键它可能在TTL0后的一小段时间内还存在于内存中直到惰性删除或定期删除真正删除了这个键过期通知才会发出这个延迟通常在0-100毫秒之间但也可能更长如果定期删除的那一轮没有抽到这个键# 设置1秒过期SET keyvalueEX1# 1秒后TTL0但键可能还在内存中# 等到惰性/定期删除真正删除时通知才发出# 可能是1.001秒后也可能是1.1秒后局限三不适用于高频事件如果你的Redis实例每秒有成千上万的键过期每个过期都会触发通知这会给Pub/Sub系统和网络带来很大压力。局限四不支持集群模式的全局通知在Redis Cluster中键空间通知只在当前节点有效。如果你想订阅所有节点的过期事件需要连接到每个节点分别订阅。延迟任务方案的可靠性分析基于键空间通知的延迟任务是最常见的应用场景但它真的可靠吗我们来做一个对比维度键空间通知Redis Stream专业MQRabbitMQ/Kafka可靠性低消息可能丢失高持久化消费者组高持久化确认机制延迟精度低取决于删除时机中轮询或阻塞读取高定时投递消费者扩展困难广播模式重复消费容易消费者组容易分区/队列断线恢复无消息丢失有未确认消息重分配有确认重试实现复杂度低中高运维成本低Redis自带中Redis自带高额外组件适用规模小型/非关键业务中型/可接受偶尔丢失大型/关键业务推荐方案┌──────────────────────────────────────────┐ │ 延迟任务方案选择决策树 │ │ │ │ 数据丢失是否可接受 │ │ │ │ │ 是 │ 否 │ │ │ │ │ │ ▼ ▼ │ │ 键空间通知 需要精确延迟 │ │ (简单) │ │ │ │ 否 │ 是 │ │ │ │ │ │ │ ▼ ▼ │ │ Redis Stream 专业MQ │ │ (中规中矩) (重量级) │ └──────────────────────────────────────────┘实际建议非关键业务如日志清理、非核心缓存更新键空间通知足够中等关键业务如订单超时提醒——超时几秒无所谓Redis Stream 定时扫描兜底关键业务如支付超时、库存锁定专业MQRocketMQ延迟消息、RabbitMQ死信队列Redis Stream实现延迟任务的思路如果你觉得键空间通知不够可靠但又不想引入专业MQ可以用Redis Stream实现一个简单的延迟队列# 生产者写入延迟任务按执行时间排序XADD delay_queue * execute_at1687700060task_datacancel_order:10086# 消费者定时轮询# 每秒执行一次XRANGE delay_queue - COUNT100# 过滤 execute_at now 的任务# 处理后 XACK 或 XDEL这种方案比键空间通知更可靠因为Stream支持持久化和消费者组。配置最佳实践# 1. 只开启你需要的事件类型不要用A全开# 如果只需要过期通知CONFIG SET notify-keyspace-eventsKEx# 2. 在redis.conf中持久化配置notify-keyspace-eventsKEx# 3. 监控Pub/Sub的输出缓冲区# 通知量大时可能导致输出缓冲区暴涨CLIENT LIST# 关注 omem 字段输出缓冲区内存# 4. 设置合理的输出缓冲区上限CONFIG SET client-output-buffer-limitnormal 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的快照是怎么拍的