写给前端的 Redis 概念课:从缓存到 Agent 的消息枢纽
从前端熟悉的缓存出发,理解 Redis Client、Pub/Sub、Channel,以及它们如何与 SSE 配合,为 Agent 的实时输出和流式恢复提供支持。
文章目录
- 一、先忘掉「Redis = 缓存」
- 二、为什么 Agent 系统里突然需要 Redis?
- 三、Redis Client 到底是什么?
- 四、Redis 不只会存数据,它还会「推消息」
- 五、什么是发布订阅?
- 六、Channel 又是什么?
- 七、Redis 为什么能「主动通知」Client?
- 八、这和 SSE 很像,但它们不是一回事
- 九、Redis Pub/Sub + SSE,其实是两段完全不同的流
- 十、但 Pub/Sub 有一个非常重要的问题:它不保存历史
- 十一、所以「历史数据」和「实时消息」通常要分开
- 十二、这里还有一个坑:为什么经常看到两个 Redis Client?
- 十三、前端工程师可以怎么类比 Redis?
- 十四、为什么 Agent 系统特别容易用到 Redis?
- 十五、但不要因此把 Redis 当成万能数据库
- 十六、重新理解 Redis
做前端的时候,我对 Redis 的理解一直非常简单:
缓存。
比如接口太慢,就把结果存 Redis。
数据库压力太大,就先查 Redis。
验证码、登录态、限流计数,也可以放 Redis。
所以很长一段时间里,我脑子里的 Redis 大概就是:
key -> value
写进去:
await redis.set("user:1001", JSON.stringify(user))
读出来:
const user = await redis.get("user:1001")
直到开始做 Agent、流式输出以及多实例部署之后,我才发现:
只把 Redis 理解成缓存,其实严重低估了它。
尤其当系统里开始出现这些东西:
- SSE
- Streaming
- Redis Pub/Sub
- Producer
- Consumer
- Channel
- 流式恢复
- 多实例
如果 Redis 这一层没有理解清楚,后面的流式架构基本很难真正看懂。
所以这一篇不准备把 Redis 的所有数据结构讲一遍。
我们只解决一个问题:
一个从前端转向 Agent 全栈的工程师,到底应该怎么理解 Redis?
一、先忘掉「Redis = 缓存」
Redis 全称是:
Remote Dictionary Server。
你可以先粗暴地把它理解成:
一个运行在独立进程里的、以内存为主要存储介质的数据服务。
例如你的 Node.js 后端:
Node.js
↓
Redis Client
↓
Redis Server
Redis Server 是一个独立服务。
Node.js 并不是直接操作 Redis 的内存,而是通过一个 Redis Client 和 Redis 建立连接,然后发送命令。
例如:
const redis = new Redis()
await redis.set("name", "dongmu")
const value = await redis.get("name")
这里其实经历的是:
Node.js
│
│ SET name dongmu
▼
Redis Server
│
│ 存储
▼
Memory
之后:
Node.js
│
│ GET name
▼
Redis Server
│
│ "dongmu"
▼
Node.js
这就是我们最熟悉的 Redis 用法。
从这个角度来看,它确实很像一个速度极快的 KV 数据库。
但问题就在这里。
Redis 并不只有:
- GET
- SET
它还有另外一套非常重要的能力:
消息通信。
也就是我们后面做 Agent Streaming 时非常关键的:
Publish / Subscribe
简称:
Pub/Sub。
二、为什么 Agent 系统里突然需要 Redis?
先从一个非常简单的 Agent 服务开始。
假设现在只有一台服务器:
Browser
│
│ POST /chat
▼
Server
│
▼
LLM
模型生成 Token:
你
你好
你好,我
你好,我是
你好,我是 ChatGPT
Server 每拿到一个 chunk,就通过 SSE 推给浏览器:
LLM
│
│ chunk
▼
Server
│
│ SSE
▼
Browser
这个架构其实非常简单。
因为所有东西都发生在同一个进程里面。
比如:
for await (const chunk of llmStream) {
res.write(chunk)
}
模型产生 chunk。
当前服务器直接把 chunk 写进 SSE Connection。
结束。
但当系统开始部署多台服务器之后,事情就变了。
比如:
Load Balancer
/ \
/ \
Server A Server B
用户第一次发消息的时候,请求可能落在:
Server A
于是 Agent 在 Server A 上运行。
但是网络突然断开。
浏览器重新建立 SSE:
Browser
│
▼
Load Balancer
│
▼
Server B
问题出现了。
Agent 明明还在:
Server A
继续生成:
chunk 101
chunk 102
chunk 103
...
但浏览器现在连接的是:
Server B
Server B 根本不知道 Server A 正在生成什么。
这时候我们就需要一个东西站在 A 和 B 中间。
例如:
Server A
│
▼
Redis
│
▼
Server B
这时候 Redis 的角色就不再只是缓存了。
它开始变成:
多个服务实例之间共享状态和传递消息的中间层。
这也是为什么当你从前端进入 Agent 后端以后,会越来越频繁地看到 Redis。
三、Redis Client 到底是什么?
这个概念非常容易混。
我们经常写:
const redis = new Redis()
这个 redis 并不是 Redis 本身。
它只是:
Redis Client。
也就是:
我们的应用程序和 Redis Server 之间的一条连接。
可以把它理解成:
Application
│
Redis Client
│
TCP Connection
│
Redis Server
例如:
const redis = new Redis()
背后会建立一条 TCP Connection。
之后你执行:
redis.get()
redis.set()
redis.del()
其实都是在通过这条连接向 Redis Server 发送命令。
所以:
Redis
和:
Redis Client
是两个完全不同的东西。
前者是:
Redis Server
后者是:
连接 Redis Server 的客户端
这个 distinction 非常重要。
因为等我们进入 Pub/Sub 之后,经常会看到:
const redis = new Redis()
const subscriber = new Redis()
很多前端第一次看到会懵:
为什么 Redis 要创建两个实例?
实际上并不是启动了两个 Redis。
而是:
Node.js
│
├── Redis Client A ─────┐
│ │
└── Redis Client B ─────┼── Redis Server
│
建立了两条独立连接。
四、Redis 不只会存数据,它还会「推消息」
这是理解后面 Agent Streaming 最重要的一步。
假设有两个后端实例:
Server A
Server B
现在我们希望:
Server A 有新消息以后,Server B 立刻知道。
一种很笨的做法是让 B 不停问 Redis:
有消息了吗?
有了吗?
有了吗?
现在有了吗?
也就是:
while (true) {
const message = await redis.get("message")
}
这叫轮询。
显然非常浪费。
Redis 提供了另外一种机制:
Pub/Sub
也就是:
发布 / 订阅。
五、什么是发布订阅?
发布订阅模式其实有三个非常经典的角色:
- Producer
- Broker
- Consumer
也可以叫:
- Publisher
- Broker
- Subscriber
对应关系是:
Producer
│
│ Publish
▼
Broker
│
│ Push
▼
Consumer
Producer 是:
消息生产者。
Consumer 是:
消息消费者。
Broker 是:
消息中间人 / 消息枢纽。
Redis 在这里扮演的就是:
Broker
所以整个结构变成:
Server A
Producer
│
│ PUBLISH
▼
Redis
Broker
│
│ Push
▼
Redis Client
Subscriber
│
▼
Server B
Server A 不需要知道 Server B 在哪里。
Server B 也不需要不停询问 Server A。
双方只需要认识 Redis。
这就是消息中间件非常核心的价值:
让生产消息的人和消费消息的人解耦。
六、Channel 又是什么?
有了 Broker 以后,还需要解决一个问题。
Redis 每秒可能有成千上万条消息。
Server B 怎么知道:
哪些消息是我要的?
所以 Pub/Sub 里面还有一个概念:
Channel
你可以把它理解成一个「广播频道」。
比如:
agent:session:10001
Server B 订阅:
await subscriber.subscribe(
"agent:session:10001"
)
相当于告诉 Redis:
以后这个 Channel 有消息,通知我。
Redis 就会记录:
agent:session:10001
Subscribers:
- Client B
此时 Server A 发布:
await publisher.publish(
"agent:session:10001",
JSON.stringify({
type: "chunk",
content: "hello"
})
)
消息首先到 Redis:
Server A
│
│ PUBLISH
▼
agent:session:10001
Redis 再检查:
谁订阅了这个 Channel?
发现:
- Client B
- Client C
- Client D
于是 Redis 会把消息推送给它们:
Redis
│
┌───────┼───────┐
▼ ▼ ▼
Client B Client C Client D
这就是所谓的:
消息扇出。
七、Redis 为什么能「主动通知」Client?
这里是我以前理解 Redis 时最大的一个认知转折。
普通 Redis 使用方式通常是:
Client → Redis
比如:
GET user:1
Redis 再返回:
Redis → Client
所以我们很容易形成一种错觉:
Redis 好像只能被调用。
实际上并不是。
Redis Client 和 Redis Server 之间本身就建立了一条:
TCP 长连接
大致可以理解成:
Application
│
Redis Client
│
│ TCP Connection
▼
Redis Server
当 Client 执行:
SUBSCRIBE channel
以后,这条连接就进入了订阅状态。
之后有新的消息:
Producer
│
│ PUBLISH
▼
Redis
Redis 可以直接通过已经存在的 TCP Connection:
Redis
│
│ Push Message
▼
Subscriber
所以这里根本不需要:
Subscriber
↓
不断轮询 Redis
而是:
Subscriber
│
│ SUBSCRIBE
▼
Redis
等待……
Redis
│
│ Message
▼
Subscriber
这是一个非常关键的区别。
八、这和 SSE 很像,但它们不是一回事
如果你是前端工程师,看到这里可能已经觉得很熟悉了。
因为 SSE 也是:
连接先建立
Server
│
│ push
▼
Client
Redis Pub/Sub 也是:
连接先建立
Redis
│
│ push
▼
Subscriber
所以它们在通信形态上确实很像。
但两个东西完全不是一个协议。
SSE 是:
Browser
│
│ HTTP
▼
Web Server
它是 Web 标准:
Server-Sent Events。
而 Redis Pub/Sub 是:
Redis Client
│
│ RESP over TCP
▼
Redis Server
使用的是 Redis 自己的协议。
所以:
SSE ≠ Redis Pub/Sub
只是二者都具备:
连接建立之后,由 Server 一侧持续向 Client 一侧推送数据。
而在 Agent Streaming 系统里面,我们恰好经常把它们串起来。
九、Redis Pub/Sub + SSE,其实是两段完全不同的流
现在把前面的东西全部连起来。
假设:
- Agent 在 Server A
- 用户 SSE 连到了 Server B
那么完整链路可能是:
LLM
│
│ chunk
▼
Server A
│
│ publish
▼
Redis Channel
│
│ Pub/Sub
▼
Server B
│
│ SSE
▼
Browser
注意这里实际上发生了两段完全不同的通信。
第一段:
Server A
↓
Redis
↓
Server B
使用:
Redis Pub/Sub
第二段:
Server B
↓
Browser
使用:
SSE
所以一个 token 可能经历:
LLM
↓
Producer
↓
Redis Broker
↓
Subscriber
↓
SSE Server
↓
Browser
这时候你再回头看这些名词:
- Producer
- Consumer
- Broker
- Subscriber
- Channel
- SSE
- Redis Client
其实就没有那么神秘了。
十、但 Pub/Sub 有一个非常重要的问题:它不保存历史
这是理解「流式恢复」之前必须知道的一件事。
假设:
Client B
正在订阅:
agent:10001
Producer 发布:
chunk 1
chunk 2
chunk 3
B 都可以收到。
但是如果 B 掉线了:
chunk 4
chunk 5
chunk 6
Redis 依然会继续 Publish。
等 B 重新连接以后:
SUBSCRIBE agent:10001
它只能收到:
chunk 7
chunk 8
...
前面的:
chunk 4
chunk 5
chunk 6
不会自动补回来。
因为 Redis Pub/Sub 本身不是消息历史系统。
它更接近:
实时广播。
你在线,就能听到。
你不在线,这段广播就过去了。
这和电视直播很像。
你晚上 8 点打开电视:
只能从晚上 8 点开始看
不会因为你刚打开电视,电视台就自动把晚上 7 点 50 的节目重新播放给你。
十一、所以「历史数据」和「实时消息」通常要分开
这时候流式恢复真正的问题就出现了。
我们既需要:
实时 chunk
又需要:
历史 chunk
于是系统通常会变成:
┌──── 存储历史 ──── Redis / DB
│
Agent Producer
│
└──── Publish ───── Redis Pub/Sub
│
▼
Subscriber
│
▼
SSE
│
▼
Browser
这里 Redis 可能同时承担两个完全不同的职责。
第一个:
State Store。
例如:
session:10001:chunks
里面保存已经生成的内容。
第二个:
Message Broker。
通过:
- PUBLISH
- SUBSCRIBE
传递最新 chunk。
所以恢复的时候,Server B 可以:
- 读取历史 chunk
- 订阅实时 channel
- 把两部分继续通过 SSE 发给浏览器
于是:
历史
+
实时
拼起来以后,用户看到的仍然是一条连续的 Agent 输出。
这其实就是很多「流式恢复」架构背后的基本思想。
十二、这里还有一个坑:为什么经常看到两个 Redis Client?
如果你以后看 Node.js 代码,经常会看到这样的写法:
const redis = new Redis()
const subscriber = redis.duplicate()
甚至:
const publisher = new Redis()
const subscriber = new Redis()
以前我会觉得很奇怪:
为什么连接同一个 Redis 要创建两个 Client?
现在就很好理解了。
一个连接可能负责:
- GET
- SET
- PUBLISH
另外一个连接专门:
SUBSCRIBE
尤其传统 Redis Pub/Sub 中,连接进入订阅状态以后,这条连接的用途会受到限制。
所以工程实践里通常会把:
普通命令连接
和:
订阅连接
分开。
于是代码可能是:
const redis = new Redis()
const subscriber = redis.duplicate()
await subscriber.subscribe(channel)
subscriber.on("message", (channel, message) => {
// 收到 Redis 推送
})
从架构角度来看:
Node.js Process
├── Redis Client A
│ GET / SET / PUBLISH
│
└── Redis Client B
SUBSCRIBE
其实非常合理。
十三、前端工程师可以怎么类比 Redis?
如果一定要从前端世界找几个类比,我觉得可以这么记。
Redis 的:
GET / SET
有点像:
共享状态存储
Redis 的:
Pub/Sub
则有点像我们前端非常熟悉的:
eventBus.emit("message", data)
eventBus.on("message", handler)
只不过普通 EventBus 的作用范围通常是:
当前 JS 进程
Redis Pub/Sub 的作用范围可以跨:
- Process
- Machine
- Pod
- Service Instance
比如:
Pod A
│
│ publish
▼
Redis
│
├────────► Pod B
│
└────────► Pod C
所以可以粗略地认为:
Redis Pub/Sub 就像一个跨进程、跨机器的 EventBus。
虽然实现原理完全不同,但作为前端工程师建立第一层直觉,这个类比非常好用。
十四、为什么 Agent 系统特别容易用到 Redis?
因为 Agent 天生就是一个:
长任务系统。
普通接口可能是:
Request
↓
300ms
↓
Response
请求生命周期很短。
但 Agent 可能是:
Request
↓
Thinking
↓
Tool Call
↓
Tool Result
↓
Thinking
↓
Tool Call
↓
Streaming
↓
30 秒
↓
60 秒
↓
甚至几分钟
一旦任务变长,就会不断遇到:
用户刷新页面怎么办?
SSE 断了怎么办?
Server 重启怎么办?
请求打到另一个 Pod 怎么办?
Agent 还在跑,但用户换机器了怎么办?
多个客户端如何订阅同一个 Agent 状态?
这时候你会发现:
进程自己的内存已经不够用了。
因为:
Server A 的内存
Server B 看不到。
Server A 一旦挂掉:
Memory
也跟着没了。
所以 Agent 后端通常需要一个:
所有实例都可以访问的共享层
Redis 恰好非常适合承担其中一部分职责。
比如:
- 状态缓存
- 任务状态
- Session
- 分布式锁
- 限流
- Pub/Sub
- 消息事件
- 流式 chunk
所以前端工程师一旦开始进入 Agent Backend,Redis 几乎是绕不开的。
十五、但不要因此把 Redis 当成万能数据库
这里也要建立一个很重要的边界。
Redis 很快。
Redis 很方便。
Redis 能存状态。
Redis 也能传消息。
但这不意味着:
什么都应该塞 Redis。
尤其是 Redis Pub/Sub。
它最大的特点就是:
- 快
- 轻
- 实时
但代价也是:
- 没有消息持久化
- Subscriber 离线就可能丢消息
如果业务需要的是:
- 每条消息必须消费
- 可以重放
- 需要 ACK
- 需要消费进度
那就不能简单地只靠 Pub/Sub。
Redis 自己还有:
Redis Streams
而更大型的系统还可能使用:
- Kafka
- RabbitMQ
- NATS
所以技术选型的重点从来不是:
Redis 好不好?
而是:
你到底想解决什么问题?
如果目标只是:
不同 Server 实例之间实时通知
Redis Pub/Sub 非常自然。
如果目标是:
可靠消息队列
那就需要进一步考虑持久化、ACK、消费进度等机制。
十六、重新理解 Redis
所以如果让我重新给 Redis 下一个定义,我现在不会再说:
Redis 是缓存。
我更愿意说:
Redis 是一个以内存为核心的数据服务,它既可以承担高速共享状态存储,也可以承担进程之间的实时通信。
在最简单的场景里:
Application
│
│ GET / SET
▼
Redis
它是存储。
而在 Agent Streaming 场景:
Producer
│
│ Publish
▼
Redis
│
│ Push
▼
Subscriber
它又变成了 Broker。
再和 SSE 串起来:
Agent
↓
Producer
↓
Redis Pub/Sub
↓
Subscriber
↓
SSE
↓
Browser
就构成了一条完整的实时数据链路。
而如果再加入:
- 历史 chunk
- 断线重连
- 多 Pod
- Session
事情就会进一步变成:
历史数据 + 实时消息
这也正是下一篇要讨论的问题:
一个 Agent 正在 Streaming 的过程中,用户 SSE 突然断了,重新连接到另一个 Server,我们到底怎么把这条流恢复回来?
理解了这一篇 Redis,后面要细讲的「流式恢复」才能更好的理解。
持续拆解 Agent 工程、产品与实战