Files

170 lines
6.4 KiB
Markdown
Raw Permalink 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, multi-level-cache, caffeine, cache-consistency, avalanche]
create time: 2026-08-08 18:43
update time: 2026-08-08 18:43
---
# 多级缓存架构设计
## 概述
在大型分布式系统中,仅靠 Redis 单级缓存已不足以支撑超高并发场景。多级缓存通过在客户端本地(L1)和远程服务(L2)之间分层存储热点数据,大幅降低远程调用延迟和网络带宽消耗。本文介绍 Caffeine 到 Redis 到 MySQL 的三级架构设计及一致性、雪崩等核心挑战的解决方案。
## 核心原理
### 三级缓存层次
```mermaid
graph LR
A["应用服务 App Service"] -->|"L1: Caffeine<br/>内存缓存 (微秒)"| B["应用服务 App Service"]
B -->|"L2: Redis<br/>远程缓存 (毫秒)"| C["MySQL<br/>持久化存储"]
A -->|"miss"| B
B -->|"miss"| C
```
**L1 — Caffeine(本地缓存)**:
- 基于 JVM 堆内内存,读写延迟小于 1 微秒
- 支持 LRU、LFU、Window LFU 多种淘汰算法
- 通过 maximumSize 限制内存占用,自动驱逐过期条目
- 局限:多实例间数据无法同步,每个实例有自己的缓存视图
**L2 — Redis(远程缓存)**:
- 集中式共享缓存,所有实例共享同一份数据
- 网络 RTT 通常在 1~5ms(同城)到 50ms+(跨城)
- 支持 TTL、持久化、复杂数据结构
**L3 — MySQL(持久层)**:
- 最终数据来源,保证数据的持久性和强一致性
- 查询延迟通常 10ms~数百 ms
### 缓存一致性挑战
多级缓存最大的痛点是数据更新时如何让所有层的缓存保持一致。
```mermaid
sequenceDiagram
participant Writer as 写请求
participant DB as MySQL
participant Redis as L2: Redis
participant App1 as 实例A (L1)
participant App2 as 实例B (L1)
Writer->>DB: UPDATE key = val
DB-->>Writer: OK
Writer->>Redis: DEL key
Redis-->>Writer: OK
Note over App1,App2: 注意:此处只删除了 Redis<br/>L1 缓存需要下次访问时刷新
App1->>Redis: GET key (miss)
Redis->>DB: SELECT * FROM ...
DB-->>Redis: result
Redis->>App1: result
App1->>App1: 写入 L1 Caffeine
```
> [!TIP]
> 最实用的策略是先删缓存再更新 DB(或先更新 DB 再删缓存)。推荐先更新 DB 再删缓存因为后一种情况下极端竞态(读请求在新旧值切换期间读到旧值并回写到缓存)的概率更低。绝不使用先删缓存再写 DB——那会导致写操作完成后、DB 写入前的窗口期内读请求拿到陈旧缓存。
### 二级缓存失效传播方案
当多个应用实例同时修改同一个 key 时,可能出现两个问题:
1. **重复重建**:多个实例同时发现缓存缺失,各自去查 DB 并重写缓存
2. **脏数据残留**:L1 缓存不知道 L2 已经被删除
解决策略:
| 方案 | 做法 | 优缺点 |
|------|-----|--------|
| **短 TTL + 异步刷新** | 给缓存设置随机短 TTL(如 5~15min),后台线程提前 2min 刷新 | 简单有效,容忍短暂不一致 |
| **消息队列广播** | DB 更新后发 MQ,各实例监听后清除自己的 L1 | 实时性好,但增加系统复杂度 |
| **Canal + binlog 监听** | 通过 Canal 解析 MySQL binlog,自动推送 invalidate 事件 | 解耦彻底,适合大规模部署 |
> [!WARNING]
> 不要依赖定时扫描比对来做一致性校验——延迟太高且成本高。生产环境首选方案:MQ 或 binlog 监听加短 TTL 兜底。
### 过期时间随机化防雪崩
大量缓存同时过期会导致请求瞬间穿透到数据库,引发雪崩。解决方法是在 TTL 基础上加上随机偏移量:
```go
import "math/rand"
func generateTTL(baseMinutes int, jitterPercent int) time.Duration {
jitter := baseMinutes * jitterPercent / 100
actualMinutes := baseMinutes - jitter + rand.Intn(2*jitter)
return time.Duration(actualMinutes) * time.Minute
}
// 例如 baseMinutes=10, jitterPercent=30
// 实际 TTL 落在 [7, 13] 分钟之间均匀分布
```
### 缓存穿透防护
场景:恶意用户或异常流量反复查询不存在的 key,绕过缓存直接打到 DB。
防护策略:
| 策略 | 做法 | 适用场景 |
|------|-----|---------|
| **空值缓存** | 查询结果为空时也缓存一个特殊标记(如 nil),设极短 TTL(30s~2min) | 适用于不存在的数据比例较低的场景 |
| **布隆过滤器** | 在缓存前先用 Bloom Filter 判断 key 是否存在 | 适用于 key 集合相对稳定、允许误判的场景 |
| **接口层鉴权限流** | 对高频无效查询做 IP 或 token 级别的限流 | 作为辅助防线 |
## 代码示例
Go 中用 singleflight 防止缓存击穿:
```go
import "golang.org/x/sync/singleflight"
type Cache struct {
group singleflight.Group
mu sync.RWMutex
local map[string]cache.Entry
}
func (c *Cache) Get(ctx context.Context, key string,
fn func() (interface{}, error)) (interface{}, error) {
// 1. L1 命中直接返回
c.mu.RLock()
if entry, ok := c.local[key]; ok && !entry.IsExpired() {
c.mu.RUnlock()
return entry.Value, nil
}
c.mu.RUnlock()
// 2. L1 miss + L2 miss → singleflight 阻止并发重复查 DB
val, err, _ := c.group.Do(key, func() (interface{}, error) {
val, err := redisGet(ctx, key)
if err != nil {
val, err = fn() // 查 DB
if err == nil {
redisSetWithTTL(ctx, key, val, generateTTL(10, 30))
c.setLocal(key, val, 15*time.Minute)
}
}
return val, err
})
return val, err
}
```
## 实践场景
1. **商品详情页缓存**:电商商品 SKU 信息变更频率低(小时级)、读请求极高(万 QPS),非常适合多级缓存。L1 存热点 SKU,L2 存全量活跃 SKU,DB 做兜底。
2. **配置中心类数据**:开关配置、规则引擎参数等全局配置,更新时通过 MQ 广播失效,L1 TTL 设 1 小时加后台预刷。
3. **限流计数**:高频调用的限流 counter 不适合放多级缓存(每次都涉及网络开销),直接用 Redis INCR 或本地原子计数器即可。
4. **容量规划参考**:假设单机 QPS 10000,L1 hit rate 90%,L2 hit rate 80%(相对 L1 miss),则对外部 Redis 的请求约 1000 × (1 - 0.8) = 200 QPS。这是评估集群规模的核心指标。
## 关联笔记
- [[03.Redis/core/Redis 五大核心数据结构]]
- [[03.Redis/strategies/缓存穿透击穿雪崩解决方案]]
- [[03.Redis/strategies/HeavyKeeper 热点探测算法]]