444 lines
14 KiB
Markdown
444 lines
14 KiB
Markdown
|
|
---
|
|||
|
|
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["请求到达"] --> Hit{"读缓存命中?"}
|
|||
|
|
Hit -->|是| Data["返回缓存数据"]
|
|||
|
|
Hit -->|否| DB["查询数据库"]
|
|||
|
|
DB --> WriteCache["写入缓存<br/>set key val EX 3600"]
|
|||
|
|
WriteCache --> Data
|
|||
|
|
|
|||
|
|
UpReq["写请求到达"] --> DelCache["先更新 DB"]
|
|||
|
|
DelCache --> DelKV["再删除缓存 Key"]
|
|||
|
|
DelKV --> Done["结束"]
|
|||
|
|
|
|||
|
|
style WriteCache fill:#dfd,stroke:#090
|
|||
|
|
style DelKV fill:#fdd,stroke:#900
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> [!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 的最终一致性
|
|||
|
|
> 写操作后立刻读,可能读到旧值(因为删缓存比下一个读请求先执行)。对于强一致场景,可以:
|
|||
|
|
> - 读操作加一把短锁(避免并发刷缓存覆盖新值)
|
|||
|
|
> - 或者直接用 DB 作为唯一真相源(放弃缓存一致性依赖)
|
|||
|
|
|
|||
|
|
### Read/Write Through(读写穿透)
|
|||
|
|
|
|||
|
|
通过中间件层自动处理缓存读写,业务代码只跟中间件交互:
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
flowchart LR
|
|||
|
|
App["App Code<br/>业务层"] --> Cache["Cache Layer<br/>中间件透明封装"]
|
|||
|
|
Cache --> DB["Database<br/>持久层"]
|
|||
|
|
|
|||
|
|
style App fill:#efe,stroke:#090
|
|||
|
|
style Cache fill:#fda,stroke:#da0
|
|||
|
|
style DB fill:#ddf,stroke:#66c
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> [!NOTE] 实际中较少纯原生实现
|
|||
|
|
> Redisson(Java)、Spring Cache 提供此抽象;Go 生态没有标准库,通常自行封装。核心思路是 **用 AOP / Middleware 把缓存逻辑从业务代码中剥离**。
|
|||
|
|
|
|||
|
|
## 二、三大经典问题
|
|||
|
|
|
|||
|
|
### 缓存穿透(Penetration)—— 不存在的数据一直打到 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: 每次非法请求都走完整链路<br/>QPS = N × 非法流量
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 方案 1:布隆过滤器(Bloom Filter)
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
flowchart LR
|
|||
|
|
Req["请求"] --> BF{"布隆过滤器"}
|
|||
|
|
BF -->|不存在<br/>直接拒绝| Reject["返回空 / 错误"]
|
|||
|
|
BF -->|可能存在| Cache["查 Redis"]
|
|||
|
|
Cache -->|"nil"| DB["查 DB"]
|
|||
|
|
Cache -->|"命中"| Return["返回缓存值"]
|
|||
|
|
DB -->|"有数据"| StoreCache["写入缓存 + 回刷 BF"]
|
|||
|
|
DB -->|"无数据"| EmptyCache["空值缓存"]
|
|||
|
|
|
|||
|
|
style Reject fill:#fcc,stroke:#c00
|
|||
|
|
style Return fill:#cfc,stroke:#090
|
|||
|
|
style StoreCache fill:#dfd,stroke:#090
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
```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 前面是安全的。
|
|||
|
|
|
|||
|
|
#### 方案 2:空值缓存
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// 缓存一个短过期时间的空值(如 30s ~ 5min)
|
|||
|
|
rdb.Set(ctx, "user:999999", "", 5*time.Minute)
|
|||
|
|
// TTL 不宜过长,否则业务数据新增后仍然拦截
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> [!QUESTION] 两种方案怎么选?
|
|||
|
|
> - 全量已知数据集(如商品 SKU ID 列表)→ **布隆过滤器**,精确高效
|
|||
|
|
> - 动态未知数据集(如任意用户 ID)→ **空值缓存**,实现简单
|
|||
|
|
|
|||
|
|
### 缓存击穿(Breakdown)—— 热点 key 过期瞬间 QPS 洪峰
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
sequenceDiagram
|
|||
|
|
participant K as Key: hot_product:42
|
|||
|
|
participant R as Redis
|
|||
|
|
participant D as DB
|
|||
|
|
|
|||
|
|
time t1
|
|||
|
|
K->>R: GET key
|
|||
|
|
R-->>K: ✅ 命中,正常返回
|
|||
|
|
|
|||
|
|
time t2 (TTL 到期)
|
|||
|
|
K->>R: GET key → nil!
|
|||
|
|
|
|||
|
|
time t3 — "击穿瞬间"
|
|||
|
|
rect rgba(255, 0, 0, 0.1)
|
|||
|
|
loop 1000 个并发请求
|
|||
|
|
R-->>Req: nil (未命中)
|
|||
|
|
Req->>D: SELECT ... (全部打到 DB!)
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
Note over D: ⚠️ DB QPS 瞬时暴增 → 可能崩溃
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 方案 1:互斥锁(Mutex Lock)
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
func GetProduct(ctx context.Context, id int64) (*Product, error) {
|
|||
|
|
data, _ := rdb.Get(ctx, "product:"+strconv.FormatInt(id, 10)).Bytes()
|
|||
|
|
if data != nil {
|
|||
|
|
var p Product
|
|||
|
|
json.Unmarshal(data, &p)
|
|||
|
|
return &p, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 拿分布式锁,只有一个 goroutine 去查 DB + 重建缓存
|
|||
|
|
lockKey := "lock:product:" + strconv.FormatInt(id, 10)
|
|||
|
|
locked, err := rdb.SetNX(ctx, lockKey, "1", 10*time.Second).Result()
|
|||
|
|
if err != nil || !locked {
|
|||
|
|
time.Sleep(50 * time.Millisecond) // 等别人重建完
|
|||
|
|
return GetProduct(ctx, id) // 递归重试(有最大深度保护)
|
|||
|
|
}
|
|||
|
|
defer rdb.Del(ctx, lockKey)
|
|||
|
|
|
|||
|
|
// 双重检查——防止自己等太久,另一个已经重建了
|
|||
|
|
data, _ = rdb.Get(ctx, "product:"+strconv.FormatInt(id, 10)).Bytes()
|
|||
|
|
if data != nil {
|
|||
|
|
var p Product
|
|||
|
|
json.Unmarshal(data, &p)
|
|||
|
|
return &p, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
p, _ := db.GetProduct(id)
|
|||
|
|
rdb.Set(ctx, "product:"+strconv.FormatInt(id, 10), toJSON(p), 30*time.Minute)
|
|||
|
|
return p, nil
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 方案 2:逻辑过期(Logical Expiration)
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
flowchart LR
|
|||
|
|
Read["读请求"] --> Check{"检查 expire_at"}
|
|||
|
|
Check -->|"未过期"| Return["直接返回旧值 ✅"]
|
|||
|
|
Check -->|"已过期"| Async["启动 goroutine 重建缓存"]
|
|||
|
|
Async --> ReturnOld["返回旧值(不阻塞)"]
|
|||
|
|
|
|||
|
|
subgraph "后台重建"
|
|||
|
|
Rebuild["查 DB 最新数据"] --> SetNew["覆盖 Redis value"]
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
style Return fill:#cfc,stroke:#090
|
|||
|
|
style ReturnOld fill:#ffd,stroke:#da0
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> [!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 // 未过期,直接返回
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 异步重建缓存(不阻塞当前请求)
|
|||
|
|
go rebuildCacheAsync(key, item.ExpireAt)
|
|||
|
|
return item.Content // 返回旧值,后台静默刷新
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 缓存雪崩(Avalanche)—— 大量 key 同时过期
|
|||
|
|
|
|||
|
|
> [!QUESTION] 穿透/击穿/雪崩 的区别是什么?
|
|||
|
|
> - **穿透**:数据**永远不存在**,请求直达 DB → 关注"过滤非法请求"
|
|||
|
|
> - **击穿**:单个**热点 key** 过期,瞬时洪峰 → 关注"防并发重建"
|
|||
|
|
> - **雪崩**:大批量 / **全部 key** 同时过期 → 关注"错峰 + 兜底"
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
flowchart LR
|
|||
|
|
subgraph "❌ 无抖动"
|
|||
|
|
T["TTL=30min"] -->|"t=30:00"| AllExpire["100% keys expire simultaneously"]
|
|||
|
|
AllExpire --> Crash["DB overload → crash"]
|
|||
|
|
style Crash fill:#fcc,stroke:#c00
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
subgraph "✅ 加随机抖动"
|
|||
|
|
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 平稳承受"| Safe
|
|||
|
|
P2 --> Safe
|
|||
|
|
P3 --> Safe
|
|||
|
|
P4 --> Safe
|
|||
|
|
P5 --> Safe
|
|||
|
|
style Safe fill:#cfc,stroke:#090
|
|||
|
|
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 "应用进程内"
|
|||
|
|
L1["L1 本地缓存<br/>sync.Map / bigcache<br/>⚡ ~1μs"]
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
subgraph "分布式层"
|
|||
|
|
L2["L2 Redis Cluster<br/>⚡ ~1ms"]
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
L1 -->|"miss"| L2
|
|||
|
|
L2 -->|"miss"| DB["MySQL / 外部 API<br/>⚡ ~5~50ms"]
|
|||
|
|
DB -->|"回填"| L2
|
|||
|
|
L2 -->|"广播失效"| L1
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 三、缓存设计原则总结
|
|||
|
|
|
|||
|
|
| # | 原则 | 说明 |
|
|||
|
|
|---|------|------|
|
|||
|
|
| 1 | 永远不信任单一数据源 | 缓存和 DB 保持最终一致 |
|
|||
|
|
| 2 | 设 TTL 是必须的 | 即使内存够也要设,防止永久占用 |
|
|||
|
|
| 3 | TTL 加随机抖动 | 避免大规模同时过期 |
|
|||
|
|
| 4 | 大 Key 拆小 | 单个 Key ≤ 5KB,防止网络阻塞 |
|
|||
|
|
| 5 | Pipeline 批量操作 | 减少 RTT,提升吞吐 |
|
|||
|
|
| 6 | 监控命中率 | 低于 80% 考虑调整策略 |
|
|||
|
|
| 7 | 不要缓存不可变数据 | 配置类可存本地内存 |
|
|||
|
|
| 8 | 敏感数据不入库 | 密码、手机号要脱敏或加密 |
|
|||
|
|
|
|||
|
|
## 四、常见架构模式速览
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
flowchart TD
|
|||
|
|
Client["客户端请求"]
|
|||
|
|
|
|||
|
|
Client --> LocalCache{"本地缓存?"}
|
|||
|
|
LocalCache -->|hit| Return["返回"]
|
|||
|
|
LocalCache -->|miss| Redis["Redis"]
|
|||
|
|
|
|||
|
|
Redis -->|hit| Return
|
|||
|
|
Redis -->|miss| DB["数据库"]
|
|||
|
|
|
|||
|
|
DB --> Redis
|
|||
|
|
Redis --> LocalCache
|
|||
|
|
|
|||
|
|
Writer["写操作"] -->|"先更 DB"| DB
|
|||
|
|
DB -->|"异步/同步刷缓存"| Redis
|
|||
|
|
|
|||
|
|
style Redis fill:#fda,stroke:#da0
|
|||
|
|
style LocalCache fill:#dfd,stroke:#090
|
|||
|
|
style DB fill:#ddf,stroke:#66c
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 五、缓存选型与演进路径
|
|||
|
|
|
|||
|
|
> [!QUESTION] 项目初期应该从哪一层开始做缓存?
|
|||
|
|
> **答案:先做 L2(Redis),稳定后再加 L1(本地缓存)。** 原因如下:
|
|||
|
|
|
|||
|
|
### 决策树
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
flowchart TD
|
|||
|
|
Start["有新需求要加缓存"] --> Q1{"数据是否频繁变化?"}
|
|||
|
|
|
|||
|
|
Q1 -->|"否 — 配置/字典"| Config["直接放应用内存<br/>不经过 Redis"]
|
|||
|
|
Q1 -->|"是"| Q2{"QPS > 5000?"}
|
|||
|
|
|
|||
|
|
Q2 -->|"否 — 低频场景"| Simple["L2 only: Cache-Aside<br/>+ Redis"]
|
|||
|
|
Q2 -->|"是 — 中频场景"| Medium["L2 + ReadThrough<br/>引入布隆过滤器防穿透"]
|
|||
|
|
Q2 -->|"是 — 高频热点"| HotKey["L1 + L2 多级缓存<br/>+ 逻辑过期防击穿"]
|
|||
|
|
|
|||
|
|
Config --> End["完成 ✅"]
|
|||
|
|
Simple --> End
|
|||
|
|
Medium --> End
|
|||
|
|
HotKey --> End
|
|||
|
|
|
|||
|
|
style Config fill:#ffd,stroke:#da0
|
|||
|
|
style Simple fill:#dfd,stroke:#090
|
|||
|
|
style Medium fill:#fdd,stroke:#900
|
|||
|
|
style HotKey fill:#fcc,stroke:#c00
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 不同阶段的关键关注点
|
|||
|
|
|
|||
|
|
| 阶段 | 核心目标 | 推荐模式 | 避坑指南 |
|
|||
|
|
|------|---------|---------|---------|
|
|||
|
|
| **V1 起步期** | 快速上线,能跑就行 | Cache-Aside + Redis | 不要过度设计,先解决有无问题 |
|
|||
|
|
| **V2 增长期** | 扛住流量高峰 | 布隆过滤器 + 互斥锁 | 关注监控指标,建立告警机制 |
|
|||
|
|
| **V3 大规模** | 极致性能和稳定性 | 多级缓存 + 逻辑过期 + 降级策略 | 一致性 vs 可用性的平衡取舍 |
|
|||
|
|
|
|||
|
|
> [!TIP] 一句话总结
|
|||
|
|
> **好的缓存架构不是设计出来的,是演进出來的**。先保证正确性,再优化性能,最后打磨可用性。
|
|||
|
|
|
|||
|
|
## 关联笔记
|
|||
|
|
|
|||
|
|
- [[hhs/Redis/09-高级特性]] — Lua 脚本 + 分布式锁
|
|||
|
|
- [[hhs/Redis/07-集群方案]] — Cluster 下的一致性挑战
|
|||
|
|
- [[hhs/Redis/README]] — 知识索引总览
|
|||
|
|
- [[hhs/GORM/09-钩子函数]] — GORM 生命周期中同步缓存的模式
|