Stream更省内存,因其采用拉模型共享存储、listpack+radix tree压缩、避免为每个消费者复制缓冲区,而Pub/Sub推模型需为慢消费者独占缓冲区且消息不落地。

为什么Stream能比Pub/Sub更省内存
Pub/Sub 的内存压力主要来自三块:每个订阅连接独占的 client-output-buffer-limit pubsub 缓冲区、频道名在 pubsub_channels 字典里的重复存储、以及消息完全不落地导致的“只能靠消费速度硬扛”。而 Stream 把消息写进键值对,用 listpack + radix tree 压缩存储,同一份消息被多个消费者读取时共享底层数据,不为每个连接复制一份缓冲区。
关键区别在于:Pub/Sub 是“推模型”,服务端必须为每个慢消费者维持独立输出队列;Stream 是“拉模型”,消费者自己决定读哪条、读多快,服务端只存一份原始数据。
迁移时必须改掉的 Pub/Sub 习惯
直接把 SUBSCRIBE 换成 XREADGROUP 不行,语义完全不同。常见踩坑点:
- 别再用
PUBLISH channel message—— 改用XADD stream_key * field value,注意*自动生成 ID,不要手拼时间戳 - 别让每个服务都
XREAD同一个 stream —— 应创建独立 consumer group:XGROUP CREATE stream_name group_name $ MKSTREAM,否则无法追踪各自消费位点 - 别忽略
XPENDING—— 消费失败的消息会卡在 pending list,不手动XACK或XCLAIM就一直占内存 - 别以为 Stream 没有缓冲区 ——
client-output-buffer-limit normal仍适用,但影响的是命令响应缓冲,不是消息体本身
Stream 内存可控的关键配置项
Stream 本身不依赖 client-output-buffer-limit pubsub,但仍有三个地方必须设限:
-
stream-node-max-bytes(默认 4096):控制每个 radix tree 节点最大字节数,调小可减少碎片,但过小会增加节点数和指针开销;物联网设备状态类小消息建议保持默认 -
stream-node-max-entries(默认 100):单个 listpack 最多存几条消息,调大能提升顺序读性能,但会拖慢XTRIM和内存释放;高频短消息场景可设为 500 -
XTRIM stream_name MAXLEN ~ 10000:必须显式执行,否则 stream 无限增长;~表示近似裁剪,比精确MAXLEN更轻量
注意:CONFIG SET stream-node-max-bytes 2048 是动态生效的,但 XTRIM 必须在业务代码里定时触发,不能只靠配置。
上线前必须验证的内存拐点
Stream 看似省内存,但错误用法反而更耗:比如用一个 stream 存所有业务事件(订单、支付、日志混在一起),会导致 radix tree 前缀熵值高、压缩率暴跌;或者 consumer group 过多且长期不消费,pending list 积压消息 ID 占用额外内存。
上线前跑个真实压测:
- 用
MEMORY USAGE stream_name查单个 stream 实际开销,对比同等消息量的 Pub/SubCLIENT LIST中所有omem总和 - 用
XINFO GROUPS stream_name看各 group 的pel-count(pending 数),>100 就要检查消费逻辑是否卡在外部 I/O - 用
INFO memory | grep used_memory_human观察整体内存曲线,特别注意 stream 写入后是否出现非线性上涨——那大概率是listpack分裂或 pending list 膨胀
最易被忽略的是:Stream 的消息 ID 是字符串,如果用 XADD key 1672531200000-0 field value 手动指定毫秒时间戳,比自动生成的 1672531200000-1 多占 3–5 字节/条,亿级消息就是几十 MB 差异。











