Files
cs-note/hhs/MQ/07-主流MQ对比/26-NATS-NSQ-Redis-Streams.md
T
2026-05-24 20:51:06 +08:00

9.2 KiB
Raw Blame History

tags, create time
tags create time
MQ
NATS
NSQ
Redis Streams
消息队列
轻量级MQ
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 最大的增强。

关联笔记