Redis Streams 实战:构建消息队列与事件流

Redis Streams 是 Redis 5.0 引入的流数据类型,专门用来构建消息队列与事件流系统。它既有 Kafka 般的可持久化、消费者组能力,又保留了 Redis 的极低延迟,非常适合做轻量级实时消息总线。

一、为什么需要 Redis Streams

在做异步解耦、事件通知时,很多团队第一时间想到 Kafka,但它部署与运维成本不低。而项目里往往已经跑着 Redis 做缓存——如果能用 Redis 直接承载中小规模的消息流,就省掉一套独立中间件。问题在于:Redis 早期的数据结构都不太适合做”正经”的队列。

1.1 老方案的局限

用 List 的 LPUSH / BRPOP 可以做队列,但缺少消费者组与确认机制;Pub/Sub 更是”发后既忘”,订阅者离线消息直接丢失。Streams 正是为填这个坑而生。

方案持久化消费者组回溯读取主要局限
List + LPUSH/BRPOP有无(需自管)难无法多消费者分摊,无 ACK 确认
Pub/Sub无无不支持订阅者离线即丢消息
Streams有有支持单流写吞吐低于 Kafka

二、核心命令速查

Streams 的命令围绕”追加—读取—确认—裁剪”四个动作展开,下面这张表覆盖了日常 90% 的场景。

命令作用
XADD追加一条消息,返回消息 ID
XREAD读取消息,支持阻塞等待新消息
XRANGE按 ID 范围读取(用于回溯历史)
XGROUP创建 / 管理消费者组
XREADGROUP消费者组内读取,自动标记 pending
XACK确认消息已处理
XPENDING查看未确认(pending)消息
XCLAIM把超时消息转移给其它消费者
XTRIM裁剪流长度,控制内存

三、写入数据:XADD

XADD 向流追加一条消息,字段用 key-value 对表示;ID 传 * 时 Redis 自动生成”毫秒时间戳-序号”形式的 ID,保证单调递增。

消息 ID 形如 1727174400000-0:前半是毫秒时间戳,后半是同一毫秒内的自增序号。因为它随写入单调递增,天然支持按时间排序与范围回溯,这也是 Streams 能做”事件溯源”式回放的基础——你随时可以用 XRANGE 把任意时间段的事件重新读出来重放。

# 追加一条订单创建事件,* 让 Redis 自动生成 ID
XADD orders * event "created" order_id 10231 user_id 8848 amount 199.00
# 返回示例:1727174400000-0

# 读取整条流(按 ID 范围)
XRANGE orders - +

四、读取数据:XREAD 与阻塞读取

普通 XREAD 适合回溯历史;加上 BLOCK 后则变成”有消息立即返回,否则等待”,等价于阻塞队列的 BRPOP,是实时消费的关键。

# 从开头读取最多 2 条(用于历史回放)
XREAD COUNT 2 STREAMS orders 0

# 阻塞读取最新消息,$ 表示当前尾部;最多等待 5000ms
XREAD BLOCK 5000 COUNT 10 STREAMS orders $

五、消费者组:竞争消费与可靠确认

单个 XREAD 无法满足”多实例分摊 + 不丢消息”。消费者组(Consumer Group)解决了这个问题:组内每条消息只投递给一个消费者,处理完必须 XACK 确认,未确认的消息会进入 pending 列表,可重新投递。

这里有个关键点:XREADGROUP 拉走的消息不会立刻从流中删除,而是进入该消费者的 pending 列表。只有当 XACK 被调用,消息才真正”消费完成”。如果消费者进程中途崩溃,未 ACK 的消息会在下次被重新投递给其他健康消费者,从而做到”至少一次”投递语义——代价是业务必须幂等,这正是第八节要强调的。

# 创建消费者组 cg1,MKSTREAM 在流不存在时自动建流
XGROUP CREATE orders cg1 $ MKSTREAM

# 消费者 consumer-1 拉取新消息(> 表示"尚未投递过"的消息)
XREADGROUP GROUP cg1 consumer-1 COUNT 5 STREAMS orders >

# 业务处理成功后确认,消息才从 pending 移除
XACK orders cg1 1727174400000-0

六、完整示例:Python 订单事件流

下面用 redis-py 给出一个最小可跑的生产者 / 消费者示例。注意先 XGROUP CREATE 建组,消费时再 XACK,形成”至少一次”投递。

import redis

r = redis.Redis(host="127.0.0.1", port=6379, decode_responses=True)
STREAM = "orders"

# 生产者:追加订单事件
def produce():
    r.xadd(STREAM, {"event": "created", "order_id": 10231, "amount": 199.0})
    print("published")

# 消费者:基于消费者组拉取,处理完再 ACK
def consume():
    while True:
        resp = r.xreadgroup("cg1", "consumer-1", {STREAM: ">"}, count=5, block=5000)
        if not resp:
            continue
        for _, messages in resp:
            for msg_id, fields in messages:
                print("handle", msg_id, fields)
                r.xack(STREAM, "cg1", msg_id)  # 成功处理才确认

if __name__ == "__main__":
    produce()
    consume()

真实项目里建议在 consume() 外层包一层 try/except:处理异常时故意不调用 XACK,让消息留在 pending 等待重试;再配合第八节的 XPENDING / XCLAIM,就能构建出健壮的”自动重试闭环”,避免一条坏消息把整个消费循环拖死。

