← [ BACK_TO_NOTES ]

BodySense Redis 第 1 课:Redis 心智模型、Cache 与 Fast State Plane

从 Redis 的内存数据结构服务器定位出发,用 BodySense 的 Session、Refresh Token、限流与临时状态建立 Cache 与 Fast State Plane 的区别。

#course #growing #backend #redis #bodysense

[!info] related notes

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 很快”这个理由,解释:

  1. 为什么 session: 放 Redis 合理;
  2. 为什么 BodyState / Assessment 不能因为 Redis 快就迁成 Redis-only truth;
  3. Cache 与 Fast State Plane 的区别;
  4. Access Token 原文、Refresh Token 原文、Session 状态分别由谁持有。

本课结论

Cache 是已有事实的高速副本;Fast State Plane 保存的是系统当前正在运行的状态,这个状态本身可以只存在 Redis 中。

Redis 的价值也不只是“内存快”,而是:

内存访问
+ 数据结构
+ TTL
+ 原子操作
+ 并发协调
创建于 2026/9/18 更新于 2026/9/18
Digital Biome
Access

键盘导航

j / k / ↑ / ↓
选择对象
Enter / o
打开选择
/ 或 Ctrl+K
全站搜索
i / f / 1–4
筛选当前列表
g → h / l / p / t / a
首页 / 笔记 / 项目 / 工具 / About
r → 1–9
打开所选对象的关联
u
返回上一页
Esc
关闭浮层 / 取消命令;输入框内清空文本