BodySense Redis 第 1 课:Redis 心智模型、Cache 与 Fast State Plane
从 Redis 的内存数据结构服务器定位出发,用 BodySense 的 Session、Refresh Token、限流与临时状态建立 Cache 与 Fast State Plane 的区别。
[!info] related notes
- 课程 MOC:BodySense Redis 从零到一课程 MOC
- 对应知识笔记:Redis、Refresh Token、Access Token
- 关系笔记:Stateful、Stateless 与 Hybrid 认证架构
- 项目:BodySense 项目 MOC
BodySense Redis 第 1 课:Redis 心智模型、Cache 与 Fast State Plane
[!abstract] 学习进度 第 1 / 12 课。目标不是先背 Redis 命令,而是先回答:为什么一份状态应该由 Redis 持有?
本课目标
建立以下心智模型:
Redis 是一个以内存为主要工作介质的数据结构服务器。它提供 String、Hash、Set、Sorted Set、Stream 等数据结构,以及 TTL、原子操作等能力,因此既可以作为缓存层,也可以直接承担 Session、限流、Token 状态、临时任务状态等高速运行状态。
不要把 Redis 简化成“数据库缓存”。
先回答最基础的问题:Redis 到底是什么
很多教程一上来就说:
Redis 是一个 Key-Value 数据库,速度很快,经常用来做缓存。
这句话不能算错,但对真正理解 Redis 来说太粗了。
更有用的定义是:
Redis 是一个以内存为主要工作介质的数据结构服务器。客户端通过网络命令访问它,Value 不是只有“字符串”,而是可以直接使用 String、Hash、Set、Sorted Set、List、Stream 等服务器原生数据结构;同时 Redis 提供 TTL、原子命令、事务/脚本、发布订阅、持久化等能力。
我们把这句话拆开。
1. Redis 是一个 Server,不是 Go 进程里的 map
如果在 Go 里写:
m := map[string]int{}
m["login_failed:1.2.x.x"] = 4
这个状态属于当前 Go 进程:
API instance A
└─ memory map
如果 BodySense 有三个 API 副本:
API A → 自己的 map
API B → 自己的 map
API C → 自己的 map
三份状态互相看不到。
Redis 则是独立服务进程:
API A ─┐
API B ─┼─ network → Redis Server
API C ─┘
所以多个应用实例可以共享同一份运行状态。
客户端发送的本质是命令:
SET name Arlon
GET name
INCR counter
SADD roles admin
BodySense 的 Go 服务使用 go-redis/v9 作为客户端,apps/api/internal/database/redis.go 负责建立 Redis Client、处理地址/密码/TLS,并在启动时 PING 验证连接。
2. “以内存为主要工作介质”是什么意思
PostgreSQL 的核心设计是 durable database;Redis 的主工作数据集主要放在内存中。
可以先形成粗略直觉:
CPU
↓
RAM ← Redis 主要工作的地方
↓
SSD / Disk ← Redis 可以做 RDB/AOF 持久化,后面再学
内存访问通常比随机磁盘 I/O 更快,因此 Redis 很适合低延迟、高频、结构相对简单的状态。
但这里不要形成另一个误解:
Redis 在内存里,所以 Redis 一定是“临时的、重启就全没”。
Redis 支持 RDB/AOF 持久化,第 7 课会专门讲。现在只需要知道:
in-memory 描述主要运行介质;是否需要持久化,是另一条独立设计轴。
3. Redis 为什么叫 Data Structure Server
最简单的 Key-Value 系统可以想成:
key → bytes
Redis 更进一步:
key → server-native data structure
例如:
user:42:name
→ String
user:42
→ Hash {name, age, role}
user_sessions:42
→ Set {s1, s2, s3}
leaderboard
→ Sorted Set
events
→ Stream
这意味着 Redis 不只是帮你“存一坨 JSON”;它知道某个 Value 是 Set,就能在服务器端直接执行:
SADD
SREM
SISMEMBER
SCARD
它知道某个 Value 是数字 String,就能直接:
INCR
DECR
这也是为什么 Redis 的价值是:
内存访问
+ 数据结构
+ TTL
+ 原子操作
+ 多实例共享
而不是只剩一个“快”。
4. Redis 为什么经常被叫做缓存,但又不只是缓存
因为 Cache 确实是 Redis 最常见的使用方式之一:
PostgreSQL
→ 把常读结果复制一份
→ Redis
→ 请求优先读 Redis
但 Redis 还可以直接保存系统当前运行中的短期状态,例如:
Session 是否 active
Refresh Token 是否 consumed
某 IP 10 分钟内失败几次
验证码是否还有效
某个短期幂等 Key 是否已占用
这些状态并不一定在 PostgreSQL 里存在一份“原件”。
因此本课程使用一个很重要的术语:
Fast State Plane:高速运行状态层。
它不是 Redis 官方产品名,而是我们用来帮助做架构分类的心智模型。
5. Redis 和 PostgreSQL 不是“谁更高级”的关系
错误比较:
Redis 更快
→ 尽量把 PostgreSQL 数据搬到 Redis
正确问题:
这份状态是什么?
谁拥有它?
需要多久?
丢了能不能重建?
需要关系/约束/审计吗?
需要什么并发语义?
例如用户姓名:
读取频率:可能非常高
生命周期:长期
需要账号事实一致性:是
Redis 丢失后应该能从数据库恢复:是
所以:
PostgreSQL = truth
Redis = optional cache
而 Session active 状态:
生命周期:短/中期
访问:几乎每个受保护请求
需要 TTL:是
需要立即 revoke:是
复杂关系查询:通常不需要
Redis 就非常适合作为它的在线 authority。
6. 一个实用的 Ownership 判断表
以后每次考虑“要不要放 Redis”,先问:
| 问题 | 倾向 PostgreSQL | 倾向 Redis |
|---|---|---|
| 需要长期业务事实/审计? | ✓ | |
| 需要 FK/复杂关系/约束? | ✓ | |
| 丢失后必须从历史恢复? | ✓ | |
| 生命周期天然很短? | ✓ | |
| 需要 TTL 自动失效? | ✓ | |
| 每个请求都要超低延迟访问? | 可能 | ✓ |
| 需要原子 counter/set membership? | ✓ | |
| Redis 丢失后可从别处重建? | truth 在别处 | Cache 很合适 |
| Redis 中状态本身就是当前 truth? | Fast State Plane |
PostgreSQL 与 Redis 的第一层分工
PostgreSQL
= Durable Truth / 长期业务事实
Redis
= Fast State Plane / 高速运行状态
例如 BodySense:
PostgreSQL
├─ User
├─ Assessment
├─ Diagnosis
├─ Treatment
└─ 需要长期持久化、审计、关系约束的数据
Redis
├─ Session
├─ Refresh Token rotation/replay state
├─ Rate Limit counter
├─ Auth revoke tombstone
├─ 验证码 / 临时状态
└─ 未来的通知、缓存、wakeup
[!important] 判断数据库归属时先问“谁是事实所有者” “访问频率高不高”不是第一判断。用户姓名即使每秒读取一万次,它的业务事实来源仍可以是 PostgreSQL;Redis 只是额外缓存副本。
Cache 与 Fast State Plane
Cache
缓存保存的是别处已经存在的事实副本:
PostgreSQL
↓ copy
Redis Cache
↓
user:42
如果 Redis 中的缓存丢了:
cache miss
→ 查询 PostgreSQL
→ 重建 cache
因此:
Cache 是可重建的高速副本。
Fast State Plane
有些状态本来就只存在 Redis:
session:abc123 = active
login_attempt:1.2.x.x = 4
verification:xyz = valid
refresh:<digest> = active / consumed
例如:
DEL session:abc123
→ Session 真的失效
不是:
cache miss
→ 去 PostgreSQL 把 Session 重建回来
因此:
Fast State Plane 保存系统当前正在运行的状态,这个状态本身就可能只存在 Redis 中。
TTL 为什么适合临时状态
例如:
SET verification:123456 "user-42" EX 300
含义:
Key = verification:123456
Value = user-42
TTL = 300 秒
5 分钟以后 Key 失效。
非常适合:
验证码 5 min
登录失败计数 10 min
Session 与 refresh 生命周期绑定
Refresh state days
幂等 Key 24 h
临时任务状态 1 h
如果完全用 PostgreSQL,应用往往还需要维护 expires_at、定时清理和过期删除,而 Redis 的 TTL 可以直接表达业务生命周期。
为什么 JWT 仍可能需要 Redis Session
假设 Access Token:
22:00 签发
22:30 到期
用户 22:05 Logout。
如果只检查 JWT:
signature ✓
exp ✓
那么旧 Token 直到 22:30 仍可能被接受。
BodySense 的思路是:
JWT
│ sid = s1
↓
Redis session:s1
Logout:
DEL session:s1
下一请求:
JWT signature ✓
JWT exp ✓
session:s1 ✗
→ 拒绝访问
[!tip] 两个事实不要混淆 JWT 回答“这是不是服务器签发、是否在 Token 自身生命周期内”;Redis Session authority 回答“服务器现在是否仍认可这次登录”。
Refresh Token:可重复使用还是 Rotation
Refresh Token 的确可以设计为在生命周期内反复使用:
R1 → A1
R1 → A2
R1 → A3
...
直到 R1 expires
但更安全的一类设计是 Refresh Token Rotation:
Login
→ A1 + R1
R1 refresh
→ A2 + R2
→ R1 invalid
R2 refresh
→ A3 + R3
→ R2 invalid
形成:
R1 → R2 → R3 → R4
旧 Token 再次出现:
R1 = consumed
↓
R1 又被提交
↓
replay / reuse detected
↓
revoke family / session
Token Family
同一次长期登录中的 Refresh Token 后代可以属于同一个 family:
Family F1
R1 → R2 → R3 → R4
检测到旧 Token replay 后,可以撤销整个 family,避免合法用户和攻击者长期共享认证链。
Token 到底存哪里
Access Token
如果 Access Token 是自包含 JWT,通常不需要把原始 Access Token复制保存到 Redis 或 PostgreSQL。
Client
↓ Bearer JWT
Server
↓ verify signature / claims
↓ sid
Redis
↓ check live session authority
Redis 保存的是服务器在线状态,而不是必须保存每个 JWT 原文。
Refresh Token
对于 opaque Refresh Token,更安全的常见模型:
客户端
→ 原始 Refresh Token
服务端
→ digest/hash + session/family/status/expiry
例如:
refresh:<sha256(R1)>
→ session_id
→ family_id
→ active / consumed
→ TTL
收到 R1 后:
SHA256(R1)
→ 查服务端状态
这样 Redis 泄漏也不会直接暴露一串可立即拿去调用 refresh endpoint 的 bearer credential。
PostgreSQL 是否也要存 Token
不是“Redis 和 PostgreSQL 各存一份才安全”。
可以按职责分:
PostgreSQL
├─ User / Account
├─ Password hash
├─ Profile
├─ Assessment / Diagnosis / Treatment
└─ 长期设备、登录历史、审计(若产品需要)
Redis
├─ Session active/revoked
├─ Refresh digest / family
├─ Rotation / replay state
├─ Auth rate limit
└─ 其他在线状态
即使 PostgreSQL 需要长期保存 Session / Device 审计,也通常保存 metadata / digest,而不是原始 Refresh Token。
Prediction
[!question] 判断主要存储位置,并说明为什么
A. 用户姓名
B. IP 在过去 10 分钟登录失败 4 次
C. 三个月前完成的一份 Assessment
D. Session 还剩 23 分钟
E. 某 Refresh Token 已经使用过一次
F. Diagnosis Run 产生新事件,需要唤醒等待中的 Web Server
[!success]- Answer / 解析 A → PostgreSQL 为 truth,Redis 最多做 Cache。 姓名是长期账户事实;高频读取不改变 ownership。
B → Redis 很合适。 这是一个短窗口 counter,需要
INCR + TTL,窗口结束后状态自然失效。C → PostgreSQL。 Assessment 是长期可恢复、可审计的业务记录,不应该因为 Redis 更快而成为 Redis-only。
D → Redis。 Session 是在线运行状态,需要 TTL、即时 revoke、多实例共享;BodySense 当前就是 Redis Session authority。
E → Redis。 Rotation/replay detection 是高频、短期、需要原子 single-winner 的认证状态。BodySense 当前保存的是 Refresh Token digest 派生 Key,而不是原始 bearer token。
F → Redis 可以参与“通知”,但不要自动推出事件本身也应该 Redis-only。 后续第 9 课会看到:Pub/Sub 可以负责 wakeup,durable RuntimeEvent 仍由 PostgreSQL 保存,才能支持 reconnect/replay。
Explain-back / L4 Gate
能够不依赖“Redis 很快”这个理由,解释:
- 为什么 session:
放 Redis 合理; - 为什么 BodyState / Assessment 不能因为 Redis 快就迁成 Redis-only truth;
- Cache 与 Fast State Plane 的区别;
- Access Token 原文、Refresh Token 原文、Session 状态分别由谁持有。
本课结论
Cache 是已有事实的高速副本;Fast State Plane 保存的是系统当前正在运行的状态,这个状态本身可以只存在 Redis 中。
Redis 的价值也不只是“内存快”,而是:
内存访问
+ 数据结构
+ TTL
+ 原子操作
+ 并发协调