缓存架构
缓存是提升系统性能、降低数据库压力的核心手段,现代系统通常采用三级缓存架构。
三级缓存
请求到来
│
▼
L1:本地缓存(进程内,JVM 堆内存)
│ miss(未命中)
▼
L2:分布式缓存(Redis Cluster)
│ miss
▼
L3:数据库(MySQL / PostgreSQL)
│
└── 查询结果 → 回填 L2 → 回填 L1 → 返回
| 层级 | 实现 | 延迟 | 容量 | 特点 |
|---|---|---|---|---|
| L1 本地 | Caffeine / Guava | < 1ms | MB 级 | 最快,不共享,有内存限制 |
| L2 分布式 | Redis Cluster | 1~5ms | GB 级 | 跨实例共享,支持持久化 |
| L3 数据库 | MySQL / PG | 10~100ms | TB 级 | 持久化,支持复杂查询 |
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 数据结构选择
| 结构 | 命令 | 典型场景 |
|---|---|---|
| String | GET/SET | 简单 KV、计数器、分布式锁 |
| Hash | HGET/HSET | 对象存储 → Hash |
| ZSet | ZADD/ZREVRANGE | 排行榜、延迟队列 → ZSet |
| List | LPUSH/RPOP | 消息队列、最新N条 |
| Set | SADD/SMEMBERS | 标签、去重、共同关注 |
| Bitmap | SETBIT/BITCOUNT | 用户签到、在线状态 |
| HyperLogLog | PFADD/PFCOUNT | 近似统计 UV |
| Stream | XADD/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)自动续期,防止业务未执行完锁已过期。