9.2 KiB
tags, create time
| tags | create time | ||||||
|---|---|---|---|---|---|---|---|
|
2026-05-24 19:52 |
NATS / NSQ / Redis Streams
概述
并不是所有场景都需要 Kafka 这样的重量级消息平台。微服务间的轻量通信、已有 Redis 基础设施上的流处理、去中心化的简单队列——这些场景下 NATS、NSQ 和 Redis Streams 各有胜场。本文分别解析三者的设计理念和核心能力,最后给出选型建议。
正文
NATS:极简高性能的消息系统
NATS 由 Derek Collison 创建,设计哲学是"做一件事并做到极致"——提供简单、快速、可靠的消息通信。NATS Server 是一个单一的 Go 可执行文件,无外部依赖,启动即用。
Subject-Based 消息模型:NATS 使用 Subject(主题)路由消息,支持通配符匹配(orders.* 匹配 orders.create,orders.> 匹配 orders.create.us 等任意层级)。发送者只需指定 Subject,NATS 自动将消息路由到所有匹配的订阅者。
核心特点:
- 极致性能:单节点可支撑每秒千万级消息,微秒级延迟
- At-Most-Once 语义:默认不持久化,消息发出去就不管了,适合实时性要求高但允许丢消息的场景
- 自适应发现:客户端无需知道完整的集群拓扑,连接任意节点即可自动发现
NATS JetStream:这是 NATS 的持久化层,补齐了消息持久化和流式消费的短板。JetStream 提供:
- 消息持久化到磁盘
- 消费者可以回放历史消息
- At-Least-Once 和 Exactly-Once 语义支持
- 消费者组(Queue Groups)和消息确认机制
graph LR
Publisher["Publisher"] -->|"Subject: orders.new"| NATS["NATS Server Cluster"]
NATS -->|"实时投递"| Sub1["Subscriber 1"]
NATS -->|"实时投递"| Sub2["Subscriber 2"]
NATS -->|"持久化"| JS["JetStream 存储"]
JS -->|"回放消费"| Sub3["JetStream Consumer"]
style NATS fill:#4A90D9,color:#fff
style JS fill:#F5A623,color:#fff
style Publisher fill:#6EC1E0,color:#fff
style Sub1 fill:#6EC1E0,color:#fff
style Sub2 fill:#6EC1E0,color:#fff
style Sub3 fill:#6EC1E0,color:#fff
[!question] NATS 和 RabbitMQ 都是"传统的"消息中间件,本质区别在哪? NATS 的核心是"消息路由器",设计优先级是速度和简洁,持久化是后加的能力(JetStream)。RabbitMQ 的核心是"消息代理",设计优先级是投递保证和灵活路由(Exchange/Binding),持久化是一等公民。简单说:NATS 是跑车,RabbitMQ 是卡车。
NSQ:去中心化的实时消息平台
NSQ 由 Bitly 开发,最大特点是去中心化——没有中心化的元数据管理节点,每个 nsqd 实例独立运行,通过 nsqlookupd 实现拓扑发现。
架构组件:
- nsqd:消息守护进程,负责接收、队列和投递消息。每个 nsqd 独立管理自己 Topic 和 Channel(类似 Consumer Group),数据存储在本地。
- nsqlookupd:拓扑发现服务,nsqd 启动时向 nsqlookupd 注册自己,消费者通过查询 nsqlookupd 发现某个 Topic 在哪些 nsqd 上。nsqlookupd 之间互不通信,可以部署多个实现高可用。
消息存储:NSQ 采用内存 + 磁盘的混合策略。消息先写入内存队列,内存队列达到阈值后溢写到磁盘。这种设计在性能和持久性之间取了折中。
特点:去中心化意味着没有单点故障,但也不支持消息回溯——消费过的消息就没了(At-Most-Once)。适合日志收集、实时通知等对消息可靠性要求不高的场景。
graph TB
Producer1["Producer"] -->|"发布消息"| NSQD1["nsqd 1"]
Producer1 -->|"发布消息"| NSQD2["nsqd 2"]
NSQD1 -->|"注册"| Lookupd["nsqlookupd"]
NSQD2 -->|"注册"| Lookupd
Lookupd -->|"拓扑发现"| Consumer["Consumer"]
Consumer -->|"消费消息"| NSQD1
Consumer -->|"消费消息"| NSQD2
style Lookupd fill:#4A90D9,color:#fff
style NSQD1 fill:#F5A623,color:#fff
style NSQD2 fill:#F5A623,color:#fff
style Producer1 fill:#6EC1E0,color:#fff
style Consumer fill:#6EC1E0,color:#fff
Redis Streams:基于 Redis 的流数据结构
Redis 5.0 引入的 Streams 是一个功能完备的流数据结构,基于 Radix Tree(基数树)实现内存中的高效存储。它让 Redis 从"缓存+简单队列"进化为一个轻量级消息流平台。
核心命令:
XADD key ID field value:向 Stream 追加消息,返回自动生成的消息 ID(时间戳-序号)XREAD COUNT 10 BLOCK 5000 STREAMS key ID:从指定位置读取消息,支持阻塞等待XREADGROUP GROUP mygroup consumer1 COUNT 10 BLOCK 5000 STREAMS key >:消费者组模式消费
Consumer Group 支持:Redis Streams 原生支持消费者组,多个消费者共享一个 Stream,每条消息只被组内的一个消费者处理。通过 Pending Entries List(PEL)跟踪已投递但未确认的消息,支持消息重试和死信处理。
graph LR
Producer["Producer"] -->|"XADD"| Stream["Redis Stream Radix Tree"]
Stream -->|"XREADGROUP"| CG["Consumer Group"]
CG -->|"分配消息"| C1["Consumer 1"]
CG -->|"分配消息"| C2["Consumer 2"]
C1 -->|"XACK"| Stream
C2 -->|"XACK"| Stream
style Stream fill:#D0021B,color:#fff
style CG fill:#F5A623,color:#fff
style Producer fill:#6EC1E0,color:#fff
style C1 fill:#6EC1E0,color:#fff
style C2 fill:#6EC1E0,color:#fff
三者对比
| 维度 | NATS(JetStream) | NSQ | Redis Streams |
|---|---|---|---|
| 持久化 | JetStream 支持,可配置副本数 | 内存+磁盘混合,不支持回溯 | 基于 AOF/RDB 持久化,可配置 |
| 吞吐量 | 极高(千万级/秒) | 高(百万级/秒) | 中等(十万级/秒,受限于单线程) |
| 延迟 | 微秒级 | 毫秒级 | 毫秒级 |
| 运维复杂度 | 低,单一二进制 | 低,组件少但 nsqlookupd 需独立部署 | 低,如果已有 Redis 则零额外成本 |
| 消费语义 | At-Most-Once / At-Least-Once / Exactly-Once | At-Most-Once | At-Least-Once(Consumer Group + ACK) |
| 消息回溯 | 支持 | 不支持 | 支持(按 ID 回溯) |
| 适用场景 | 微服务通信、IoT、实时事件 | 日志收集、实时通知 | 已有 Redis 基础设施上的轻量队列 |
轻量级 MQ 选型建议
[!question] Redis Streams 功能这么强大,为什么还需要专门的 MQ? Redis Streams 功能确实很全面,但它的吞吐受限于 Redis 单线程模型,大数据量下会成为瓶颈。而且 Redis 本质是内存数据库,Streams 只是它的一个数据结构——如果消息量把内存撑爆,会影响 Redis 上其他业务。更重要的是,专门的 MQ(如 NATS JetStream)在集群扩展、数据分片、消费语义保证上做得更深入。简单说:Redis Streams 是"顺带能做消息队列",NATS/Kafka 是"专门为消息队列而生"。
什么时候选 NATS:微服务间高频通信、IoT 设备消息上报、需要极低延迟的实时事件系统。NATS 的 Go 原生实现和零依赖特性,让它特别适合云原生和 Kubernetes 环境。
什么时候选 NSQ:日志收集管道、实时通知推送、对消息可靠性要求不高的异步任务分发。去中心化设计让它在需要快速搭建、不想维护额外基础设施时很香。
什么时候选 Redis Streams:团队已经深度使用 Redis,需要在不引入新组件的前提下获得消息队列能力;消息量不大(日均千万级以内);对延迟和吞吐要求不极端。
Go 代码示例:NATS JetStream
// 连接 NATS 并使用 JetStream 发布和消费
nc, _ := nats.Connect("nats://localhost:4222")
defer nc.Close()
js, _ := nc.JetStream()
// 创建 Stream(持久化存储单元)
js.AddStream(&nats.StreamConfig{
Name: "ORDERS",
Subjects: []string{"orders.>"}, // 订阅所有 orders. 开头的 Subject
Storage: nats.FileStorage, // 持久化到磁盘
Replicas: 3, // 3 副本
})
// 发布消息
_, err := js.Publish("orders.new", []byte(`{"orderId":"1001","item":"laptop"}`))
if err != nil {
fmt.Println("发布失败:", err)
}
// 消费消息(Push 模式)
sub, _ := js.Subscribe("orders.>", func(msg *nats.Msg) {
fmt.Println("收到消息:", string(msg.Data))
msg.Ack() // 确认消费
}, nats.Durable("order-processor"), // 持久化消费者名,重启后从上次位点继续
nats.ManualAck(), // 手动确认模式
)
defer sub.Unsubscribe()
select {} // 阻塞等待
代码解析:先通过 AddStream 创建一个名为 ORDERS 的 JetStream Stream,配置了文件存储和 3 副本。Publish 将消息发送到 orders.new Subject,该 Subject 匹配 Stream 的 orders.> 通配符。Subscribe 使用 Push 模式消费,Durable 参数让消费位点持久化——即使消费者重启也能从断点继续。这是 NATS JetStream 相比原生 NATS 最大的增强。