174 lines
9.2 KiB
Markdown
174 lines
9.2 KiB
Markdown
|
|
---
|
|||
|
|
tags: [MQ, NATS, NSQ, Redis Streams, 消息队列, 轻量级MQ]
|
|||
|
|
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)和消息确认机制
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
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)。适合日志收集、实时通知等对消息可靠性要求不高的场景。
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
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)跟踪已投递但未确认的消息,支持消息重试和死信处理。
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
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
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// 连接 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 最大的增强。
|
|||
|
|
|
|||
|
|
## 关联笔记
|
|||
|
|
|
|||
|
|
- [[07-主流MQ对比/22-Kafka|Kafka]]
|
|||
|
|
- [[07-主流MQ对比/24-RocketMQ|RocketMQ]]
|
|||
|
|
- [[07-主流MQ对比/25-Apache-Pulsar|Apache Pulsar]]
|
|||
|
|
- [[02-消息模型/3-MQ-消息模型|MQ 消息模型]]
|
|||
|
|
- [[07-主流MQ对比/27-MQ-选型对比|MQ 选型对比]]
|
|||
|
|
- [[12-架构与实战/47-MQ-与微服务|MQ 与微服务]]
|