← 全部文章

写给前端的 Redis 概念课:从缓存到 Agent 的消息枢纽

从前端熟悉的缓存出发,理解 Redis Client、Pub/Sub、Channel,以及它们如何与 SSE 配合,为 Agent 的实时输出和流式恢复提供支持。

文章目录
  1. 一、先忘掉「Redis = 缓存」
  2. 二、为什么 Agent 系统里突然需要 Redis?
  3. 三、Redis Client 到底是什么?
  4. 四、Redis 不只会存数据,它还会「推消息」
  5. 五、什么是发布订阅?
  6. 六、Channel 又是什么?
  7. 七、Redis 为什么能「主动通知」Client?
  8. 八、这和 SSE 很像,但它们不是一回事
  9. 九、Redis Pub/Sub + SSE,其实是两段完全不同的流
  10. 十、但 Pub/Sub 有一个非常重要的问题:它不保存历史
  11. 十一、所以「历史数据」和「实时消息」通常要分开
  12. 十二、这里还有一个坑:为什么经常看到两个 Redis Client?
  13. 十三、前端工程师可以怎么类比 Redis?
  14. 十四、为什么 Agent 系统特别容易用到 Redis?
  15. 十五、但不要因此把 Redis 当成万能数据库
  16. 十六、重新理解 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 可以:

  1. 读取历史 chunk
  2. 订阅实时 channel
  3. 把两部分继续通过 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 工程、产品与实战