在分布式系统中,缓存是提升性能的银弹,但很多开发者往往只关注远程缓存(如Redis)而忽略了本地缓存的价值。我曾经接手过一个电商项目,商品详情页接口在促销期间RT(响应时间)高达300ms,经过分析发现80%的请求都在反复查询相同的商品数据。即使使用了Redis,网络I/O和序列化开销仍然成为性能瓶颈。
让我们做个简单计算:假设一个Key在Redis中的查询耗时1ms,看起来很快对吧?但在1000QPS的场景下:
这些隐藏成本在高压下会被放大。而本地缓存(如Caffeine)的访问只需要50纳秒左右,相差5个数量级。
根据我的实战观察,在典型互联网应用中:
这就为多级缓存提供了理论基础:将最热的数据放在访问成本最低的存储层。
我们采用经典的三层架构:
| 层级 | 技术实现 | 延迟 | 容量 | 命中率目标 | 适用场景 |
|---|---|---|---|---|---|
| L1 | Caffeine | 50ns | 10MB | 80% | 进程内零拷贝 |
| L2 | Redis | 1ms | 100GB | 15% | 集群共享 |
| L3 | MySQL | 10ms+ | TB | 5% | 持久化存储 |
经验提示:单机QPS 1万时,L1命中率每提升1%,CPU使用率可下降3%
只需要3个基础依赖:
xml复制<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.1.8</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
配置示例:
yaml复制spring:
cache:
type: caffeine
caffeine:
spec: maximumSize=10000,expireAfterWrite=60s
redis:
host: 127.0.0.1
port: 6379
timeout: 200ms
lettuce:
pool:
max-active: 64
java复制@Component
@Slf4j
public class CacheTemplate<K, V> {
private final Cache<K, V> local = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(Duration.ofSeconds(60))
.recordStats() // 开启命中率统计
.build();
@Autowired
private RedisTemplate<K, V> redisTemplate;
public V get(K key, Supplier<V> dbFallback) {
// L1查询
V v = local.getIfPresent(key);
if (v != null) {
log.debug("L1 hit {}", key);
return v;
}
// L2查询
v = redisTemplate.opsForValue().get(key);
if (v != null) {
local.put(key, v); // 回填L1
log.debug("L2 hit {}", key);
return v;
}
// L3查询
v = dbFallback.get();
if (v != null) {
set(key, v); // 双写
}
return v;
}
// 其他方法省略...
}
java复制@RestController
@RequestMapping("/api/product")
@RequiredArgsConstructor
public class ProductController {
private final CacheTemplate<Long, Product> cache;
private final ProductRepository repository;
@GetMapping("/{id}")
public Product getProduct(@PathVariable Long id) {
return cache.get(id, () -> repository.findById(id).orElse(null));
}
}
java复制public V get(K key, Supplier<V> dbFallback) {
// 已有代码...
// 处理空值
if (v == null) {
local.put(key, (V) NULL_OBJECT); // 特殊空值标记
redisTemplate.opsForValue().set(key, (V) NULL_OBJECT, Duration.ofSeconds(5));
}
return v == NULL_OBJECT ? null : v;
}
对于热点Key:
对于大Key(>1MB):
java复制// 分片存储
public void setLargeData(K key, V value) {
if (serializedSize(value) > 1_000_000) {
Map<String, String> chunks = splitToChunks(value);
redisTemplate.opsForHash().putAll("large:"+key, chunks);
} else {
set(key, value);
}
}
java复制@EventListener(ApplicationReadyEvent.class)
public void warmUpCache() {
List<Long> hotItems = repository.findTop100ByOrderByViewCountDesc()
.stream().map(Product::getId)
.collect(Collectors.toList());
// 并行预热
hotItems.parallelStream().forEach(id ->
cache.get(id, () -> repository.findById(id).orElse(null))
);
}
java复制private Duration randomTTL(long baseSeconds) {
long variation = ThreadLocalRandom.current().nextLong(0, 300);
return Duration.ofSeconds(baseSeconds + variation);
}
public void set(K key, V value) {
local.put(key, value);
redisTemplate.opsForValue().set(
key,
value,
randomTTL(300) // 5分钟±随机偏移
);
}
java复制@Bean
public MeterBinder caffeineMetrics(Cache<K, V> localCache) {
return registry -> CaffeineMetrics.monitor(registry, localCache, "product_cache");
}
关键监控指标:
cache_hit_rate:低于70%需告警cache_eviction_count:突增表示容量不足cache_load_time:加载耗时监控sql复制# L1缓存命中率
100 * sum(rate(cache_hits_total[1m]))
/ sum(rate(cache_requests_total[1m]))
# Redis内存使用
redis_memory_used_bytes / redis_memory_max_bytes
使用wrk2压测结果:
| 场景 | 平均RT | P99 RT | QPS | CPU使用率 |
|---|---|---|---|---|
| 直接查DB | 28ms | 120ms | 1,200 | 65% |
| 仅Redis | 5.1ms | 18ms | 8,000 | 40% |
| 三级缓存 | 1.9ms | 4ms | 15,000 | 25% |
在实际项目中,这套方案将商品查询接口从平均45ms优化到了3ms,在双十一期间成功支撑了平时10倍的流量。
对于更复杂的场景,可以考虑:
我在实际项目中发现,合理的缓存层级设计加上精细化的监控,往往比单纯增加硬件资源更有效。曾经通过调整本地缓存大小和过期策略,在不增加服务器的情况下将系统吞吐量提升了3倍。