何时应使用 Redis 作为消息代理?
探索利用 Redis 两大核心功能(发布/订阅和流)作为消息代理的理想场景。本全面指南详细介绍了 Redis 消息传递的性能优势、低延迟和基础设施优势。了解临时性发布/订阅与持久化流之间的关键区别,理解它们与 Kafka 等专用代理相比的局限性,并找到实用的用例——从简单的缓存失效到健壮的轻量级任务队列——帮助您为异步通信需求选择合适的工具。
何时应使用 Redis 作为消息代理?
当任务规模小、速度快且与 Redis 中已有的数据紧密相关时,Redis 可以成为一个优秀的消息代理。但如果您需要强大的交付保证、长期保留、路由功能,或者需要一个在消息历史远大于内存时仍能稳定运行的代理,那么 Redis 可能就不是合适的选择。
如果您不再问“Redis 能处理消息吗?”,而是问“哪种 Redis 消息传递原语符合我能容忍的故障模式?”,那么决策就会变得更容易。
Redis 提供了几种模式,但针对这个问题,最重要的有两种:
- 发布/订阅:用于实时广播。
- 流:用于持久化、类似日志的消息处理,支持消费者组。
它们从远处看很相似,但在操作上完全不同。
当可以接受丢失消息时,使用发布/订阅
Redis 发布/订阅是一种实时广播。发布者向频道发送消息,已连接的订阅者接收消息。Redis 不会为断开的订阅者存储该消息,也没有内置的确认机制。
这对于某些任务来说是完美的:
SUBSCRIBE cache:invalidations
PUBLISH cache:invalidations 'product:123'
如果一个应用程序实例因在错误时刻重启而错过了该失效通知,世界不应因此终结。本地缓存应具有 TTL、版本检查或其他恢复方式。发布/订阅是一条通知路径,而非事实来源。
发布/订阅的良好用例:
- 应用程序实例间的缓存失效。
- 实时 UI 更新,客户端只关心当前状态。
- 存在信号,如“用户正在输入”或“工作进程心跳已更改”。
- 轻量级部署或配置更改通知。
- 可容忍丢失消息的扇出事件。
发布/订阅的不良用例:
- 支付处理。
- 必须最终发送的电子邮件任务。
- 不可跳过的库存更新。
- 审计日志。
- 任何断开的消费者必须稍后追赶上的场景。
发布/订阅速度快,因为它做的事情少。这就是权衡。
当消费者需要追赶时,使用流
Redis 流在流数据结构中存储条目:
XADD orders:events * order_id 42 status paid
消费者可以从某个位置读取:
XREAD COUNT 10 STREAMS orders:events 0
消费者组允许多个工作进程共享工作:
XGROUP CREATE orders:events order-workers 0 MKSTREAM
XREADGROUP GROUP order-workers worker-1 COUNT 10 STREAMS orders:events >
XACK orders:events order-workers 1740000000000-0
使用消费者组时,传递给工作进程的消息会保留在待处理条目列表中,直到通过 XACK 确认。如果工作进程在读取后但在确认前崩溃,另一个工作进程可以检查并认领待处理的工作。当您正确构建消费者时,这可以提供至少一次处理。
至少一次意味着可能出现重复。您的工作进程必须是幂等的。例如,电子邮件工作进程应记录 email_job_id=abc123 已发送,然后再尝试再次发送。订单工作进程应避免在看到相同流条目两次时重复收费。
流的良好用例:
- 轻量级后台任务。
- 需要在短暂中断后重放的内部服务事件。
- 中小型事件日志。
- 工作进程池,其中每个任务应由组中的一个工作进程处理。
- 具有有限保留期的活动源或状态更改日志。
流并非免费。条目存在于 Redis 内存中,除非您设计修剪或过期。如果您从不修剪繁忙的流,该流将成为您的下一个内存事故。
使用修剪:
XADD orders:events MAXLEN ~ 100000 * order_id 42 status paid
XTRIM orders:events MAXLEN ~ 100000
使用 ~ 进行近似修剪通常比精确修剪更便宜。根据恢复需求选择保留期,而不是凭希望。
Redis 列表对于简单队列仍然有用
在流出现之前,许多 Redis 队列使用列表:
LPUSH jobs:email '{"to":"[email protected]"}'
BRPOP jobs:email 5
列表对于非常简单的队列仍然适用,特别是当您需要阻塞弹出行为且不需要消费者组或历史记录时。限制在于恢复。如果工作进程弹出一个任务并在完成前崩溃,该任务将丢失,除非您添加额外的记账。
有一些模式使用 BRPOPLPUSH 或 BLMOVE 将任务移动到处理列表,然后在成功后删除。这些模式可以工作,但一旦您需要待处理跟踪、重试和多个消费者,流通常会为您提供一个更清晰的起点。
当简单性比代理功能更重要时,选择 Redis
当 Redis 已经是您技术栈的一部分且工作负载适中时,Redis 消息传递很有吸引力。您避免了操作另一个分布式系统。开发人员已经熟悉 Redis 客户端、监控、凭据和部署路径。
这是一个合理的理由。操作简单性具有实际价值。
Redis 的延迟也非常低。如果您的应用程序和 Redis 位于同一区域或私有网络中,发布一个小通知通常既便宜又快速。对于缓存失效或实时状态更新,更重的代理可能是不必要的。
Redis 还允许您仔细地结合状态更改和消息。Lua 脚本或事务可以在一个 Redis 端操作中更新键并追加到流。这对于 Redis 作为中央状态持有者的小型系统可能很有用。
问题是 Redis 不应意外地成为您的万能代理。如果每个服务都开始添加高容量流而没有保留计划,那么“简单”的选择就会变成一个过载的内存日志存储。
当故障处理是核心时,选择专用代理
Kafka、RabbitMQ、Pulsar、NATS JetStream 和云队列服务的存在是因为消息传递很快就会变得复杂。
当您需要以下功能时,使用专用代理:
- 以周、月或年计的长保留期。
- 远大于内存的消息历史。
- 代理内置的死信队列和重试策略。
- 延迟投递、优先级、路由键、交换器或主题分区。
- 专为消息传递设计的跨区域复制模式。
- 多个独立消费者组重放相同的事件历史。
- 围绕延迟、偏移量、再平衡和审计的更强大工具。
Kafka 通常更适合高容量事件管道和可重放日志。RabbitMQ 通常更适合复杂的路由、确认和工作队列。当您想要托管的持久性和简单的操作边界时,云队列通常更好。
Redis 流可以处理有用的生产工作负载,但它仍然是 Redis。其数据以内存为中心,必须理解其持久性设置,并且其代理功能有意比专用系统更小。
一个具体的决策方法
在选择 Redis 之前,先问这些问题:
- 消费者能否在不丢失数据的情况下错过消息?
- 断开的消费者是否需要追赶?
- 消息需要保留多长时间?
- 保留的消息数据能否舒适地放入 Redis 内存?
- 工作进程是否能安全地处理重复消息?
- 您是否需要死信队列、延迟重试、优先级或路由规则?
- 此流量是否会干扰 Redis 缓存或会话?
如果丢失消息是可接受的,发布/订阅可能就足够了。
如果消费者需要追赶且保留期有限,流可能就足够了。
如果消息数据需要长期保留、多个团队重放、复杂的代理行为或强大的操作分离,请使用专用代理。
示例:缓存失效
一个应用程序将产品页面存储在本地进程内存和 Redis 中。当产品更改时,管理服务发布:
PUBLISH cache:invalidate product:123
每个订阅了 cache:invalidate 的 Web 实例都会删除其本地副本。如果一个 Web 实例错过了消息,其本地条目仍有五分钟的 TTL,并且它还会在下一个请求时检查产品版本字段。发布/订阅是可行的,因为存在恢复路径。
在这里使用 Kafka 可能会增加比价值更多的操作负担。
示例:后台电子邮件任务
用户注册后,您需要发送一封欢迎电子邮件。如果工作进程宕机一分钟,任务仍必须稍后发送。发布/订阅不适合。
Redis 流可以工作:
XADD email:jobs MAXLEN ~ 100000 * job_id abc123 type welcome user_id 42
工作进程通过消费者组读取,发送电子邮件,将 job_id 记录为已完成,并调用 XACK。监控器检查待处理任务并回收旧任务。这对于一个适度的内部队列来说是合理的。
如果电子邮件投递变得大规模,需要延迟重试、死信处理、按客户速率限制和丰富的操作仪表板,那么专用队列开始看起来更好。
示例:审计事件
审计事件通常需要持久性、搜索、保留,有时还需要法律或合规处理。Redis 流可能作为短缓冲区有用,但 Redis 不应是最终的审计存储。使用专为保留和审查设计的持久化日志、数据库、对象存储管道或托管事件服务。
如果您选择 Redis 的操作注意事项
对于发布/订阅:
- 配置
client-output-buffer-limit pubsub。 - 使用专用的订阅者连接。
- 构建重新连接和重新订阅行为。
- 将消息视为提示,而非持久化事实。
对于流:
- 使用
MAXLEN、MINID或显式修剪设置保留策略。 - 监控待处理条目。
- 使消费者幂等。
- 仅在任务成功后使用
XACK。 - 规划如何认领和重试停滞的消息。
- 监控内存、持久性和复制延迟。
当您选择 Redis 中与任务匹配的部分时,Redis 是一个优秀的消息代理。发布/订阅是一个实时信号。流是一个有界的持久化日志。两者都不应仅仅因为 Redis 已经在运行而被选择,但当它们的故障模型与您的应用程序匹配时,两者都可以是最简单的正确答案。
令人不安的中间地带
许多团队处于中间地带:发布/订阅太容易丢失,Kafka 感觉太大,RabbitMQ 感觉是另一个需要操作的系统。Redis 流在那里可以是一个很好的答案,但前提是您将其视为真正的队列,而不是神奇的列表。
一个健康的流设计需要围绕这些细节的所有权:
- 谁创建流和消费者组?
- 预期有多少消费者?
- 在消息被认领之前,最大待处理时间是多少?
- 重复失败后会发生什么?
- 保留多少流历史?
- 什么仪表板或警报显示增长中的延迟?
没有这些答案,流可能会静默失败。工作进程可能读取消息并在 XACK 之前崩溃,导致条目永远待处理。另一个工作进程可能永远不会认领它们。流长度可能持续增长,因为没有人配置修剪。Redis 内存上升,但应用程序团队认为“队列是持久的”,所以他们直到实例受到压力才注意到。
一个简单的工作进程通常应执行此循环:
读取一个小批次
幂等地处理每条消息
仅确认成功的消息
定期检查待处理消息
认领过时的待处理消息
根据保留策略修剪
这比发布/订阅更多工作,而这正是关键。持久性总是将复杂性转移到某处。Redis 流保持代理端相当小,但应用程序仍然拥有重试、死信行为和幂等性。
如果团队中没有人想要拥有这些细节,那么托管队列从长远来看可能更便宜,即使它在第一天看起来更重。最好的代理不是基准测试中最快的那个。而是您的团队在凌晨 3 点无需猜测就能操作其故障行为的那个。