08 · Redis 协调平面
Redis 在 Buzz 中不是数据库替代品,而是跨 Relay pod 的协调平面:事件广播、动态 topic 管理、presence、限流、NIP-98 防重放、缓存失效和连接控制都依赖它。安全设计的核心原则是:Redis 可以决定谁收到候选消息,但不能决定谁有权看到消息。
1. 两类连接
buzz-pubsub 明确分离:
| 连接 | 用途 |
|---|---|
| pool | GET/SET/INCR/EVAL/PUBLISH 等普通命令,可并发借用 |
| dedicated PubSub | SUBSCRIBE/UNSUBSCRIBE 是有状态协议,必须由单独任务持有 |
如果用连接池随机执行订阅,连接归还后其协议状态会污染下一个调用者;如果把所有普通命令塞进 PubSub 连接,又会被订阅读取循环卡住。
2. Topic 设计
事件 topic 大致按社区和频道划分:
buzz:{community}:channel:{channel_id}
buzz:{community}:global此外还有缓存失效、连接控制和其他子系统 key/channel。community 是 key 的一部分,使 Redis 层也尽量不混租户;但即使 topic 错配,Relay fan-out 仍要用 TenantContext 与 access gate 阻止泄露。
3. 动态订阅状态机
PubSubManager 维护 desired topic refcount:
retain(topic)
0 → 1 : SUBSCRIBE
n → n+1 : only refcount
release(topic)
n → n-1 : only refcount
1 → 0 : wait 500ms → if still 0 UNSUBSCRIBE500 ms debounce 避免用户快速切换频道时产生订阅风暴。Redis 断线后以 1s 到 30s 指数退避重连,并从 desired set 恢复订阅,不以服务器记忆为准。
4. 本地 broadcast channel
Redis reader 把事件解析后送入 Tokio broadcast channel(代码文档给出 4096 容量)。每个消费者自己跟进;落后的 consumer 会收到 lag 错误,而不是拖住 Redis reader。
这种“允许 lag、靠历史补偿”的策略适合事件通知,但不适合不可丢的财务命令。Buzz 把需要可靠恢复的 Push 放入 PostgreSQL outbox,就是在区分两类语义。
5. Presence
Presence 使用 community-scoped Redis key,TTL 约 180 秒。它是租约式状态:
- 客户端周期刷新。
- 超时自然离线,不要求完美 disconnect。
- pod 崩溃不会留下永久在线。
- 读取 presence 不需要扫描持久事件表。
Typing 更短命,主要通过 ephemeral kind/fan-out 表达;不要把根架构文档中旧的模块描述误认为当前一定存在独立 typing store。
6. 固定窗口限流
RedisRateLimiter 用 Lua 把计数与首次 EXPIRE 绑定在原子操作中:
key = rate:{tenant}:{subject}:{bucket}
count = INCR key
if count == 1: EXPIRE key window
allow = count <= limit优点是多 pod 共享、实现小、性能可预测;缺点是窗口边界突发和 Redis 依赖。它已在当前 Relay AppState 接入,故旧 ARCHITECTURE.md 的“no rate limiter”限制已经过期。
7. NIP-98 replay guard
HTTP 签名通常有时间窗口,但仅校验时间仍允许窗口内重放。Redis replay store 按认证 event ID/主体记录已用 nonce,使用 SET NX EX 类语义:
- 第一次成功占位。
- 同一认证事件再次使用被拒绝。
- TTL 后自动回收。
- 多 pod 共享判断。
对写入、Git、上传等非幂等端点尤其重要。
8. 缓存失效与连接控制
成员、封禁、频道配置变化后,单 pod 内更新缓存不够。Relay 通过 Redis 广播 invalidation/control:
- 清除其他 pod 的 community/channel/member cache。
- 要求关联连接重验或断开。
- 让新的访问规则尽快作用于 live fan-out。
即便失效消息丢失,关键请求仍应有 TTL 或权威 DB 复核作为最终安全保障;缓存失效只缩短陈旧窗口。
9. Redis 故障时的语义
| 能力 | Redis 故障影响 | 权威事实是否丢失 |
|---|---|---|
| 跨 pod fan-out | 其他 pod 实时通知延迟,重连可补历史 | 否,event 已在 PostgreSQL |
| presence/typing | 暂时未知或离线 | 这些本来就是 ephemeral |
| rate/replay | 按 endpoint 选择拒绝或降级 | 不应静默放宽高风险边界 |
| cache invalidation | 其他 pod 短暂陈旧 | DB 仍是权威 |
| connection control | 断开传播延迟 | 请求/fan-out 权限仍应复核 |
10. 为什么 Redis 不是安全边界
Redis 缺少读者身份上下文:一个频道 topic 上的消息面对的是整 pod,而不是最终连接。授权需要当前数据库成员、Agent owner、kind privacy 和 tenant label。把授权放进 topic 名会导致角色变更、私密 kind 和主体门难以表达。
正确分层是:
Redis topic = routing hint
subscription index = candidate hint
Relay access gate = authorization decision11. 源码入口
crates/buzz-pubsub/src/lib.rs:连接分层、PubSub manager、重连和 topic refcount。crates/buzz-pubsub/src/presence.rs:租户级 presence TTL。crates/buzz-pubsub/src/rate_limiter.rs:原子 fixed-window limiter。crates/buzz-pubsub/src/nip98_replay.rs:防重放。crates/buzz-relay/src/state.rs:Relay 接线、缓存失效与连接控制消费者。