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 既能承载简单事件,也能承载结构化业务消息。
消息顺序: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。
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 的范围查询效率较高。第二,条目中的字段名在每个条目里独立存储,如果字段名很长且条目很多,内存放大效应会很明显。实践中可以通过缩短字段名、控制单条字段数量来缓解。
| 操作 | 命令 | 关键参数 | 典型返回 |
|---|---|---|---|
| 写入 | XADD | key、ID、field value | 新条目 ID |
| 范围读取 | XRANGE | start、end、COUNT | 条目数组 |
| 反向读取 | XREVRANGE | end、start、COUNT | 倒序条目数组 |
| 查看长度 | XLEN | key | 条目数量 |
把以上内容串起来:Stream 是一条由 Entry ID 严格排序的追加日志,每个条目携带扁平字段值;XADD 负责在尾部追加并返回 ID,XRANGE 负责按 ID 闭区间读取;底层 radix tree + listpack 决定了范围查询高效、但字段名重复存储会带来内存成本。掌握这四点,就为后续消费者组、PENDING 列表和消息确认打下了基础。