10 — 缓存系统设计

核心规律:缓存是空间的换时间,一致性vs性能永远在权衡 一句话:没有缓存的系统是脆弱的,只有缓存的系统是不可靠的


一、核心规律

1.1 缓存三定律

1. 所有缓存都会失效 — 提前设计失效策略
2. 缓存命中不是100% — 接受Miss的存在
3. 缓存可能不一致 — 最终一致性优于强一致

1.2 缓存架构层次

flowchart TB
    subgraph L1["L1: CPU缓存"]
        C1["纳秒级"]
    end
    subgraph L2["L2: 内存缓存 Redis"]
        C2["微秒级 ← 应用层主要使用"]
    end
    subgraph L3["L3: 本地缓存 APCu"]
        C3["毫秒级"]
    end
    subgraph L4["L4: 数据库"]
        C4["毫秒级"]
    end
    subgraph L5["L5: 磁盘/对象存储"]
        C5["秒级"]
    end
    
    C2 -->|"热点数据"| C1
    C3 -->|"次热点"| C2
    C4 -->|"主要数据源"| C3
    C5 -->|"归档数据"| C4
    
    style C2 fill:#e8f5e9
    style C3 fill:#fff3e0
    style C4 fill:#e3f2fd

二、Redis核心

2.1 数据结构与场景

结构命令应用场景
StringSET/GET/INCR缓存、计数、分布式锁
HashHSET/HGET对象存储(用户信息)
ListLPUSH/RPOP消息队列、时间线
SetSADD/SISMEMBER标签、去重、抽奖
ZSetZADD/ZRANGE排行榜、延时队列
BitmapSETBIT/GETBIT用户签到、活跃统计
HyperLogLogPFADD/PFCOUNTUV计数
GeoGEOADD/GEODIST附近的人

2.2 持久化机制

flowchart LR
    subgraph RDB["RDB快照"]
        R1["定期全量快照"]
        R2["恢复快"]
        R3["可能丢数据"]
    end
    
    subgraph AOF["AOF日志"]
        A1["记录每次写操作"]
        A2["数据安全"]
        A3["文件大恢复慢"]
    end
    
    R1 & R2 & R3 --> BOTH["生产环境:RDB+AOF混合"]
    A1 & A2 & A3 --> BOTH
    
    style BOTH fill:#e8f5e9

推荐配置:

save 900 1      # 15分钟至少1个key变化,做快照
save 300 10     # 5分钟至少10个key变化,做快照
save 60 10000   # 1分钟至少10000个key变化,做快照
appendonly yes  # 开启AOF
appendfsync everysec  # 每秒刷盘

三、缓存三大问题

3.1 问题对比

flowchart TB
    subgraph 雪崩["❄️ 缓存雪崩"]
        S1["大量Key同时过期"]
        S2["Redis宕机"]
        S1 & S2 --> S3["所有请求打到DB<br/>→ DB崩溃"]
    end
    
    subgraph 穿透["🔍 缓存穿透"]
        P1["查询不存在的数据"]
        P1 --> P2["每次都绕过缓存<br/>→ DB压力"]
    end
    
    subgraph 击穿["💥 缓存击穿"]
        C1["热点Key过期瞬间"]
        C1 --> C2["大量请求同时打到DB<br/>→ 单Key压力"]
    end
    
    style S3 fill:#ffebee
    style P2 fill:#fff3e0
    style C2 fill:#e3f2fd

3.2 解决方案

问题原因解决方案
雪崩Key集中过期/Redis宕机随机TTL + 集群部署 + 限流降级
穿透查询不存在的数据布隆过滤器 + 缓存空值
击穿热点Key过期瞬间互斥锁 + 永不过期

3.3 代码示例

// 缓存空值防穿透
public function getUser(int $id): ?User
{
    $cacheKey = "user:{$id}";
    $data = Cache::get($cacheKey);
    
    if ($data === null) {
        $user = $this->repo->find($id);
        // 即使为null也缓存,设置较短TTL
        $ttl = $user ? 3600 : 60;
        Cache::put($cacheKey, $user?->toArray(), $ttl);
        return $user;
    }
    
    return $data ? (object) $data : null;
}
 
// 互斥锁防击穿
public function getHotProduct(int $id): Product
{
    $cacheKey = "product:hot:{$id}";
    
    return Cache::remember($cacheKey, 3600, function() use ($id) {
        return $this->repo->find($id)?->toArray();
    });
}

四、缓存策略

4.1 常见模式

模式说明一致性复杂度
Cache-Aside先查缓存,miss则查DB并写入最终一致
Read-Through缓存代理,应用只调缓存强一致
Write-Through写缓存同时写DB强一致
Write-Behind写缓存,异步写DB最终一致

4.2 缓存失效策略

失效时机选择:
├── TTL(时间过期):最简单,可能过期过早或过晚
├── 主动删除:写入时删除缓存,保证一致性
├── 延迟双删:先删后写再删,解决并发问题
└── 逻辑过期:不删缓存,后台异步刷新

五、缓存设计最佳实践

5.1 Key设计规范

格式:{业务名}:{实体}:{ID}:{字段}

示例:
  user:profile:123:name        # 用户基本信息
  order:list:status:paid       # 已支付订单列表
  product:hot:top10            # 热销商品TOP10

禁止:
  ❌ key = 'user_123'          # 无业务前缀
  ❌ key = 'a'                 # 过于简短

5.2 缓存设计 Checklist

□ 设置合理的TTL(避免永久缓存)
□ 缓存Key包含业务前缀(便于管理和清理)
□ 关键数据考虑缓存失效策略
□ 热点数据考虑永不过期+后台刷新
□ 监控缓存命中率和内存使用
□ 设计缓存降级方案(Redis挂了怎么办)

六、本章总结

核心规律:缓存是性能的加速器,也是复杂性的来源。用好缓存的关键是理解一致性与性能的二律背反。

关键记忆点

  • ✅ Redis八大数据结构,各有所长
  • ✅ RDB适合备份,AOF适合恢复
  • ✅ 穿透→布隆过滤器+空值缓存,击穿→互斥锁,雪崩→随机TTL
  • ✅ Cache-Aside是最常用的缓存模式

延伸阅读