缓存架构

缓存是提升系统性能、降低数据库压力的核心手段,现代系统通常采用三级缓存架构。

三级缓存

请求到来
    │
    ▼
L1:本地缓存(进程内,JVM 堆内存)
    │ miss(未命中)
    ▼
L2:分布式缓存(Redis Cluster)
    │ miss
    ▼
L3:数据库(MySQL / PostgreSQL)
    │
    └── 查询结果 → 回填 L2 → 回填 L1 → 返回
层级实现延迟容量特点
L1 本地Caffeine / Guava< 1msMB 级最快,不共享,有内存限制
L2 分布式Redis Cluster1~5msGB 级跨实例共享,支持持久化
L3 数据库MySQL / PG10~100msTB 级持久化,支持复杂查询

L1 本地缓存

Caffeine

Java 生态最佳本地缓存,基于 W-TinyLFU 算法,命中率比 LRU 高。

Cache<String, User> cache = Caffeine.newBuilder()
    .maximumSize(10_000)           // 最大条数
    .expireAfterWrite(5, MINUTES)  // 写后过期
    .refreshAfterWrite(1, MINUTES) // 写后异步刷新
    .build();

适用数据:

  • 基础配置(不频繁变更)
  • 热点商品信息
  • 用户权限数据

注意: 多实例部署时各节点缓存独立,数据可能不一致,需要通过 MQ 或 Redis Pub/Sub 广播失效。

L2 分布式缓存(Redis)

Redis Cluster 架构

客户端
   │ 根据 key hash slot 路由
   ▼
┌────────────┐  ┌────────────┐  ┌────────────┐
│  Master 0  │  │  Master 1  │  │  Master 2  │
│ Slot 0~5460│  │Slot 5461~  │  │Slot 10923~ │
│            │  │   10922    │  │   16383    │
└─────┬──────┘  └─────┬──────┘  └─────┬──────┘
      │               │               │
┌─────▼──────┐  ┌─────▼──────┐  ┌─────▼──────┐
│  Slave 0   │  │  Slave 1   │  │  Slave 2   │
└────────────┘  └────────────┘  └────────────┘
  • 16384 个 Hash Slot 分布在各 Master 上
  • 每个 Master 有 Slave 备份,Master 宕机自动切换
  • 客户端缓存 Slot 映射,直接路由到正确节点

Redis 数据结构选择

结构命令典型场景
StringGET/SET简单 KV、计数器、分布式锁
HashHGET/HSET对象存储 → Hash
ZSetZADD/ZREVRANGE排行榜、延迟队列 → ZSet
ListLPUSH/RPOP消息队列、最新N条
SetSADD/SMEMBERS标签、去重、共同关注
BitmapSETBIT/BITCOUNT用户签到、在线状态
HyperLogLogPFADD/PFCOUNT近似统计 UV
StreamXADD/XREAD轻量消息队列

缓存一致性策略

Cache Aside(旁路缓存,最常用)

读:
  查缓存
    命中 → 返回
    未命中 → 查 DB → 写缓存 → 返回

写:
  更新 DB → 删除缓存(不是更新缓存)

为什么删缓存而不是更新? 更新缓存在并发写场景会产生数据竞争,删除是幂等操作更安全。

双删策略(应对并发读写)

写操作:
  1. 先删缓存
  2. 更新 DB
  3. 延迟 500ms 再删一次缓存(防止步骤2期间有读请求重建了旧缓存)

消息队列异步更新

DB 变更 → Binlog → Canal → Kafka → 缓存更新服务 → 删除/更新 Redis

适用于对一致性要求不是极端严格,但需要自动化处理缓存失效的场景。

缓存问题与解决

缓存穿透

问题: 查询不存在的数据,缓存和 DB 都没有,请求全部打到 DB。

解决:

  • 缓存空值(set key "" expire 60s)
  • 布隆过滤器(Bloom Filter):写入时记录,查询时先判断是否可能存在

缓存击穿

问题: 热点 key 过期瞬间,大量并发请求同时穿透到 DB。

解决:

  • 热点 key 不设置过期时间,由后台任务定期刷新
  • 互斥锁:只让一个请求查 DB,其余等待
  • 逻辑过期:缓存中存储过期时间,读到时异步刷新

缓存雪崩

问题: 大量 key 同时过期,或 Redis 集群宕机,流量涌向 DB。

解决:

  • 过期时间加随机抖动(expireTime + random(0, 300s))
  • Redis 高可用(Cluster / Sentinel)
  • 服务降级:Redis 不可用时限流或返回默认值
  • 本地缓存兜底

分布式锁(基于 Redis)

// SET key value NX PX 30000
// NX: 不存在才设置
// PX: 毫秒级过期时间
 
Boolean locked = redis.set(lockKey, requestId, SetOption.NX, 30_000);
if (locked) {
    try {
        // 临界区业务逻辑
    } finally {
        // 验证 value 是自己的再删除(防误删)
        if (requestId.equals(redis.get(lockKey))) {
            redis.del(lockKey);
        }
    }
}

Redisson:生产环境推荐使用 Redisson,内置看门狗(Watchdog)自动续期,防止业务未执行完锁已过期。


Java 实战(Spring Boot / Cloud)