Files
cs-note/hhs/Redis/10-缓存架构模式.md
T
2026-06-08 23:08:57 +08:00

611 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [Redis, 缓存, 架构]
create time: 2026-05-15 18:15
---
# Redis 缓存架构模式
## 概述
在实际生产系统中,Redis 很少直接裸用——它需要与业务数据库、应用层紧密配合。本节介绍最常见的**缓存设计模式**以及经典的三大问题(穿透/击穿/雪崩)的解决方案。
> [!QUESTION] 为什么有了 DB 还需要缓存?
> - **性能**:内存读写 ≈ 1μs,磁盘 IO ≈ 1ms,差距三个数量级
> - **容量**:DB 扛不住瞬时峰值,缓存可以做"削峰填谷"
> - **成本**:一台 Redis 集群的成本远低于扩容 MySQL
**核心矛盾**:缓存带来了性能提升,也引入了**一致性**和**可用性**的新风险。下文所有模式都是围绕这个矛盾在做取舍。
## 一、基础缓存策略
### Cache-Aside Pattern(旁路缓存)—— 最常用
```mermaid
flowchart TD
Req["Request"] --> Hit{"Cache Hit?"}
Hit -->|Yes| Data["Return Cache Data"]
Hit -->|No| DB["Query DB"]
DB --> WriteCache["Write Cache set key val EX 3600"]
WriteCache --> Data
UpReq["Write Request"] --> DelCache["Update DB First"]
DelCache --> DelKV["Delete Cache Key"]
DelKV --> Done["Done"]
WriteCache:::success
DelKV:::danger
```
> [!QUESTION] 写操作为什么"删缓存"而不是"更新缓存"?
> - **删缓存**:简单可靠,下次读时自然回源 DB 拿到最新数据
> - **更新缓存**:需要构造完整值(可能跨表 JOIN),复杂且容易不一致
> - **关键原则**:读时保证一致性,写后不保证实时性(延迟满足)
#### Go 示例
```go
// 读操作
func GetUser(ctx context.Context, id int64) (*User, error) {
data, err := rdb.Get(ctx, "user:"+strconv.FormatInt(id, 10)).Bytes()
if err == redis.Nil {
// 缓存未命中 → 查 DB
u, err := db.GetUser(id)
if err != nil {
return nil, err
}
// 回填缓存
ttl := 30*time.Minute + time.Duration(rand.Intn(300))*time.Second
rdb.Set(ctx, "user:"+strconv.FormatInt(id, 10), toJSON(u), ttl)
return u, nil
}
var u User
json.Unmarshal(data, &u)
return &u, nil
}
// 写操作 —— 先更 DB,再删缓存
func UpdateUser(ctx context.Context, u *User) error {
if err := db.UpdateUser(u); err != nil {
return err
}
rdb.Del(ctx, "user:"+strconv.FormatInt(u.ID, 10))
return nil
}
```
> [!WARNING] Cache-Aside 的竞态窗口
> 典型竞态:读请求 A 先 miss 缓存并开始查 DB,此时写请求 B 更新 DB 并删缓存;A 从 DB 取到**旧值**后回填缓存,覆盖了 B 的删除操作。后续读请求将持续拿到旧数据。
> 解决方案:
> - **延迟双删**:写后删一次缓存,延迟几百毫秒再删一次(覆盖并发读的回填)
> - **读操作加短锁**:回填缓存前加锁,避免并发读覆盖新值
> - **版本号 / 时间戳**:回填时比对版本,旧版本不写入缓存
### Read/Write Through(读写穿透)
通过中间件层自动处理缓存读写,业务代码只跟中间件交互:
```mermaid
flowchart LR
App["App Code"] --> Cache["Cache Layer"]
Cache --> DB["Database"]
App:::success
Cache:::warning
DB:::info
```
> [!NOTE] 实际中较少纯原生实现
> Redisson(Java)、Spring Cache 提供此抽象;Go 生态没有标准库,通常自行封装。核心思路是 **用 AOP / Middleware 把缓存逻辑从业务代码中剥离**。
### Write-Behind(异步写回)—— 高写入吞吐场景
与 Read/Write Through 不同,Write-Behind 将写操作**先落到缓存**,再**异步批量刷回 DB**,适合写密集型场景(如计数器、日志采集)。
```mermaid
flowchart LR
App["Write Request"] --> Cache["Redis, Return OK"]
Cache -->|"Async Queue, Batch Merge"| Writer["Write-Back Worker"]
Writer --> DB["MySQL"]
App:::success
Cache:::warning
Writer:::info
```
> [!WARNING] Write-Behind 的风险
> - **数据丢失风险**:缓存宕机时,尚未刷回 DB 的数据会丢失
> - **实现复杂度高**:需要可靠的异步队列 + 重试机制 + 幂等保证
> - **适用场景有限**:适合"可容忍少量丢失"的计数、埋点类业务,**不适合核心交易数据**
## 二、三大经典问题
### 缓存穿透(Penetration)—— 不存在的数据一直打到 DB
缓存穿透是指查询的数据**在缓存和数据库中都不存在**。由于缓存永远无法命中,每次请求都会穿透缓存直接打到数据库。在正常业务中这种情况偶尔发生无伤大雅,但如果攻击者利用大量不存在的 ID(如随机负数、极端大的随机数)频繁请求,流量会全部落到 DB 上,可能直接打垮数据库。
> [!QUESTION] 为什么是"不存在的数据"而不是"过期数据"?
> - **过期数据**:过段时间就自然恢复了,是 TTL 管理的范畴
> - **不存在的数据**:DB 和缓存都没有,请求每次都打到 DB,属于**恶意攻击或 Bug**
```mermaid
sequenceDiagram
participant C as "客户端"
participant R as "Redis"
participant D as "MySQL"
C->>R: GET user:999999999
R-->>C: nil
C->>D: SELECT * FROM users WHERE id=999999999
D-->>C: empty result
Note over C,D: "每次非法请求都走完整链路, QPS = N * 非法流量"
```
#### 方案 1:布隆过滤器(Bloom Filter)
```mermaid
flowchart LR
Req["Request"] --> BF{"Bloom Filter"}
BF -->|"Not Exist, Reject"| Reject["Return Empty"]
BF -->|"Maybe Exist"| Cache["Query Redis"]
Cache -->|"nil"| DB["Query DB"]
Cache -->|"Hit"| Return["Return Cached"]
DB -->|"Has Data"| StoreCache["Write Cache + Update BF"]
DB -->|"No Data"| EmptyCache["Cache Empty Value"]
Reject:::danger
Return:::success
StoreCache:::success
```
```go
// Package main — 布隆过滤器预加载示例
import "github.com/bits-and-blooms/bloom/v3"
bf := bloom.NewWithEstimates(1_000_000, 0.01) // 百万级元素,错误率 1%
bf.Add([]byte("user:1"))
bf.Add([]byte("user:2"))
if !bf.Test([]byte("user:999999")) {
return nil, fmt.Errorf("key does not exist") // 直接拒绝,不查 DB
}
```
> [!TIP] 布隆过滤器的误判
> `false positive` 会发生(判断存在但实际不存在),但不会 `false negative`。所以把拦截器放在 DB 前面是安全的。
> [!NOTE] Go 进程内 BF vs Redis 原生 BF
> - **Go 进程内**(如 `bloom/v3`):零网络开销,但多实例间数据不共享,适合单体或实例数少的场景
> - **Redis 原生**(`BF.RESERVE` / `BF.ADD` / `BF.EXISTS`,需加载 RedisBloom 模块):多实例共享同一份过滤器,适合分布式部署,但每次判断需一次网络 RTT
> - 生产推荐:用 Redis 原生 BF 做全局拦截 + 本地缓存做热点加速,两者配合效果最佳
#### 方案 2:空值缓存
```go
// 缓存一个短过期时间的空值(如 30s ~ 5min)
rdb.Set(ctx, "user:999999", "", 5*time.Minute)
// TTL 不宜过长,否则业务数据新增后仍然拦截
```
> [!QUESTION] 两种方案怎么选?
> - 全量已知数据集(如商品 SKU ID 列表)→ **布隆过滤器**,精确高效
> - 动态未知数据集(如任意用户 ID)→ **空值缓存**,实现简单
### 缓存击穿(Breakdown)—— 热点 key 过期瞬间 QPS 洪峰
#### 什么是热点 Key?
热点 Key 是指在**短时间内被极高频访问**的少数 Key,它们通常承载了系统中最核心、最高频的读取数据。与其他 Key 相比,热点 Key 的特征是:**访问量极度集中**——可能只占全部 Key 的 1%,却承载了 90% 以上的读请求。
| 场景 | 热点 Key 示例 | 访问特征 |
|------|-------------|---------|
| 电商秒杀 | `product:1001:detail` | 秒杀开始瞬间,万级 QPS 集中打同一个商品 |
| 社交热搜 | `hot:topic:某明星` | 热搜爆发期,读写比可达 100:1 |
| 配置中心 | `config:global` | 几乎所有请求都读取,读远大于写 |
| 排行榜 | `rank:daily:top10` | 定时更新,查询窗口内被密集读取 |
> [!QUESTION] 怎么判断某个 Key 是不是"热点"?
> 核心指标是 **QPS 集中度**:如果单个 Key 的 QPS 远超平均值(例如平均 QPS=10,但某个 Key 达到 5000+),它就是热点。具体发现手段见下方「如何发现热点 Key」小节。
理解了热点 Key 的概念后,缓存击穿的问题就好理解了——
#### 击穿是如何发生的?
缓存击穿针对的就是**单个热点 Key**。当某个热点 Key 恰好过期的瞬间,成百上千个并发请求同时发现缓存 miss,转而并发打到数据库,造成瞬时 QPS 洪峰。这与雪崩不同——它不需要大量 Key 同时过期,**一个热点 Key 就够了**。
```mermaid
sequenceDiagram
participant C as Client
participant R as Redis
participant D as DB
Note over C,R: t1 — 正常期
C->>R: GET key
R-->>C: ✅ 命中,正常返回
Note over C,R: t2 — TTL 到期
C->>R: GET key → nil!
Note over C,D: t3 — 击穿瞬间
rect rgba(255, 0, 0, 0.1)
loop 1000 concurrent requests
C->>R: GET key
R-->>C: nil (未命中)
C->>D: SELECT ...
end
end
Note over D: DB QPS spike, may crash
```
#### 方案 1:互斥锁(Mutex Lock)
核心思路很简单:**热点 key 过期后,只允许一个请求去查 DB 重建缓存,其余请求排队等待,等缓存重建完成后再读缓存。** 就像只有一个窗口办理业务,其他人后面排队。
具体流程:
1. 缓存 miss 时,先尝试用 `SETNX` 抢一把分布式锁
2. 抢到锁的请求 → 查 DB → 回写缓存 → 释放锁
3. 没抢到锁的请求 → 短暂 sleep → 重试读缓存(大概率已被重建好了)
4. 带**双重检查**:拿到锁后再读一次缓存,防止等待期间别人已经重建完毕
> [!QUESTION] 如果不用分布式锁会怎样?
> 假设 1000 个请求同时 miss 缓存,它们会并发打到 DB,相当于没有缓存保护。加锁后只有 1 次 DB 查询,其余 999 次读缓存即可命中。
```go
func GetProduct(ctx context.Context, id int64) (*Product, error) {
cacheKey := "product:" + strconv.FormatInt(id, 10)
// 先查缓存
data, _ := rdb.Get(ctx, cacheKey).Bytes()
if data != nil {
var p Product
json.Unmarshal(data, &p)
return &p, nil
}
// 拿分布式锁,只有一个 goroutine 去查 DB + 重建缓存
lockKey := "lock:" + cacheKey
const maxRetries = 3
locked := false
for i := 0; i < maxRetries; i++ {
locked, _ = rdb.SetNX(ctx, lockKey, "1", 10*time.Second).Result()
if locked {
break
}
// 未拿到锁 → 等别人重建完后再查缓存
time.Sleep(50 * time.Millisecond)
if v, _ := rdb.Get(ctx, cacheKey).Bytes(); v != nil {
var p Product
json.Unmarshal(v, &p)
return &p, nil
}
if i == maxRetries-1 {
return nil, errors.New("获取锁超时,降级处理")
}
}
// 确保锁在函数退出时释放(无论成功或 panic)
if locked {
defer rdb.Del(ctx, lockKey)
}
// 双重检查——防止自己等太久,另一个已经重建了
data, _ = rdb.Get(ctx, cacheKey).Bytes()
if data != nil {
var p Product
json.Unmarshal(data, &p)
return &p, nil
}
p, err := db.GetProduct(id)
if err != nil {
return nil, err
}
rdb.Set(ctx, cacheKey, toJSON(p), 30*time.Minute)
return p, nil
}
```
#### 方案 2:逻辑过期(Logical Expiration)
互斥锁虽然解决了并发问题,但拿到锁的请求还是要等 DB 查询完成——这段时间用户是被**阻塞**的。逻辑过期的思路是:**永远不让用户等。**
做法:Redis 的 key **不设物理 TTL**(永远不过期),而是在 value 里额外存一个 `expire_at` 字段表示"逻辑过期时间"。每次读取时:
1. 检查 `expire_at`:没过期 → 直接返回,零开销
2. 已过期 → **先返回旧数据**(用户体验不断裂),同时**异步触发后台重建**
这就把"同步等 DB"变成了"异步静默刷新",代价是:过期瞬间的短暂时间窗口内,用户拿到的是**旧数据**。
> [!WARNING] 为什么需要 singleflight?
> 逻辑过期的判断发生在每个请求上,如果一秒内 1000 个请求都发现 key 过期,它们会各自触发重建。`singleflight` 保证同一个 key 的多次并发重建合并为一次 DB 查询,避免"重建风暴"。
```mermaid
flowchart LR
Read["Read Request"] --> Check{"Check expire_at"}
Check -->|"Not Expired"| Return["Return Old Value OK"]
Check -->|"Expired"| Async["Async Rebuild Cache"]
Async --> ReturnOld["Return Old Value, No Block"]
subgraph "Background Rebuild"
Rebuild["Query DB"] --> SetNew["Overwrite Redis Value"]
end
Return:::success
ReturnOld:::warning
```
> [!TIP] 两种方案的本质区别
> - **Mutex Lock**:保证返回的一定是**最新数据**,但会短暂阻塞
> - **Logical Expiration**:保证**永远不阻塞用户**,但可能返回略旧的数据
```go
// 读取时不检查物理 TTL,只检查逻辑过期时间
v := rdb.Get(ctx, key).Val()
var item struct {
Content string
ExpireAt int64
}
json.Unmarshal([]byte(v), &item)
if time.Now().Unix() < item.ExpireAt {
return item.Content // 未过期,直接返回
}
// 异步重建缓存(不阻塞当前请求)
// ⚠️ 需配合 singleflight 或分布式锁,防止并发触发大量重建
go rebuildCacheAsync(key, item.ExpireAt)
return item.Content // 返回旧值,后台静默刷新
```
> [!TIP] singleflight 防止缓存重建风暴
> 当多个 goroutine 同时发现 key 过期并尝试重建时,`singleflight` 保证**同一个 key 只触发一次 DB 查询**,其余 goroutine 等待并共享结果。
```go
import "golang.org/x/sync/singleflight"
var rebuildGroup singleflight.Group
func rebuildCacheAsync(key string, oldExpireAt int64) {
// singleflight 保证同一 key 只有一个 goroutine 真正执行
_, _, _ = rebuildGroup.Do(key, func() (interface{}, error) {
// 再次检查:可能别的 goroutine 已经重建完了
v := rdb.Get(ctx, key).Val()
var item struct{ ExpireAt int64 }
json.Unmarshal([]byte(v), &item)
if item.ExpireAt > oldExpireAt {
return nil, nil // 已被别人刷新,跳过
}
// 查 DB → 回写缓存(设置新的逻辑过期时间)
data := queryDB(key)
newExpireAt := time.Now().Add(30 * time.Minute).Unix()
payload, _ := json.Marshal(map[string]interface{}{
"content": data,
"expire_at": newExpireAt,
})
rdb.Set(ctx, key, payload, 0) // 不设物理 TTL,靠逻辑过期
return nil, nil
})
}
```
#### 如何发现热点 Key?
击穿的前提是"热点",但如果不知道哪些 Key 是热点,防护就无从谈起:
| 方案 | 原理 | 适用场景 |
|------|------|---------|
| **Redis `MONITOR` / `redis-cli --hotkeys`** | Redis 4.0+ 的 LFU 采样,直接输出高频 Key | 开发调试、小规模集群 |
| **客户端代理统计** | 在 SDK 层埋点,按 Key 维度统计 QPS | 生产环境精确统计 |
| **Redis Cluster `CLUSTER COUNTKEYSINSLOT`** | 统计 slot 级别的 Key 分布 | 定位数据倾斜 |
| **离线分析慢查询日志** | `SLOWLOG GET` + 日志聚合 | 事后分析 |
### 缓存雪崩(Avalanche)—— 大量 key 同时过期
缓存雪崩是三种问题中影响面最大的。它发生在**大面积缓存同时失效**的时刻——通常是因为大量 Key 设置了相同的 TTL,或者 Redis 节点整体宕机。失效瞬间,海量请求集体涌向数据库,DB 瞬时压力暴增,可能直接被打垮甚至引发级联故障,导致整个服务不可用。雪崩的破坏力在于"雪球效应":DB 慢查询堆积 → 连接池耗尽 → 上游超时 → 用户重试 → 流量进一步放大。
> [!QUESTION] 穿透/击穿/雪崩 的区别是什么?
> - **穿透**:数据**永远不存在**,请求直达 DB → 关注"过滤非法请求"
> - **击穿**:单个**热点 key** 过期,瞬时洪峰 → 关注"防并发重建"
> - **雪崩**:大批量 / **全部 key** 同时过期 → 关注"错峰 + 兜底"
```mermaid
flowchart LR
subgraph "No Jitter"
T["TTL=30min"] -->|"t=30:00"| AllExpire["100% keys expire simultaneously"]
AllExpire --> Crash["DB overload crash"]
Crash:::danger
end
subgraph "With Jitter"
T2["TTL=30min +-5min"] -->|"t=30:00"| P1["20% expire"]
T2 -->|"t=31:00"| P2["20% expire"]
T2 -->|"t=32:00"| P3["20% expire"]
T2 -->|"t=33:00"| P4["20% expire"]
T2 -->|"t=34:00"| P5["20% expire"]
P1 -->|"DB handles smoothly"| Safe
P2 --> Safe
P3 --> Safe
P4 --> Safe
P5 --> Safe
Safe:::success
end
```
#### 解决方案
**① TTL 加随机偏移(最常用)**
```go
// ✅ 给 TTL 加随机偏移(抖动)
baseTTL := 30 * time.Minute
jitter := time.Duration(rand.Intn(300)) * time.Second // 0~5min 随机
rdb.Set(ctx, key, value, baseTTL+jitter)
```
> [!NOTE] 抖动范围怎么定?
> 建议设为基础 TTL 的 **10% ~ 20%**。太小效果不明显,太大则可能导致冷数据过早失效。
**② 多级缓存**
```go
// L1 本地缓存(进程内)→ L2 Redis(分布式)→ DB
func GetValue(key string) ([]byte, error) {
if v := localCache.Get(key); v != nil { // ~1μs,L1 hit
return v, nil
}
rdbKey := "cache:" + key
if v, err := rdb.Get(ctx, rdbKey).Bytes(); err == nil { // ~1ms,L2 hit
localCache.Set(key, v) // L1 回填
return v, nil
}
// L2 miss → 查 DB → 回填两级缓存
data := queryDB(key)
l1TTL, l2TTL := 5*time.Minute, 30*time.Minute
rdb.Set(ctx, rdbKey, data, l2TTL)
localCache.Set(key, data)
return data, nil
}
```
**③ 限流降级(兜底策略)**
```go
// 当缓存命中率骤降时,触发熔断
if cacheMissRate > 0.5 {
// 返回默认响应 / 排队等待 / 直接限流
return fallbackResponse()
}
```
> [!WARNING] 多级缓存的一致性挑战
> 本地缓存和 Redis 之间天然存在延迟不一致。多节点部署下,A 节点的本地缓存和 B 节点的本地缓存可能看到不同值。**适合读多写少、允许短暂不一致的场景**。
```mermaid
flowchart LR
subgraph "In-Process"
L1["L1 Local Cache, ~1us"]
end
subgraph "Distributed"
L2["L2 Redis Cluster, ~1ms"]
end
L1 -->|"miss"| L2
L2 -->|"miss"| DB["MySQL, ~5-50ms"]
DB -->|"Backfill"| L2
L2 -->|"Broadcast Invalidation"| L1
```
## 三、缓存设计原则总结
| # | 原则 | 说明 |
|---|------|------|
| 1 | 永远不信任单一数据源 | 缓存和 DB 保持最终一致 |
| 2 | 设 TTL 是必须的 | 即使内存够也要设,防止永久占用 |
| 3 | TTL 加随机抖动 | 避免大规模同时过期 |
| 4 | 大 Key 拆小 | 单个 Key ≤ 5KB,防止网络阻塞 |
| 5 | Pipeline 批量操作 | 减少 RTT,提升吞吐 |
| 6 | 监控命中率 | 低于 80% 考虑调整策略 |
| 7 | 静态数据不必放 Redis | 配置、字典等低频变化数据用本地缓存即可,节省网络开销 |
| 8 | 敏感数据不缓存明文 | 密码、手机号等脱敏或加密后再写入缓存 |
## 四、进阶:基于 Binlog 的缓存一致性
Cache-Aside 的"先删缓存再更新 DB"存在竞态窗口。生产中更可靠的方案是**监听 DB 变更日志(Binlog),异步驱动缓存失效**。
```mermaid
flowchart LR
App["Business Write"] --> DB["MySQL"]
DB -->|"Binlog"| Canal["Canal or Debezium"]
Canal --> MQ["Kafka or RocketMQ"]
MQ --> Consumer["Cache Invalidation Worker"]
Consumer -->|"DEL key"| Redis["Redis"]
DB:::info
Canal:::warning
Redis:::danger
```
> [!QUESTION] 这种方案解决了什么问题?
> - **解除业务代码耦合**:写操作不需要关心缓存失效,由 Binlog 订阅统一处理
> - **减少竞态窗口**:Binlog 保证有序,可以按变更顺序精确删除缓存
> - **适合多数据源**:一次 DB 变更可以同时驱动多个缓存实例失效
> [!NOTE] 实际落地注意点
> - **延迟**:Binlog → MQ → Consumer 链路通常有 **100ms~1s** 延迟,仍属于最终一致性
> - **幂等**:Consumer 必须保证幂等(重复消费同一 Binlog 不产生副作用)
> - **顺序性**:同一行的多次变更需要保证消费顺序,通常按 `ROW_ID` 分区
## 五、常见架构模式速览
```mermaid
flowchart TD
Client["Client Request"]
Client --> LocalCache{"Local Cache?"}
LocalCache -->|hit| Return["Return"]
LocalCache -->|miss| Redis["Redis"]
Redis -->|hit| Return
Redis -->|miss| DB["Database"]
DB --> Redis
Redis --> LocalCache
Writer["Write"] -->|"Update DB First"| DB
DB -->|"Async Sync Cache"| Redis
Redis:::warning
LocalCache:::success
DB:::info
```
## 六、缓存选型与演进路径
> [!QUESTION] 项目初期应该从哪一层开始做缓存?
> **答案:先做 L2(Redis),稳定后再加 L1(本地缓存)。** 原因如下:
### 决策树
```mermaid
flowchart TD
Start["有新需求要加缓存"] --> Q1{"数据是否频繁变化?"}
Q1 -->|"否 — 配置/字典"| Config["直接放应用内存<br/>不经过 Redis"]
Q1 -->|"Yes"| Q2{"QPS > 5000?"}
Q2 -->|"No - Low QPS"| Simple["L2 only Cache-Aside + Redis"]
Q2 -->|"Yes - Mid QPS"| Medium["L2 + ReadThrough + Bloom Filter"]
Q2 -->|"Yes - High QPS"| HotKey["L1 + L2 Multi-Level Cache"]
Config --> End["Done ✅"]
Simple --> End
Medium --> End
HotKey --> End
Config:::warning
Simple:::success
Medium:::danger
HotKey:::danger
```
### 不同阶段的关键关注点
| 阶段 | 核心目标 | 推荐模式 | 避坑指南 |
|------|---------|---------|---------|
| **V1 起步期** | 快速上线,能跑就行 | Cache-Aside + Redis | 不要过度设计,先解决有无问题 |
| **V2 增长期** | 扛住流量高峰 | 布隆过滤器 + 互斥锁 | 关注监控指标,建立告警机制 |
| **V3 大规模** | 极致性能和稳定性 | 多级缓存 + 逻辑过期 + 降级策略 | 一致性 vs 可用性的平衡取舍 |
> [!TIP] 一句话总结
> **好的缓存架构不是设计出来的,是演进出来的**。先保证正确性,再优化性能,最后打磨可用性。
## 关联笔记
- [[hhs/Redis/09-高级特性]] — Lua 脚本 + 分布式锁
- [[hhs/Redis/07-集群方案]] — Cluster 下的一致性挑战
- [[hhs/Redis/README]] — 知识索引总览
- [[hhs/GORM/09-钩子函数]] — GORM 生命周期中同步缓存的模式