Redis Stream 是 WebSocket 历史消息存储最优解,因其支持时间序、消费者组、消息回溯;而 Pub/Sub 丢消息、LIST 缺乏多消费与精确追踪能力。

WebSocket 本身不保存消息,所有“历史记录”都得靠外部存储。Redis Stream 是目前最匹配的方案——它天然支持时间序、消费者组、消息回溯,比用 ZSET 或 LIST 手动维护靠谱得多。
为什么必须用 Redis Stream 而不是 Pub/Sub 或 LIST
Pub/Sub 是纯广播,消息一发即焚,断连就丢;LIST 虽可存,但没法按时间范围查、不支持多消费者读同一份数据、没有消息 ID 追踪能力。而 Stream 提供:
-
XADD自动打上毫秒级时间戳和唯一 ID(如1640995200000-0),天然适合消息排序 -
XREADGROUP支持多服务实例各自消费同一批消息(比如网关 A 和网关 B 都要写历史),互不干扰 -
XTRIM可自动裁剪过期消息,避免无限膨胀(例如只保留最近 10 万条) - 客户端断线重连后,能用
XREAD ... ID精确续读,不会漏或重
Stream key 设计与写入时机
别把所有消息塞进一个 stream:all。按业务维度分 key,常见做法是:stream:chat:{roomId} 或 stream:dm:{userIdA}:{userIdB}。写入必须在 WebSocket 消息真正“送达成功”后触发——不是收到就写,而是确认对方在线或已落库再写。
- 用户 A 发送消息 → 先查
online:usersSet 判断 B 是否在线 - 若在线,直接
session.sendMessage()推送;若离线,跳过推送,只执行XADD stream:dm:1001:2002 * content "xxx" timestamp "1716471382" - 务必用
*让 Redis 自动生成 ID,别手动生成(易冲突、难排序)
WebSocket 连接建立后如何拉取历史消息
前端连接 WebSocket 后,第一件事不是等推送,而是发一个初始化请求(比如 STOMP 的 SUBSCRIBE 带参数,或自定义 {"type":"fetch_history","from_id":"1640995200000-0","count":50})。后端收到后调用:
XREVRANGE stream:dm:1001:2002 1640995200000-0 - COUNT 50
注意三点:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 用
XREVRANGE而非XRANGE,保证最新消息在前(前端列表 append 更自然) -
from_id应该是上一次最后一条消息 ID,不是时间戳;Stream ID 比时间戳更精确 - 如果用户首次登录,
from_id可传+(最大 ID),再加COUNT限制数量,避免全量扫描
消费者组 + ACK 机制防消息丢失
当有多个网关实例时,不能让每个实例都独立 XREAD 同一个 Stream——会重复消费。必须用消费者组:
XGROUP CREATE stream:dm:1001:2002 gateway_group $ MKSTREAM
然后各实例用:
XREADGROUP GROUP gateway_group instance_1 STREAMS stream:dm:1001:2002 >
关键点:
-
>表示只读新消息;若需重放,改用具体 ID(如1640995200000-0) - 读到消息后,必须立刻
XACK stream:dm:1001:2002 gateway_group {id},否则下次XREADGROUP还会返回它 - 未
ACK的消息会在PENDING列表里滞留,默认 60 秒后被其他实例抢走——这个超时值要结合业务重试逻辑设好
Stream 不是银弹:它不支持模糊搜索、不存结构化字段(比如 senderId 需手动塞进 value JSON 里)、单个 Stream 过大时 XLEN 会变慢。真要查“张三发的所有图片”,还得靠 MySQL + Redis 双写兜底。

















