多级缓存架构:本地、Redis 与数据库的分工#
“加了 Redis 还是慢”——因为热数据不该只走 Redis,最快的是进程内缓存。多级缓存架构:本地缓存扛热数据、Redis 扛共享数据、数据库兜底。这篇文章是分级设计、一致性和踩坑记录。
一、为什么需要多级#
二、三级架构#
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 设计(关键)#
四、一致性处理#
五、缓存穿透防护#
多级缓存下穿透更可怕(每一级都 miss 才到 DB):
六、效果数据#
七、踩坑记录#
- 本地缓存无限涨:每个进程缓存一堆 key → OOM → 本地缓存必须限容量(LRU)+ 短 TTL
- 本地缓存和 Redis 不一致:进程 A 更新了,进程 B 本地还是旧的 → 短 TTL 兜底(可接受的秒级不一致)
- 穿透没防护:恶意查询不存在的 key → DB 被打 → 空值缓存 + 布隆过滤器
- 回填风暴:缓存全过期,所有请求同时查 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) // 只有一个请求查库,其他等待
})
|
八、什么时候用多级#
多级缓存的核心认知:
- 三级分工:本地(最快/最短 TTL)/ Redis(共享)/ DB(兜底)
- TTL 越近越短:秒级不一致可接受,换来性能
- 一致性用延迟双删 + TTL 兜底
- 穿透/击穿防护必做:空值缓存 + singleflight
多级缓存是"用一致性换性能"的经典权衡:理解每一级的作用和代价,设计才有底气——不是"缓存越多越好",是"每一级都各司其职"。
微信公众号「福清而不淡」
后端架构 · AI 工程 · 云原生的一线实践,扫码关注,不错过更新。
本文已同步发布到公众号,欢迎留言交流。