缓存是性能优化的第一现场。从线程级 ThreadLocal 到进程级 Caffeine,再到分布式 Redis,每一层缓存都是空间换时间的权衡。本文逐层拆解缓存设计,聚焦一致性、并发、过期三大挑战。
1. 缓存层级金字塔
访问速度 ▲
│ L1 CPU Cache < 1ns
│ L2/L3 CPU Cache < 10ns
│ ThreadLocal ~10ns
│ Caffeine (本地) ~100ns
│ Redis (网络) ~1ms
│ 数据库 ~10ms
│ 外部接口 ~100ms+
└────────────────────────
容量 →
企业常用的三级缓存:
- L1:Caffeine(JVM 本地,读 QPS 10 万+)
- L2:Redis(分布式共享,读 QPS 5-10 万)
- L3:数据库(最终持久化)
2. Caffeine:高性能本地缓存
2.1 核心特性
- W-TinyLFU 驱逐算法:频率 + 时效双维度,命中率优于 LRU/LFU
- Window Cache:热点数据保护窗口
- 异步加载 / 刷新:不阻塞读线程
- 弱/软引用支持:防止内存泄漏
2.2 使用示例
LoadingCache<String, User> userCache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(Duration.ofMinutes(10))
.refreshAfterWrite(Duration.ofMinutes(5)) // 异步刷新,不阻塞读
.recordStats() // 开启统计
.build(key -> userRepository.findById(key)); // CacheLoader
// 使用
User user = userCache.get(userId); // 缓存未命中 → 自动加载
userCache.invalidate(userId); // 主动失效
2.3 W-TinyLFU 原理
访问请求
↓
Window Cache (1% 空间,纯粹 FIFO)
├─ 命中 → 移到 TinyLFU 过滤器评估
│
└─ Window淘汰 → 进入 TinyLFU 淘汰区
↓
TinyLFU (99% 空间)
├─ Doorkeeper Bloom Filter → 严格频率统计
├─ Count-Min Sketch → 近似频率计数(节省内存)
└─ 淘汰低频 → 保留高频
对比其他算法:
| 算法 | 特点 | 劣势 |
|---|---|---|
| LRU | 淘汰最近最少使用 | 对突发流量敏感,历史热点被覆盖 |
| LFU | 淘汰使用频率最低 | 累积频率,新热点无法进入 |
| W-TinyLFU | 窗口 + 频率 | 略复杂,但命中率高 |
3. Spring Cache 抽象
3.1 注解驱动缓存
@Service
public class ProductService {
@Cacheable(value = "products", key = "#id", unless = "#result == null")
public Product getProduct(Long id) {
return productDao.findById(id);
}
@CachePut(value = "products", key = "#product.id")
public Product updateProduct(Product product) {
return productDao.save(product);
}
@CacheEvict(value = "products", key = "#id")
public void deleteProduct(Long id) {
productDao.deleteById(id);
}
@Caching(evict = {
@CacheEvict(value = "products", key = "#product.id"),
@CacheEvict(value = "products", key = "'list'")
})
public Product saveProduct(Product product) {
return productDao.save(product);
}
}
3.2 自定义 CacheManager
@Configuration
public class CacheConfig {
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10))
.serializeKeysWith(RedisSerializationContext.SerializationPair
.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer()));
return RedisCacheManager.builder(factory)
.cacheDefaults(config)
.withCacheConfiguration("products", config.entryTtl(Duration.ofMinutes(5)))
.withCacheConfiguration("users", config.entryTtl(Duration.ofHours(1)))
.transactionAware()
.build();
}
}
4. Redis 缓存实战
4.1 序列化选型
| 序列化器 | 优点 | 缺点 |
|---|---|---|
JdkSerializationRedisSerializer | 原生兼容 | 二进制不可读、体积大 |
StringRedisSerializer | 可读 | 仅支持 String |
Jackson2JsonRedisSerializer | JSON 可读、跨语言 | 日期/类型信息需处理 |
GenericJackson2JsonRedisSerializer | 自动类型信息 | 类型安全需开启白名单 |
Kryo / FST(自定义) | 体积小、速度快 | 额外依赖、兼容性 |
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// Key: String
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
// Value: JSON with type info
ObjectMapper mapper = new ObjectMapper()
.activateDefaultTyping(
BasicPolymorphicTypeValidator.builder()
.allowIfBaseType(Object.class).build(),
ObjectMapper.DefaultTyping.NON_FINAL
);
template.setValueSerializer(new GenericJackson2JsonRedisSerializer(mapper));
template.afterPropertiesSet();
return template;
}
4.2 多级缓存架构 (Caffeine + Redis)
@Component
public class MultiLevelCache {
private final LoadingCache<String, Object> localCache;
private final RedisTemplate<String, Object> redisTemplate;
public Object get(String key) {
// L1: 本地缓存
Object value = localCache.getIfPresent(key);
if (value != null) return value;
// L2: Redis
value = redisTemplate.opsForValue().get(key);
if (value != null) {
localCache.put(key, value); // 回填本地
return value;
}
// L3: 数据库(CacheLoader 自动处理)
value = loadFromDatabase(key);
if (value != null) {
redisTemplate.opsForValue().set(key, value, Duration.ofMinutes(10));
localCache.put(key, value);
}
return value;
}
}
5. 缓存三大经典问题
5.1 缓存穿透(Cache Penetration)
场景:查询不存在 key(如 id=-1),绕过缓存直达数据库。
方案:
// 1. 布隆过滤器(Bloom Filter)预判定
@Component
public class BloomFilterCache {
private final BloomFilter<String> bloomFilter;
public Object get(String key) {
if (!bloomFilter.mightContain(key)) {
return null; // 肯定不存在,直接返回
}
// 继续查缓存/数据库
}
}
// 2. 缓存空值(短期)
if (value == null) {
redisTemplate.opsForValue().set(key, EMPTY_VALUE, Duration.ofMinutes(5));
}
布隆过滤器参数:
- 预计元素
n,期望误判率p→ 位图大小m = -n * ln(p) / (ln2)^2,哈希函数k = m/n * ln2 - Guava:
BloomFilter.create(Funnels.stringFunnel(), 1000000, 0.01)
5.2 缓存击穿(Cache Breakdown)
场景:热点 key 过期瞬间,大量请求同时打到数据库。
方案:互斥锁 + 异步重建
public Object getWithMutex(String key) {
Object value = redisTemplate.opsForValue().get(key);
if (value != null) return value;
// 分布式锁
String lockKey = "lock:" + key;
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", Duration.ofSeconds(10));
if (Boolean.TRUE.equals(locked)) {
try {
value = loadFromDatabase(key);
redisTemplate.opsForValue().set(key, value, Duration.ofMinutes(10));
} finally {
redisTemplate.delete(lockKey);
}
} else {
// 其他线程等待后重试
Thread.sleep(100);
return getWithMutex(key);
}
return value;
}
Redisson 实现:
RLock lock = redissonClient.getLock("cache:lock:" + key);
try {
lock.lock(10, TimeUnit.SECONDS);
// 双重检查
value = redisTemplate.opsForValue().get(key);
if (value == null) {
value = loadFromDatabase(key);
redisTemplate.opsForValue().set(key, value, Duration.ofMinutes(10));
}
} finally {
lock.unlock();
}
5.3 缓存雪崩(Cache Avalanche)
场景:大量 key 同时过期 / Redis 宕机,数据库瞬时压力暴增。
方案:
// 1. 过期时间加随机偏移
Duration baseTtl = Duration.ofMinutes(30);
Duration jitter = Duration.ofSeconds(ThreadLocalRandom.current().nextInt(300));
redisTemplate.opsForValue().set(key, value, baseTtl.plus(jitter));
// 2. 多级缓存 + 熔断
// L1 Caffeine 即使 Redis 宕机,仍有 10 分钟缓冲
// 3. Redis 高可用
// 主从 + Sentinel / Cluster;K8s 环境下持久化 PVC + StatefulSet
6. 缓存一致性策略
6.1 Cache-Aside(旁路缓存)
读:Cache → Miss → DB → 写入 Cache
写:更新 DB → 删除 Cache(非更新 Cache,避免并发脏写)
延迟双删(最终一致性增强):
public void update(Product product) {
cache.delete("product:" + product.getId()); // 先删
db.update(product); // 再写
// 异步延迟再删(覆盖读写并发窗口)
executor.schedule(() -> cache.delete("product:" + product.getId()),
500, TimeUnit.MILLISECONDS);
}
6.2 Read-Through / Write-Through
缓存层封装了 DB 操作,代码只与缓存交互。需使用支持此模式的框架(如 Redis with CacheStore)。
6.3 Write-Behind(异步写回)
写请求 → 写入缓存 → 立即返回
└── 异步队列 → 批量写入 DB
适用:写多读少、允许短暂不一致(如计数器、日志)。
6.4 Canal + 缓存订阅
MySQL Binlog → Canal Server → Kafka/消息队列 → 缓存更新/删除
优势:业务代码零侵入,最终一致性。
7. Redisson 高级功能
// 分布式对象
RMap<String, User> map = redisson.getMap("users");
RLocalCachedMap<String, User> localMap = redisson.getLocalCachedMap("users",
LocalCachedMapOptions.<String, User>defaults()
.evictionPolicy(LocalCachedMapOptions.EvictionPolicy.LRU)
.timeToLive(10, TimeUnit.MINUTES));
// 分布式锁(可重入、看门狗自动续期)
RLock lock = redisson.getLock("order:" + orderId);
lock.lock();
try { processOrder(); } finally { lock.unlock(); }
// 分布式计数器
RAtomicLong counter = redisson.getAtomicLong("counter");
counter.incrementAndGet();
8. 性能基准参考
| 操作 | Caffeine (本地) | Redis (单节点) | Redis (集群) |
|---|---|---|---|
| 读 | ~10M ops/s | ~100K ops/s | ~80K ops/s |
| 写 | ~5M ops/s | ~50K ops/s | ~40K ops/s |
| 内存/M条目 | ~200MB | ~400MB | ~400MB |
| 序列化损耗 | ~0 | 10-30% | 10-30% |
延伸阅读
- 分布式锁与协调服务 — Redisson 锁与 ZooKeeper 对比
- ORM 框架 — ORM 二级缓存体系
- Spring Boot 核心原理与自动配置 — Spring Cache 自动配置原理
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。