apps/server/docs/ai-context/redis-boundaries-and-pubsub.md
这篇文档约束服务端使用 Redis 时的几个高风险边界:
这不是“推荐写法”集合,而是后续改代码时应该默认遵守的约束。
当前 apps/server 中 Redis 主要承担四类职责:
user:{userId}:fluxtts:voices:upstream:{model} —— TTL 600s,仅 200 响应入缓存,见 src/routes/openai/v1/index.ts::handleListVoicesconfig:{key}chat:{userId}:broadcastbilling-eventsuser:{userId}:flux-meter:tts:debt其中:
Redis key 和 Pub/Sub channel 必须通过单独 helper 构造,不要在多个调用点重复写模板字符串。
推荐模式:
function fluxRedisKey(userId: string): string {
return `user:${userId}:flux`
}
function userBroadcastChannel(userId: string): string {
if (typeof userId !== 'string' || userId.length === 0) {
throw new TypeError('user broadcast channel requires a non-empty string userId')
}
return `chat:${userId}:broadcast`
}
这样做的原因不是“风格统一”,而是为了避免:
不要假设 `${value}` 可以安全把任意值转成 Redis key。
原因:
value 在运行时是对象,会得到 [object Object]发布侧必须显式构造消息对象,不要把“业务对象刚好长得像 payload”当成协议。
推荐模式:
interface BroadcastMessage {
userId: string
payload: {
chatId: string
messages: unknown[]
fromSeq: number
toSeq: number
}
}
function createBroadcastMessage(
userId: string,
payload: BroadcastMessage['payload'],
): BroadcastMessage {
if (typeof userId !== 'string' || userId.length === 0) {
throw new TypeError('broadcast message requires a non-empty string userId')
}
return { userId, payload }
}
消费侧不要只写:
const data = JSON.parse(message) as BroadcastMessage
因为这只是类型断言,不是校验。
至少要验证:
userId 是非空字符串payload.chatId 是字符串payload.messages 是数组fromSeq / toSeq 是数字如果消息不合法,应该记录错误并丢弃,而不是继续广播到本地连接。
Redis Streams 的参考实现已经在 src/libs/mq/stream.ts 里。
这层模式值得复用的点有两个:
serialize() 收口deserialize() 和运行时检查收口也就是说:
XADD / XREADGROUPsrc/routes/chat-ws.ts 当前采用:
这个设计的语义必须明确:
pullMessages因此后续如果改聊天同步:
userId、chatId、streamMessageId?src/services/flux.tssrc/libs/mq/stream.tssrc/routes/chat-ws.tsconfig-and-naming-conventions.md`prefix:${id}`,优先做小范围收口JSON.parse(...) as SomeType 出现在 Redis 边界,默认把它视为待修复点