资讯动态

Redis发布-订阅模式详解:原理、实战与避坑指南

发布时间:2026/10/14 10:43:59 来源:尧图企业网站定制
Redis 的发布-订阅模式很多人第一次接触是在做实时消息推送的时候。当时我手里有个需求几个客户端要同时收到某个状态变更的通知第一反应是轮询但延迟和资源消耗都让人难受。后来翻 Redis 文档发现它自带 Pub/Sub几行命令就能跑通那种“原来这么简单”的感觉至今记得。这篇文章就围绕 Redis 发布-订阅模式展开从它到底解决了什么问题、底层怎么实现的、到实际项目里怎么用、有哪些坑全部拆开讲一遍。不管你是刚接触 Redis 的新手还是已经用过但没深究的开发者应该都能从中拿到点东西。1. 发布-订阅模式到底解决了什么问题1.1 从轮询到推送的思维转变在没有发布-订阅之前最朴素的做法是轮询。客户端每隔几秒问一次服务端“有没有新消息”服务端查一下数据库或者缓存有就返回没有就返回空。这个模式能跑但问题很明显延迟取决于轮询间隔间隔短了请求量爆炸间隔长了消息不及时。更关键的是大量轮询请求是无效的服务端白白消耗资源。发布-订阅换了个思路不是消费者主动去拉而是生产者推。消息产生的那一刻Redis 主动把消息投递给所有订阅了对应频道的客户端。这个转变带来的好处是实时的、低延迟的而且消费者不需要关心消息什么时候来只需要挂在那里等就行。你可以把它理解成收音机电台发布者在播你调好频道订阅者就能听不需要每隔几秒去问电台“现在有节目吗”。1.2 Redis Pub/Sub 的定位与边界Redis 的发布-订阅是一个轻量级的消息通信机制核心命令就几个SUBSCRIBE、UNSUBSCRIBE、PUBLISH、PSUBSCRIBE、PUNSUBSCRIBE。它不持久化消息不保证送达没有 ACK 机制也不支持消息回溯。这意味着什么意味着它适合做实时通知、事件广播、简单解耦但不适合做订单、支付这类要求可靠投递的场景。我见过有人拿 Redis Pub/Sub 当消息队列用结果消费者重启期间的消息全丢了排查半天才发现是模式选错了。所以第一件事就是搞清楚它的边界它是“广播”不是“队列”。广播的特点是消息发出去了谁在线谁收到不在线就错过Redis 不负责补发。1.3 和其他消息方案的对比特性Redis Pub/Sub专业消息队列轮询实时性高毫秒级高取决于间隔消息持久化不支持支持取决于存储送达保证至多一次至少一次/精确一次取决于实现消费者离线消息丢失可保留不涉及部署复杂度极低较高低适用场景实时通知、广播可靠投递、削峰简单状态查询这张表不是要分出高下而是帮你做选型。如果你的场景是“服务端配置变了通知所有节点刷新”Redis Pub/Sub 几行代码就搞定上专业消息队列反而是杀鸡用牛刀。但如果是“用户下单后要保证库存扣减消息不丢”那就别用 Pub/Sub。2. 核心命令与底层机制拆解2.1 五个核心命令的用法与区别SUBSCRIBE channel [channel ...]用来订阅一个或多个具体频道。执行后客户端进入订阅状态只能执行订阅相关命令不能再执行GET、SET这类普通命令。这一点新手很容易踩坑订阅之后发现其他命令报错其实是客户端被“锁”在订阅模式里了。PSUBSCRIBE pattern [pattern ...]是模式订阅支持通配符。比如PSUBSCRIBE news.*可以匹配news.tech、news.sports。它的灵活性更高但要注意模式匹配是有开销的频道数量多的时候性能会受影响。PUBLISH channel message向指定频道发消息返回值为收到消息的订阅者数量。注意这个返回值是“当时在线且订阅了的客户端数”不包括模式订阅的匹配数实际上 Redis 返回值包含模式订阅者但语义上要理解清楚。UNSUBSCRIBE和PUNSUBSCRIBE分别退订具体频道和模式频道。不指定参数时退订所有。# 终端 A订阅 SUBSCRIBE order.created # 输出Reading messages... (press Ctrl-C to quit) # 终端 B发布 PUBLISH order.created order_id1001 # 输出(integer) 1 # 终端 A 收到 # 1) message # 2) order.created # 3) order_id10012.2 底层数据结构字典与链表Redis 内部维护了一个pubsub_channels字典key 是频道名value 是一个链表链表里挂着所有订阅该频道的客户端。当执行PUBLISH时Redis 先根据频道名找到对应的链表然后遍历链表把消息逐个写给客户端。模式订阅单独存在pubsub_patterns链表里每个节点包含模式和订阅的客户端。发布消息时除了遍历频道订阅者还要遍历模式链表用模式去匹配频道名匹配成功就发送。这就是为什么模式订阅多了会影响性能——每次发布都要做一轮模式匹配。2.3 消息是“推”还是“拉”Redis Pub/Sub 是服务端主动推。当PUBLISH执行时Redis 直接把消息写入订阅者客户端的输出缓冲区然后由事件循环把缓冲区数据发出去。这里有个关键点如果订阅者消费慢输出缓冲区会堆积超过限制client-output-buffer-limit pubsub客户端会被强制断开。这个参数默认是 32MB 硬限制、8MB 软限制持续 60 秒生产环境要根据消息量调整。提示client-output-buffer-limit pubsub 0 0 0表示不限制但强烈不建议慢消费者会把 Redis 内存吃光。3. 实际项目中的落地方式3.1 用 Redis CLI 快速验证刚接触的时候别急着写代码先用命令行把流程跑通。开两个终端一个订阅一个发布观察输出。这一步能帮你建立直观感受订阅端会收到一个三元素数组第一个是消息类型message或pmessage第二个是频道名第三个是消息内容。模式订阅收到的类型是pmessage而且会多一个元素表示实际匹配的频道。比如PSUBSCRIBE news.*收到news.tech的消息时数组是[pmessage, news.*, news.tech, 内容]。这个细节在写客户端解析逻辑时很重要不处理的话会取错字段。3.2 Python 客户端实现用redis-py实现一个简单的发布者和订阅者import redis import threading import time # 发布者 def publisher(): r redis.Redis(hostlocalhost, port6379, decode_responsesTrue) for i in range(5): count r.publish(demo.channel, fmessage-{i}) print(f发布 message-{i}在线订阅者数{count}) time.sleep(1) # 订阅者 def subscriber(name): r redis.Redis(hostlocalhost, port6379, decode_responsesTrue) pubsub r.pubsub() pubsub.subscribe(demo.channel) for msg in pubsub.listen(): if msg[type] message: print(f[{name}] 收到{msg[data]}) if __name__ __main__: t1 threading.Thread(targetsubscriber, args(订阅者A,)) t2 threading.Thread(targetsubscriber, args(订阅者B,)) t1.start() t2.start() time.sleep(1) publisher()这段代码跑起来你会看到两个订阅者都收到了全部 5 条消息这就是广播语义。注意pubsub.listen()是阻塞的实际项目里要放在独立线程或异步任务里别阻塞主流程。3.3 模式订阅的实战用法假设你有一个多租户系统每个租户有独立的事件频道格式是tenant.{id}.event。如果每个租户都单独订阅租户多了订阅数爆炸。这时候模式订阅就派上用场pubsub.psubscribe(tenant.*.event) for msg in pubsub.listen(): if msg[type] pmessage: actual_channel msg[channel] # 实际匹配的频道 tenant_id actual_channel.split(.)[1] handle_event(tenant_id, msg[data])这样一条模式订阅覆盖所有租户代码简洁维护成本低。但要注意如果租户数量上千每次发布都要遍历模式链表做匹配性能会下降。我的经验是模式订阅控制在几十个以内比较稳妥再多就要考虑分片或者换方案。3.4 与 WebSocket 结合做实时推送一个典型场景是 Web 后端用 WebSocket 维持长连接Redis Pub/Sub 做跨节点的消息分发。用户 A 在节点 1 发消息节点 2 上的用户 B 要收到怎么实现节点 1 把消息PUBLISH到 Redis所有节点都订阅了同一个频道节点 2 收到后通过 WebSocket 推给用户 B。这个架构的关键是每个节点都要有一个订阅者连接。注意订阅连接是独占的不能和普通命令共用同一个连接订阅状态下不能执行其他命令所以通常单独开一个连接专门做订阅。连接断了要自动重连重连后重新订阅这部分逻辑必须做健壮否则消息会静默丢失。4. 常见问题与排查技巧实录4.1 消息丢失的几种情况消息丢失是问得最多的问题。Redis Pub/Sub 本身不保证送达丢失场景主要有订阅者不在线、订阅者处理慢被断开、网络抖动导致连接中断、Redis 主从切换。前两种是设计使然后两种需要靠客户端重连机制兜底。排查的时候先确认订阅者是否真的在线。用PUBLISH的返回值判断返回 0 说明当时没有订阅者。如果返回值大于 0 但订阅者说没收到检查是不是连接断了没重连或者消息被输出缓冲区限制断开了。查看CLIENT LIST可以看到每个连接的omem输出缓冲区内存和sub订阅频道数。4.2 订阅者收不到消息的排查清单现象可能原因排查方法PUBLISH 返回 0没有订阅者在线检查订阅端连接状态订阅端无输出连接断开未重连查看客户端日志、网络状态部分消息丢失输出缓冲区超限被断开查看client-output-buffer-limit配置模式订阅不匹配通配符写法错误用PUBSUB CHANNELS确认频道名消息延迟高订阅者处理慢检查订阅端消费逻辑是否阻塞4.3 连接管理与重连策略生产环境必须做重连。我的做法是订阅线程里包一层循环listen()抛异常或者正常结束就等几秒重连重连后重新执行subscribe。同时加一个心跳检测比如每隔 30 秒PING一下确认连接还活着。def robust_subscriber(): while True: try: r redis.Redis(hostlocalhost, port6379) pubsub r.pubsub() pubsub.subscribe(demo.channel) for msg in pubsub.listen(): if msg[type] message: process(msg[data]) except Exception as e: print(f订阅异常{e}5秒后重连) time.sleep(5)这段代码简单但有效。注意重连后可能有短暂的消息空窗期对可靠性要求高的场景要在业务层做补偿。4.4 性能优化的几个实操点第一控制消息大小。Pub/Sub 的消息走网络传输单条消息别超过几 KB大消息考虑传引用比如消息 ID让订阅者自己去取。第二控制频道数量。频道太多PUBLISH时查找和遍历的开销增加。第三模式订阅慎用通配符news.*比news.tech.*匹配范围大开销也大。第四订阅者处理逻辑要异步化收到消息后丢到队列里慢慢处理别在listen循环里做耗时操作否则会拖慢整个订阅连接。注意Redis 6.0 之后引入了客户端缓存和 RESP3但 Pub/Sub 的核心机制没变。如果你用的是集群模式Pub/Sub 的消息默认只在当前节点广播不会跨节点转发跨节点广播需要用SPUBLISH/SSUBSCRIBERedis 7.0 的分片 Pub/Sub。5. 进阶分片 Pub/Sub 与集群适配5.1 普通 Pub/Sub 在集群下的局限Redis Cluster 把数据分片到不同节点但普通 Pub/Sub 的频道不属于任何分片消息只在接收PUBLISH的那个节点上广播。这意味着如果订阅者连的是节点 A发布者在节点 B 发消息订阅者是收不到的。这个问题在单机模式下不存在一上集群就暴露。解决办法有两个一是让所有发布都走同一个节点但这样有单点瓶颈二是用 Redis 7.0 引入的分片 Pub/Sub。5.2 分片 Pub/Sub 的用法分片 Pub/Sub 用SPUBLISH和SSUBSCRIBE频道通过哈希映射到具体分片消息只在对应分片广播。这样每个节点只处理自己负责的频道天然支持水平扩展。# 订阅分片频道 SSUBSCRIBE shard.channel # 发布分片消息 SPUBLISH shard.channel hello注意SPUBLISH的返回值是收到消息的订阅者数但只在当前分片内统计。如果你的订阅者分布在多个节点需要确保它们订阅的是同一个分片频道否则收不到。5.3 什么时候该考虑换方案如果你发现需要消息持久化、需要消费者组、需要消息回溯、需要 ACK 确认那 Redis Pub/Sub 就不合适了。Redis 5.0 引入的 Stream 类型支持消费者组和持久化可以作为轻量级替代。再往上就是专业消息队列的领域了。选型的核心是匹配需求不是追求技术先进。6. 我踩过的坑与实操心得第一个坑是订阅连接复用。早期图省事用一个连接既订阅又执行普通命令结果订阅之后所有GET/SET都报错。后来才知道订阅状态下连接只能执行订阅相关命令必须单独开连接。第二个坑是忘记处理pmessage类型。用模式订阅时消息类型是pmessage不是message一开始只判断了message导致模式订阅的消息全被过滤掉了。这个 bug 藏得深因为订阅本身是成功的只是消息没进处理逻辑。第三个坑是输出缓冲区。有一次压测订阅者处理慢Redis 日志里出现大量客户端被断开的记录排查发现是client-output-buffer-limit默认值太小。调整之后问题解决但也让我意识到 Pub/Sub 对慢消费者不友好消费逻辑必须快。第四个坑是集群下的频道不互通。本地单机测试一切正常上集群后消息时有时无查了半天才发现是发布和订阅连了不同节点。后来统一用分片 Pub/Sub 才彻底解决。这些坑的共同点是文档里都写了但没踩过就不会真正注意。所以我的建议是新项目接入 Pub/Sub 之前先把这几个场景在测试环境模拟一遍比看十遍文档都管用。最后分享一个小技巧用PUBSUB NUMSUB channel可以查看指定频道的订阅者数量PUBSUB CHANNELS列出当前有订阅者的频道PUBSUB NUMPAT查看模式订阅总数。这几个命令在排查“消息发出去没人收”的时候特别有用能快速定位是发布端问题还是订阅端问题。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