七、Redis Streams 还是 Kafka?

两者并非互斥。Streams 胜在”零额外组件、低延迟”,适合中小规模、已有 Redis 的内部事件;Kafka 胜在超大规模与强持久,适合核心交易链路。选型时先看吞吐与持久化诉求,再决定用哪把锤子。关于 Kafka 的选型与可靠性设计,可延伸阅读 Kafka 入门实战:消息队列选型与可靠性保障。

更常见的做法是”分级事件总线”:用 Kafka 扛支付、订单落库等强持久核心链路,用 Redis Streams 承载通知、审计、内部状态同步等对延迟敏感、可容忍少量重放的旁路事件,两者各取所长。这样既能守住核心链路的可靠性,又不必为每一个小需求都背上 Kafka 的运维负担。

维度Redis StreamsKafka
部署复杂度极低(已有 Redis 即可)较高(Broker + 协调组件)
单流吞吐数万 QPS数十万~百万 QPS
持久化AOF/RDB,可配置分段文件,强持久
典型场景轻量通知、内部事件流核心链路、多租户日志

八、三个常见坑与避坑清单

8.1 消息无限堆积导致内存膨胀

Streams 默认只增不删,长期运行会撑爆内存。务必设置长度上限:写入时用 MAXLEN,或定期 XTRIM 裁剪。

# 写入时直接限制长度(~ 表示近似裁剪,性能更好)
XADD orders MAXLEN ~ 10000 * event "created" order_id 10232

# 或事后裁剪,只保留最近 10000 条
XTRIM orders MAXLEN 10000

8.2 重复消费与幂等

消费者组保证”至少一次”投递:进程崩溃会让消息留在 pending 中,恢复后重新投递。业务侧必须按消息 ID 做幂等(如用 order_id 去重),否则同一订单可能被处理两次。Redis 缓存层面的穿透/击穿防护思路也值得参考,详见 Redis 缓存设计:穿透、击穿、雪崩与最佳实践。

8.3 pending 堆积成”死信”

消费者异常退出会让消息永远 pending。XPENDING 监控、XCLAIM 转移超时消息是标准解法;重试超过阈值就搬到独立死信流,避免阻塞主流程。

# 查看超过 30s 未确认的消息
XPENDING orders cg1 IDLE 30000

# 把超时消息转移给 consumer-2 重试
XCLAIM orders cg1 consumer-2 30000 1727174400000-0

九、生产落地建议

  • 始终为流设置 MAXLEN 上限或定期 XTRIM,守住内存水位。
  • 用消费者组 + XACK 实现至少一次语义,业务侧务必做幂等。
  • 用 XPENDING 监控 pending,定期 XCLAIM 清理卡住的消息。
  • 与 Kafka 不互斥:核心链路用 Kafka,轻量内部事件用 Streams。
  • 若顾虑 Redis 许可与持久化,可考虑迁移到其开源分支 Valkey,迁移路径见 Valkey 实战:Redis 许可变更后的开源替代与平滑迁移;需要实时双向推送时还能配合 WebSocket 实时通信实战 把流事件推到前端。

十、小结

Redis Streams 把”消息队列 + 事件流”的能力以极低门槛带进了你本来就有的 Redis,核心就是 XADD 写入、XREADGROUP 消费、XACK 确认、XTRIM 控长四板斧。中小规模场景下,它比引入 Kafka 更划算;用对消费者组与幂等,就能稳定撑起实时事件总线。

上一篇 SolidJS 实战:细粒度响应式前端框架
下一篇 容器内时区错乱:定时任务跑错时间的排查复盘
最新评论
Docker 网络模式详解:bridge/host 生产选型 - FSData
Docker 网络模式详解:bridge/host 生产选型 - FSData
8月20日
[…] 注意最后那条:把端口绑定到 127.0.0.1,再由宿主机上的 Nginx 反代进来,比直接 -p 80:80 暴露安全得多,也方便统一做
Vue3 + TypeScript 工程化:类型安全的前端架构 - FSData
Vue3 + TypeScript 工程化:类型安全的前端架构 - FSData
8月20日
[…] Pinia 的工程化用法,可以参考我们之前的实战文章 《Vue3 组合式 API + Pinia:状态管理从入门到实战》,那里覆盖了 Sto
2026 年最值得装的 10 个 VS Code 插件(AI/前端/后端/效率全覆盖) - FSData
2026 年最值得装的 10 个 VS Code 插件(AI/前端/后端/效率全覆盖) - FSData
8月19日
[…] 状态,点一下就能起停容器,配合我们讲过的 Docker Compose 编排 […]
Redis 缓存设计:穿透、击穿、雪崩与最佳实践 - FSData
Redis 缓存设计:穿透、击穿、雪崩与最佳实践 - FSData
8月19日
[…] 缓存三大坑的本质都是”缓存失效导致请求穿透到数据库”。记住口诀:穿透用布隆、击穿用锁、雪崩用错峰。配合合理的过期策略
GitHub Actions 实战:从零搭建 CI/CD 流水线 - FSData
GitHub Actions 实战:从零搭建 CI/CD 流水线 - FSData
8月19日
[…] Docker 镜像部署。结合我们之前写的 Docker Compose 一键编排实战,可以在 CI […]