CHAPTER 01

数据与状态分离

理解这个概念之前,我们需要先搞明白两个词——「数据」和「状态」到底有什么区别?为什么把它们分开,扩容就变得轻松了?

💾

数据 Data

持久化的、可共享的信息。存储在数据库、文件系统、对象存储中。多实例共享同一份数据源。

MySQL Redis S3

状态 State

运行时产生的、存在内存中的信息。每个进程/实例独立持有,无法直接共享。重启即丢失。

内存变量 Session 缓存
CHAPTER 02

状态耦合 vs 状态分离

让我们通过一个具体例子,看两种设计的天壤之别。

Click to Switch Demo
❌ 耦合架构

每台服务器都存状态

1// 每个 Node 实例自己存 Session
2const sessions = {}; // 内存中
3app.post('/login', (req, res) => {
4 sessions[userId] = { role: 'admin' }; // 只存在这台机器
5});
❌ 扩容困难 ❌ 负载不均 ❌ 单点故障
VS
✅ 分离架构

状态外置到 Redis

1// Session 存到独立的 Redis
2const redis = new Redis();
3app.post('/login', (req, res) => {
4 await redis.set(`session:${userId}`, data);
5});
✅ 秒级扩容 ✅ 请求均摊 ✅ 无状态服务
CHAPTER 03

扩容场景对比

同样是扩容请求量 10 倍,两种架构的操作天差地别。

❌ 耦合架构扩容

  1. 先评估每个实例的负载上限
  2. 计算需要新增几台机器
  3. 部署新机器,安装相同环境
  4. 配置负载均衡
  5. 测试请求是否会路由到新机器
  6. ⚠️ 如果用户 session 在旧机器,新机器读不到
  7. ⚠️ 需要 sticky session 或 session 复制

耗时:30-60 分钟,复杂且容易出错

✅ 分离架构扩容

  1. 打开 K8s/Docker Swarm 控制台
  2. 把副本数从 3 改成 10
  3. 按回车

耗时:30 秒,自动完成

原因:无状态服务可以随时启停,
新实例启动后自动从 Redis 读取状态

Horizontal Scaling Animation
CHAPTER 04

为什么分离就能轻松扩容?

核心原理:通过把「状态」从应用实例中抽离出来,让服务变成「无状态」的。

State Separation Principle
🔄

任意实例可处理请求

请求可以路由到任意一个实例,不需要关心「这个用户上次访问的是哪台机器」。因为所有状态都在外部存储。

秒级启停

实例挂了?直接 kill 掉,新实例 5 秒拉起。不需要迁移状态,因为状态本来就不在实例上。

📊

流量自动均摊

负载均衡器可以随意选择最优实例。不存在「某台机器特别热」的问题。

🛡️

故障隔离

一个实例崩溃不影响其他实例。用户请求自动被路由到健康实例,对用户无感知。

CHAPTER 05

常见实现方式

把状态外置,这些是标配方案。

🔴

Redis - Session / 缓存

最常用的内存数据库,存储用户 Session、热点数据缓存。毫秒级读写,适合高频访问的场景。

1redis.set(`session:${userId}`, sessionData)
2redis.get(`session:${userId}`)
🐬

MySQL - 业务数据

持久化的业务数据,用户信息、订单、配置等。关系型数据库保证数据一致性。

1INSERT INTO users (id, name) VALUES (...)
2SELECT * FROM orders WHERE userId = ?
🦜

S3 / OSS - 文件存储

用户上传的文件、图片、视频等。分布式文件系统,提供海量存储和高可用。

1oss.upload('avatar/user123.jpg', file)
2oss.getUrl('avatar/user123.jpg')
Complete Architecture
无状态服务(可水平扩展)
Redis 状态存储
MySQL 持久化存储
负载均衡器
CHAPTER 06

❌ 这些是状态耦合的反模式

在应用内存中存储用户 Session
每个实例都有独立的 Session 副本,用户可能被路由到任意实例,导致登录失效。→ 改用 Redis Session
在实例变量中存储配置/缓存
配置变了需要重启所有实例。→ 改用配置中心(Apollo / Nacos / etcd)
本地文件存储用户上传的文件
文件只在当前机器,其他机器访问不到。→ 改用 OSS / S3 / 分布式文件系统
单实例内维护长连接/订阅关系
实例挂了连接全断。→ 改用消息队列 + 消费者组,或 WebSocket 网关
FINAL

一句话总结

把「状态」当作「数据」来处理

数据存在 DB/Redis/OSS → 任何实例都能访问 →
服务变成「无状态」→
可以随意扩缩容 → 故障自动恢复 → 运维轻松

记住这个公式

无状态服务 + 外部存储 = 可水平扩展

什么时候该分离?

  • 需要水平扩展(多实例)时
  • 需要高可用(故障恢复)时
  • 需要弹性伸缩(高峰期扩容)时
  • 需要 Session 共享时