2. Redis Streams核心数据结构

Redis Stream 是 Redis 5.0 引入的日志型数据结构,它把“消息”组织成一条只追加(append-only)的时间线。理解 Stream 的关键,不是把它当成一个普通队列,而是把它看成一串带唯一编号的条目(Entry),每个条目内部又是一个字段-值集合。只有把 Entry ID、字段值、顺序和内存模型这四件事串起来,后续的消费者组、ACK、PENDING 才有稳固的认知基础。

Stream 的组成:条目、ID 与字段值

一个 Stream 由若干条目组成,每个条目包含三部分:Entry ID、字段-值对(field-value pairs),以及可选的删除标记。Entry ID 是形如 毫秒时间戳-序号 的字符串,例如 1718000000000-0。前半段是毫秒级 Unix 时间戳,后半段是同一毫秒内的自增序号。这个设计让 ID 天然单调递增,从而保证消息顺序。

字段值部分类似一个扁平的哈希:一个条目可以携带多个字段,例如 sensor=temp、value=23.5、unit=C。字段名和字段值都是二进制安全的字符串,Redis 不解释其含义,只负责存储和返回。因此 Stream 既能承载简单事件,也能承载结构化业务消息。

Stream 条目结构与 ID 递增关系 时间/ID 递增方向 → Entry ID: 1718000000000-0 sensor = temp value = 23.5 unit = C 字段-值对(扁平哈希) Entry ID: 1718000000000-1 sensor = temp value = 23.7 unit = C 同毫秒内序号 +1 Entry ID: 1718000000001-0 sensor = temp value = 23.6 unit = C 毫秒进位,序号归零 内存模型要点 • 条目按 ID 顺序存储于紧凑的 radix tree / listpack 结构中,尾部追加成本低 • 每个条目独立保存字段-值对,不共享字段名,字段多时内存开销需关注

消息顺序:ID 即顺序

Stream 的顺序由 Entry ID 决定,而不是由插入的物理位置决定。Redis 保证新写入的 ID 一定大于已有最大 ID,因此 XRANGE 按 ID 升序返回时,就是消息的真实发生顺序。这一点与 List 不同:List 的 LPUSH/RPUSH 只决定两端位置,而 Stream 的 ID 自带全局单调性。

实际使用中,XADD 可以显式指定 ID,也可以使用 * 让 Redis 自动生成。自动生成时,Redis 取当前毫秒时间戳,若与上一条 ID 的毫秒部分相同,则序号加一;若时间戳更大,则序号从 0 开始。这解释了为什么同一毫秒内连续写入会得到 ...-0、...-1、...-2 这样的 ID。

显式指定 ID 时必须大于当前最大 ID,否则会报错 ERR The ID specified in XADD is equal or smaller than the target stream top item。这是 Stream 只追加语义的直接体现。

XADD 写入:从命令到条目

XADD 的基本语法是 XADD key [NOMKSTREAM] [MAXLEN|MINID ...] <ID> field value [field value ...]。最简形式如下:

XADD sensor:temp * sensor temp value 23.5 unit C
# 返回: "1718000000000-0"

XADD sensor:temp * sensor temp value 23.7 unit C
# 返回: "1718000000000-1"

XADD sensor:temp * sensor temp value 23.6 unit C
# 返回: "1718000000001-0"

这里 * 表示由 Redis 生成 ID。返回值就是新条目的完整 ID,工程中常把它作为消息的唯一标识回传给上游。若希望控制 Stream 长度,可以加上 MAXLEN:

XADD sensor:temp MAXLEN ~ 1000 * sensor temp value 23.8 unit C

~ 表示近似裁剪,Redis 可能在节点边界处多保留少量条目,换取更高的写入效率;去掉 ~ 则精确裁剪到 1000 条。裁剪只删除旧条目,不影响 ID 的单调递增。

XRANGE 读取:按 ID 区间取条目

XRANGE 用于按 ID 范围读取,语法为 XRANGE key start end [COUNT count]。其中 - 表示最小 ID,+ 表示最大 ID。例如读取全部条目:

XRANGE sensor:temp - +
1) 1) "1718000000000-0"
   2) 1) "sensor"
      2) "temp"
      3) "value"
      4) "23.5"
      5) "unit"
      6) "C"
2) 1) "1718000000000-1"
   2) 1) "sensor"
      2) "temp"
      3) "value"
      4) "23.7"
      5) "unit"
      6) "C"

返回结构是“条目数组”,每个条目又是“ID + 字段值扁平数组”。注意字段值在返回时是交替出现的,解析时需要按两个一组读取。若只想要某一段,可以指定具体 ID:

XRANGE sensor:temp 1718000000000-1 1718000000001-0
# 返回 ID 在 [1718000000000-1, 1718000000001-0] 闭区间内的条目

XRANGE 的区间是闭区间,start 和 end 都会被包含。若 start 大于 end,返回空数组。COUNT 用于限制返回条数,适合分页读取。

内存模型与工程注意点

Stream 在内存中并不是简单的链表。Redis 使用 radix tree(rax)作为顶层索引,每个节点挂载一个 listpack 来存储若干连续条目。listpack 是一种紧凑的连续内存结构,适合小条目批量存储,能显著减少指针开销。当单个 listpack 超过配置阈值(如 stream-node-max-entries、stream-node-max-bytes)时,会分裂成新的节点。

这种设计带来两个工程影响。第一,按 ID 范围读取时,radix tree 能快速定位到起始节点,再在 listpack 内顺序扫描,因此 XRANGE 的范围查询效率较高。第二,条目中的字段名在每个条目里独立存储,如果字段名很长且条目很多,内存放大效应会很明显。实践中可以通过缩短字段名、控制单条字段数量来缓解。

操作命令关键参数典型返回
写入XADDkey、ID、field value新条目 ID
范围读取XRANGEstart、end、COUNT条目数组
反向读取XREVRANGEend、start、COUNT倒序条目数组
查看长度XLENkey条目数量
不要用 XRANGE 做实时消费。XRANGE 是拉取式读取,不会阻塞等待新消息;实时消费应使用 XREAD 的 BLOCK 模式或消费者组。XRANGE 更适合历史回放、审计和按 ID 精确查询。

把以上内容串起来:Stream 是一条由 Entry ID 严格排序的追加日志,每个条目携带扁平字段值;XADD 负责在尾部追加并返回 ID,XRANGE 负责按 ID 闭区间读取;底层 radix tree + listpack 决定了范围查询高效、但字段名重复存储会带来内存成本。掌握这四点,就为后续消费者组、PENDING 列表和消息确认打下了基础。