Appearance
Redis 排查 Cheatsheet(Pub/Sub + Streams · 协作模式线上排障)
线上协作出问题时的一页速查。先查 Key 约定 → 再敲命令 → 按症状对照。 命令都标注 🔒只读安全 / ⚠️有副作用。线上务必先读后写。 配套教程:
docs/协作模式/2026-07-04-redis-pubsub-入门教程.md、docs/协作模式/2026-07-04-redis-pubsub-进阶-Streams对比与项目读码.md。
0. 先连上 Redis
bash
# 方式一(推荐):用项目现成 CLI,自动读 config.REDIS_URL
cd backend
make redis-channels # 列出正在协作的 channel
make redis-sub ARGS=<workspace_id> # 实时监听某 workspace 消息流
make redis-presence ARGS=<workspace_id> # 查在线成员 + 光标
# 方式二:裸 redis-cli(自己填 REDIS_URL)
redis-cli -u "$REDIS_URL"
# 或带库索引
redis-cli -u "$REDIS_URL" -n 0💡 排查 Pub/Sub 第一原则:Pub/Sub 消息发完即焚、
KEYS查不到,必须实时监听才能看到。看到症状时第一反应是make redis-sub挂上听。
1. 本项目 Redis Key / Channel 约定(排查入口)
排查第一步永远是知道数据存在哪个 key。协作模式涉及的全部约定:
1.1 实时协同(Pub/Sub + Hash)
| 用途 | 类型 | Key / Channel | TTL | 来源 |
|---|---|---|---|---|
| 画布实时广播频道 | Pub/Sub channel | workspace:realtime:{workspace_id} | 无(发完即焚) | redis_broadcaster.py:_CHANNEL_PREFIX |
| 在线成员/光标/选区 | Hash | workspace:presence:{workspace_id} | 120s | workspace_presence_service.py:_PRESENCE_KEY_PREFIX |
1.2 聊天续接(Streams + 辅助)
| 用途 | 类型 | Key | TTL | 来源 |
|---|---|---|---|---|
| 流式事件日志(续接核心) | Stream | chat:stream:{message_id} | 活跃期内 | resume/redis_keys.py:STREAM_PATTERN |
| 会话当前活跃消息 ID | String | chat:active:conversation:{conversation_id} | 活跃期内 | ACTIVE_CONVERSATION_PATTERN |
| 用户占用并发槽位 | ZSet | chat:active:user:{user_id} | 活跃期内 | ACTIVE_USER_PATTERN |
| 消息归属用户 | String | chat:message:{message_id}:user | 活跃期内 | MESSAGE_USER_PATTERN |
| 生成租约(谁在生成) | String | chat:generation:lease:{message_id} | 短租 | GENERATION_LEASE_PATTERN |
| 终态收敛锁(防重复落库) | String | chat:generation:finalize-lock:{message_id} | 短锁 | GENERATION_FINALIZE_LOCK_PATTERN |
| 暂停标记 | String | chat:pause:{message_id} | — | PAUSE_PATTERN |
1.3 其他
| 用途 | 类型 | Key | 来源 |
|---|---|---|---|
| 视频流活跃标记 | String | video:active_stream:{thread_id} | services/core/video_stream_guard.py |
| 续接指标 | String | chat:resume:metrics:{day}:{name}{label_suffix} | resume/metrics.py |
🔍 找 key 的通用招:不确定 key 全名时用
SCAN模糊扫(见 §6),不要用KEYS *(线上大库会阻塞)。
2. Pub/Sub 命令速查
2.1 发布 / 订阅
| 命令 | 作用 | 风险 |
|---|---|---|
SUBSCRIBE ch1 ch2 🔒 | 订阅指定频道(进入订阅态,该连接只能收) | — |
UNSUBSCRIBE [ch] 🔒 | 退订;不带参数=退订全部 | — |
PSUBSCRIBE order.* 🔒 | P=Pattern,按通配符订阅(*/?/[abc]) | — |
PUNSUBSCRIBE [pattern] 🔒 | 退订模式 | — |
PUBLISH ch msg ⚠️ | 往频道发消息,返回送达订阅者数(0=没人听) | 副作用:触发订阅者逻辑 |
⚠️ 进了
SUBSCRIBE态的redis-cli连接不能再敲别的命令,排查时务必单独开终端。
2.2 查台(排查最常用)
bash
# 🔒 列出当前有订阅者的协作 channel(即正在协作的 workspace)
PUBSUB CHANNELS workspace:realtime:*
# 🔒 查某 channel 有几个订阅者
PUBSUB NUMSUB workspace:realtime:<workspace_id>
# 🔒 查当前活跃的模式订阅
PUBSUB NUMSUB
PUBSUB CHANNELS💡
PUBSUB CHANNELS只列"当前有活跃订阅者"的频道。如果返回空,不代表频道从没存在,只代表此刻没人订阅——这是排查"消息没收到"的关键线索。
3. Streams 命令速查
| 命令 | 作用 | 风险 |
|---|---|---|
XADD ch * field val ⚠️ | 追加一条,* 自动生成 ID | 写入 |
XLEN ch 🔒 | 流里有几条 | — |
XRANGE ch - + 🔒 | 倒带读全部(-最老 +最新) | — |
XRANGE ch <id> + 🔒 | 从某 ID 之后读(排查断点) | — |
XREAD COUNT 50 STREAMS ch <id> 🔒 | 从某 ID 之后读新消息;$=只读未来 | — |
XREAD BLOCK 15000 STREAMS ch $ 🔒 | 阻塞最多 15s 等新消息 | — |
XINFO STREAM ch 🔒 | 看流元信息(长度、首尾 ID) | — |
XINFO GROUPS ch 🔒 | 看消费组 | — |
XPENDING ch <group> 🔒 | 看消费组内未 ACK 的消息(排查卡住) | — |
XACK ch <group> <id> ⚠️ | 确认处理完 | 改变消费状态 |
XTRIM ch MAXLEN 1000 ⚠️ | 裁剪到最近 N 条 | 丢弃旧消息 |
XDEL ch <id> ⚠️ | 删除指定消息 | 不可逆 |
项目实战:查某条 AI 消息的续接流
bash
# 🔒 看这个 message_id 的流里有多少事件、首尾 ID
XINFO STREAM chat:stream:<message_id>
# 🔒 倒带看全部流式事件(token 序列),排查"续接内容对不对"
XRANGE chat:stream:<message_id> - +
# 🔒 从某个事件 ID 之后读(模拟前端断点续读)
XREAD COUNT 50 STREAMS chat:stream:<message_id> <entry-id>
# 🔒 看这条消息还活着吗(活跃消息 ID 登记)
GET chat:active:conversation:<conversation_id>
GET chat:generation:lease:<message_id> # 生成租约还在=还在生成4. 通用排查命令(读 key 本身)
| 命令 | 作用 | 风险 |
|---|---|---|
TYPE <key> 🔒 | 先确认 key 类型(决定后续用哪条命令) | — |
TTL <key> 🔒 | 剩余秒数(-1=永不过期,-2=不存在/已过期) | — |
EXISTS <key> 🔒 | 是否存在 | — |
SCAN 0 MATCH workspace:presence:* COUNT 100 🔒 | 安全模糊扫(线上用这个) | — |
KEYS <pattern> ❌ | 模糊扫(阻塞,线上大库禁用) | 阻塞 |
HGETALL <key> 🔒 | Hash 全量(presence 用) | — |
HGET <key> <field> 🔒 | Hash 单字段(presence 某用户) | — |
ZRANGE <key> 0 -1 🔒 | ZSet 全量(用户并发槽位用) | — |
DBSIZE 🔒 | 当前库 key 数 | — |
INFO clients 🔒 | 客户端连接数(排查连接池) | — |
INFO memory 🔒 | 内存占用(排查 Streams 堆积) | — |
CLIENT LIST 🔒 | 列出连接(排查谁占着连接) | — |
SLOWLOG GET 10 🔒 | 慢查询 | — |
有副作用的命令(谨慎)
| 命令 | 作用 | 风险 |
|---|---|---|
EXPIRE <key> <sec> ⚠️ | 给 key 设过期 | 续期/提前过期 |
DEL <key> ⚠️ | 删单个 key(必须带具体 key,禁止批量) | 不可逆 |
FLUSHDB / FLUSHALL ❌❌ | 清库 | 绝对禁止(违反仓库安全规则) |
5. 症状驱动排障流程(实战核心)
症状 A:画布操作(移动产物/新增),其他成员看不到
关键命令:
bash
# 1. 确认对方实例有没有订阅这个频道(在对方实例的 redis 上查全局)
PUBSUB NUMSUB workspace:realtime:<workspace_id>
# 2. 自己挂监听,操作画布看后端到底发没发
make redis-sub ARGS=<workspace_id>
# 3. 没消息 → 查后端日志(发布侧)
grep "workspace.output\|workspace.realtime" <日志>症状 B:某人在线,但别人看不到他的光标/在线状态
bash
# 1. 🔒 查 presence hash 里有没有他(field = user_id)
make redis-presence ARGS=<workspace_id>
# 或裸命令:
HGETALL workspace:presence:<workspace_id>
# 2. 🔒 查 hash 还剩多久过期(-2=已过期,说明他超过 120s 没动)
TTL workspace:presence:<workspace_id>
# 3. 🔒 查他单个人
HGET workspace:presence:<workspace_id> <user_id>💡 presence 在用户真移动鼠标时才写入,120s 不动自动过期。如果某人看不到另一人光标:① 后者可能没动鼠标(hash 没写)② 后者 hash 已过期 ③ 前者重连时没收到 presence:state 快照(查 WS 初始同步)。
症状 C:聊天刷新后,AI 回答丢失/续接不上
bash
# 1. 🔒 这条消息的 Stream 还在吗?
TYPE chat:stream:<message_id> # 应返回 stream;none=已清理
XINFO STREAM chat:stream:<message_id> # 长度、首尾 ID
# 2. 🔒 Stream 里到底有没有内容(倒带看 token 序列)
XRANGE chat:stream:<message_id> - +
# 3. 🔒 这条消息还"活跃"吗(续接的前提)
GET chat:active:conversation:<conversation_id>
GET chat:generation:lease:<message_id> # 返回值=还在生成;nil=已结束
# 4. 🔒 用户占用的并发槽位(超限会拒续接)
ZRANGE chat:active:user:<user_id> 0 -1| 现象 | 可能原因 |
|---|---|
TYPE 返回 none | Stream 已过期清理,续接窗口已过(只能重新生成) |
XINFO 有数据但前端空白 | 前端没带 message_id 续接,或带的 entry-id 错位 |
XRANGE 内容不全 | 生成中途异常,查 lease_refresh_failed 指标 / finalize-lock |
lease 已 nil 但 Stream 还在 | 生成已结束,前端应走终态而非续接 |
症状 D:消息发布出去 PUBLISH 返回 0 / 没人收到
| 检查 | 命令 |
|---|---|
| 订阅者先于发布订阅了吗? | Pub/Sub 发完即焚,发布时没人听=永久丢 |
| 频道名拼对了吗? | PUBSUB CHANNELS workspace:realtime:* 核对 |
| 多实例下对方实例订阅了吗? | PUBSUB NUMSUB 是全局的,返回的是所有实例订阅者总和 |
| 订阅连接池满了吗? | INFO clients + 日志 broadcaster |
症状 E:Redis 慢 / 连接池打满 / 内存涨
bash
# 1. 🔒 连接数排查(项目 pubsub 独立池 64,publish 共享池 50)
INFO clients
CLIENT LIST | head # 看谁占着连接(注意 pubsub 会长期占)
# 2. 🔒 内存排查(Streams 不裁剪会涨)
INFO memory
SCAN 0 MATCH chat:stream:* COUNT 100 # 有多少续接 Stream 没清
# 3. 🔒 慢查询
SLOWLOG GET 20
# 4. ⚠️ 单个异常 Stream 太大,按 message_id 精准清(禁止 FLUSH)
XLEN chat:stream:<message_id>
# 确认无误后再处理,优先让它自然过期(EXPIRE 短一点)而不是 DEL6. 多实例排查重要提醒
协作模式线上是多实例部署,排查时要分清"本实例"和"全局":
| 数据 | 作用域 | 说明 |
|---|---|---|
| Pub/Sub channel 订阅者 | 全局(跨实例) | PUBSUB NUMSUB 返回所有实例订阅者总和 |
WebSocket 连接(ConnectionRegistry) | 本实例 | 每个 python 进程一份内存表 |
| presence hash | 全局(存 Redis) | 所有实例读写同一个 workspace:presence:* |
| Stream | 全局(存 Redis) | 按 message_id 全局唯一 |
💡 所以"画布消息跨实例不通"时:先确认
PUBSUB NUMSUB全局有订阅者(排除 Redis 本身问题),再分别查两个实例的_pubsub_loop日志(可能是某实例订阅协程挂了/没重连)。
7. 红线(违反会出大事)
对应仓库 AGENTS.md 安全规则,Redis 排查时同样适用:
| ❌ 禁止 | ✅ 正确做法 |
|---|---|
FLUSHDB / FLUSHALL | 永远不用;要清让 key 自然过期或精准 DEL <具体key> |
KEYS *(线上大库) | SCAN 0 MATCH ... COUNT 100 分页扫 |
无目标 DEL / 批量删 | 只对具体 key 操作,删前先 TYPE/GET 确认 |
改/删 workspace:presence:* 想清在线态 | 让它 120s 自然过期;非要清只能 DEL 单个 hash |
在生产 PUBLISH 测试消息触发业务 | 用专用测试 workspace,或只 SUBSCRIBE 监听不动手 |
8. 一页速查卡(打印备用)
┌──────────────────────── 协作模式 Redis 排查速查 ────────────────────────┐
│ 连接: make redis-sub ARGS=<W> make redis-presence ARGS=<W> │
│ make redis-channels redis-cli -u "$REDIS_URL" │
│ │
│ KEY 约定: │
│ 画布广播 channel : workspace:realtime:{W} (发完即焚) │
│ 在线成员 hash : workspace:presence:{W} (TTL 120s) │
│ 续接 Stream : chat:stream:{msg_id} (XADD/XRANGE/XREAD) │
│ 生成租约 : chat:generation:lease:{msg_id} │
│ │
│ Pub/Sub 查台: PUBSUB CHANNELS workspace:realtime:* │
│ PUBSUB NUMSUB workspace:realtime:{W} │
│ │
│ Stream 倒带: XRANGE chat:stream:{msg} - + │
│ 续点读: XREAD COUNT 50 STREAMS chat:stream:{msg} {id} │
│ 状态: XINFO STREAM chat:stream:{msg} │
│ │
│ 安全: 只读用 SCAN 不用 KEYS;只删具体 key;禁 FLUSH;禁批量 DEL │
│ 多实例: NUMSUB/presence/Stream 是全局;WS 连接表是本实例 │
└────────────────────────────────────────────────────────────────────────┘9. 相关文件索引
| 文件 | 职责 |
|---|---|
backend/scripts/redis_debug.py | 现成排查 CLI(sub/presence/channels) |
backend/services/workspace_realtime/redis_broadcaster.py | Pub/Sub 实现(channel 前缀) |
backend/services/workspace_realtime/workspace_presence_service.py | presence hash 实现 |
backend/services/core/resume/redis_keys.py | 续接相关全部 key 约定 |
backend/services/core/resume/stream_service.py | Stream(xadd/xread)实现 |
backend/core/redis.py | 全局 Redis 客户端单例(get_redis) |