Redis Stream 深入理解:它到底解决了什么问题

在 Redis 5.0 之前,如果你想用 Redis 做"消息",基本绕不开 List 或 Pub/Sub。List 可以做阻塞消费,但消息一旦被取走就彻底消失,消费者挂了就是事故;Pub/Sub 更直接,消息不落地,订阅者一掉线,消息就像没发生过一样。这两种模型都不具备可靠消息系统的核心能力:可追溯、可确认、可重试。

Redis Stream 的出现,本质上就是 Redis 官方承认了一件事:Redis 需要一个真正的日志型消息结构

Stream 的核心设计非常像 Kafka:所有消息只追加、不修改,每条消息都有一个全局有序的 ID。这个 ID 由时间戳和序号组成,例如:

bash 复制代码
1700000000000-0
1700000000000-1

Redis 保证 ID 单调递增,这一点非常关键,因为它让 Stream 天然支持范围查询、顺序消费和断点恢复。

向 Stream 中写入消息使用的是 XADD

bash 复制代码
XADD mystream * user alice action login

* 表示让 Redis 自动生成 ID。每条消息不是一个简单字符串,而是一组 field-value,这一点和 List 最大的不同在于:Stream 的 value 是结构化的

真正让 Stream 成为"消息队列"的,并不是 XADD,而是 消费组(Consumer Group)。消费组解决的是"同一条消息只能被一个消费者处理,并且必须被确认"。

你可以先创建一个消费组:

bash 复制代码
XGROUP CREATE mystream group1 0

0 表示从头开始消费历史消息。

消费者使用 XREADGROUP 拉消息:

bash 复制代码
XREADGROUP GROUP group1 c1 COUNT 1 BLOCK 0 STREAMS mystream >

这里有一个非常容易被忽略但极其重要的点:

当这条消息被投递给消费者 c1 时,它并没有从 Stream 中删除 ,而是被放进了消费组的一个结构里,叫 PEL(Pending Entries List)

PEL 的含义是:已投递,但尚未确认的消息

只要你没 XACK,Redis 就认为这条消息"可能没处理完"。

bash 复制代码
XACK mystream group1 1700000000000-0

ACK 之后,这条消息才会从 PEL 中移除。但注意:消息本身依然留在 Stream 里,只是这个消费组不再关心它。

PEL 的存在,直接解决了 List 和 Pub/Sub 永远解决不了的问题:
消费者宕机怎么办?

假设消费者 c1 读到一条消息,还没来得及处理就挂了,这条消息就会一直躺在 PEL 里。你可以通过:

bash 复制代码
XPENDING mystream group1

看到有哪些"卡住"的消息,然后用:

bash 复制代码
XCLAIM mystream group1 c2 60000 1700000000000-0

把超过 60 秒没处理的消息,抢给另一个消费者 c2

这一套机制,本质上就是 Redis 自己实现的 at-least-once 消费模型

从内部实现来看,Stream 并不是一个简单的链表。Redis 使用的是 Radix Tree(基数树) 来存储消息 ID 和内容,这让它在 ID 有序的前提下,依然可以高效做区间扫描,比如:

bash 复制代码
XRANGE mystream 1700000000000-0 +

这也是为什么 Stream 可以非常自然地支持"重放历史消息",而 List 和 Pub/Sub 做不到。

需要注意的是,Stream 不会自动删除消息。如果你只消费、不裁剪,Stream 会无限增长:

bash 复制代码
XTRIM mystream MAXLEN ~ 100000

这里的 ~ 是近似裁剪,Redis 会在性能和精确度之间做权衡。

还有一个实践中的坑:PEL 一定要被清理。如果消费者逻辑有问题,消息一直不 ACK,PEL 会越来越大,最后看起来像"没消息可消费",但实际上全卡在 PEL 里。

相关推荐
2301_8180084412 小时前
如何删除ASM中的数据文件_ALTER DISKGROUP DROP FILE彻底清除
jvm·数据库·python
IT界的老黄牛12 小时前
MySQL 磁盘告警 1.2TB:从衣柜原理到 gh-ost 卧底,一次释放 540GB 的实战复盘
运维·数据库·mysql
deviant-ART12 小时前
MySQL 实战:如何根据 ID 将表 B 的字段更新到表 A
数据库·mysql
2401_8987176613 小时前
mysql如何进行全量数据库备份_mysqldump工具的使用技巧
jvm·数据库·python
搬码后生仔13 小时前
【navicat不安装sql server直接远程连接服务器数据库】
运维·服务器·数据库
@小柯555m13 小时前
MySql(基础操作符--用where过滤空值练习)
数据库·sql·mysql
m0_7485548113 小时前
SQL注入的安全架构设计_将数据库置于内网隔离区
jvm·数据库·python
KeyonY13 小时前
车联网规则引擎设计之热更新与版本管理
redis·golang·车联网
许彰午13 小时前
CacheSQL:一个面向政务系统的内存缓存数据库中间件
java·数据库·缓存·中间件·面试·开源软件·政务
iAm_Ike13 小时前
怎么关闭MongoDB不需要的HTTP管理接口及REST API
jvm·数据库·python