多级缓存架构:本地、Redis 与数据库的分工

“加了 Redis 还是慢”——因为热数据不该只走 Redis,最快的是进程内缓存。多级缓存架构:本地缓存扛热数据、Redis 扛共享数据、数据库兜底。这篇文章是分级设计、一致性和踩坑记录。

一、为什么需要多级

单 本 层 地 1 2 3 缓 读 不 天 R . . . 存 耗 占 然 e ( 网 扛 d 网 热 R 进 ~ 络 住 i 络 e 程 0 、 热 s 开 k d 内 . 不 销 e i ) 0 打 k 的 : y s : 1 e 瓶 每 m R y 颈 次 打 挂 s e ( : 读 爆 了 ( d 每 都 单 全 比 i 个 要 节 挂 s 进 走 点 ( R 程 网 ( 没 e 都 络 前 有 d 有 ( 文 降 i 一 0 讲 级 s 份 . 过 ) ) 1 的 快 - 热 0 1 . k 0 5 e - m y 5 s 0 ) 问 题 倍 ) )

二、三级架构

请 求 │ ├ │ │ ├ │ │ └ ─ ─ ─ 第 第 第 查 1 命 未 2 命 未 3 库 中 命 中 命 级 率 中 级 率 中 级 → : : : 本 6 ↓ R + ↓ 数 回 地 0 e 1 据 填 缓 - d 5 库 存 8 i % ( R ( 0 s ( 兜 e 进 % ( 次 底 d 程 ( 共 热 ) i 内 热 享 数 s , 数 缓 据 C 据 存 ) → a ) ) f 回 f 填 e 本 i 地 n e / m a p )
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
func GetUser(id int64) *User {
    // L1:本地缓存
    if u, ok := localCache.Get(id); ok {
        return u.(*User)
    }
    // L2:Redis
    val, err := redis.Get(ctx, key(id))
    if err == nil && val != "" {
        u := unmarshal(val)
        localCache.Set(id, u, 1*time.Second)   // 回填 L1(短 TTL)
        return u
    }
    // L3:数据库
    u, err := db.FindUser(id)
    if err == nil {
        redis.Set(ctx, key(id), marshal(u), 10*time.Minute)  // 回填 L2
        localCache.Set(id, u, 1*time.Second)                 // 回填 L1
    }
    return u
}

三、各级 TTL 设计(关键)

原 本 R 数 为 则 地 e 据 什 : 缓 d 库 么 本 T 1 越 存 i : 本 地 T s 靠 : s 无 地 缓 L 近 1 : 缓 存 T 用 s 1 T 存 没 越 T 户 ( 0 T 有 长 L , 短 m L T 失 : T i ( T 效 → 最 T T n 真 L 通 多 L T ( 身 知 更 L 共 ) 要 ( 新 1 越 — 享 短 进 后 短 — 缓 : 程 读 秒 ( 更 存 间 到 旧 一 新 , 不 旧 数 致 快 更 通 数 据 性 , 新 ) 据 , 损 容 频 越 可 失 忍 率 久 接 越 秒 决 受 小 级 定 ) 不 ) 一 致 )

四、一致性处理

写 问 方 实 操 题 案 践 作 先 : : 1 2 3 : : 更 D . . . 延 新 B 迟 延 版 订 双 D 更 迟 本 阅 删 B 新 双 号 成 删 : b + → 功 : 缓 i , 删 存 n R 删 R 带 l e e R 版 o d R d e 本 g i e i d , s d s i D 删 i s B 缓 T s 删 存 T 除 → 版 ( L → 失 本 可 败 等 更 靠 兜 ( 高 , 底 本 → 5 架 ( 地 0 → 构 最 缓 R 0 重 坏 存 e m 不 ) 多 靠 d s 读 活 短 i 几 s → 分 T 钟 T 有 再 旧 L 旧 删 数 数 一 据 自 据 次 ) 然 过 期 )

五、缓存穿透防护

多级缓存下穿透更可怕(每一级都 miss 才到 DB):

穿 防 透 护 场 : 1 2 3 景 . . . : 查 缓 布 接 询 存 隆 口 不 空 过 层 存 值 滤 校 在 ( 器 验 的 不 ( ( 存 不 非 I 在 存 法 D 的 在 参 也 的 数 → 缓 直 直 存 接 接 每 , 拒 返 级 T 绝 回 都 T ) ) L m i 短 s s 3 0 → s ) 直 接 打 D B

六、效果数据

场 景 : 无 单 多 简 缓 层 级 历 存 缓 L 详 : R 存 1 情 D e : 读 B d 本 命 ( i 地 中 读 5 s 命 率 多 m : 中 写 s 0 6 少 , . 0 5 ) 高 3 . % 峰 m 0 期 s 1 / , m D R s L B e , 2 d R 压 i e 命 力 s d 中 大 i 率 Q s P 2 S Q 5 P % 高 S / 降 D 7 B 0 % 只 + 扛 1 0 %

七、踩坑记录

  1. 本地缓存无限涨:每个进程缓存一堆 key → OOM → 本地缓存必须限容量(LRU)+ 短 TTL
  2. 本地缓存和 Redis 不一致:进程 A 更新了,进程 B 本地还是旧的 → 短 TTL 兜底(可接受的秒级不一致)
  3. 穿透没防护:恶意查询不存在的 key → DB 被打 → 空值缓存 + 布隆过滤器
  4. 回填风暴:缓存全过期,所有请求同时查 DB → 回填加锁(singleflight,只让一个请求查库)
1
2
3
4
5
// 回填防击穿:singleflight(一个 key 同时只查一次库)
var g singleflight.Group
u, err, _ := g.Do(key, func() (any, error) {
    return db.FindUser(id)   // 只有一个请求查库,其他等待
})

八、什么时候用多级

✅ ✅ ✅ ❌ ❌ 读 接 有 写 数 多 口 热 多 据 写 延 读 一 少 迟 k 少 致 的 敏 e ( 性 核 感 y 缓 要 心 ( 存 求 数 < 风 命 极 据 险 中 高 ( 5 率 ( 用 m 低 秒 户 s , 级 信 没 不 息 要 意 能 、 求 义 容 商 ) ) 忍 品 ) 、 — 配 — 置 考 ) 虑 其 他 方 案

总结

多级缓存的核心认知:

  1. 三级分工:本地(最快/最短 TTL)/ Redis(共享)/ DB(兜底)
  2. TTL 越近越短:秒级不一致可接受,换来性能
  3. 一致性用延迟双删 + TTL 兜底
  4. 穿透/击穿防护必做:空值缓存 + singleflight

多级缓存是"用一致性换性能"的经典权衡:理解每一级的作用和代价,设计才有底气——不是"缓存越多越好",是"每一级都各司其职"。