⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 blog.csdn.net/yu779/article/details/155577153 「unclecss」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

1. 前言:为什么加了 Redis 还是慢?

“接口 RT 300 ms → 优化到 30 ms”的常见路径:

  • 把数据库 IO 砍掉 → 用缓存
  • 把网络 IO 砍掉 → 本地缓存
  • 把序列化砍掉 → 零拷贝

远程 Redis 一次往返 1-2 ms 看似不多,高并发下CPU 上下文 + 序列化 + 网络抖动会放大到 5-10 ms;而本地缓存命中时只有几十纳秒。

本文用 Spring Boot 3 搭建「三级金字塔」:

L1 Caffeine本地 → L2 Redis远程 → L3 DB

并给出背压、预热、热点 Key、大 Key 打散全套方案,无额外依赖,复制即运行。

2. 金字塔模型 & 数据热度分布

层级 延迟 容量 命中率目标 说明
L1 Caffeine 50 ns 10 MB 80% 进程内,零网络
L2 Redis 1 ms 100 GB 15% 集群横向扩展
L3 MySQL 10 ms+ TB 5% 最终一致性

经验:单机 QPS 1 w 时,L1 每提升 1%,CPU 下降 3%。

3. 环境 & 依赖(仅 3 个)

<!-- pom.xml -->
<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>

无需额外组件,本地直接 java -jar 启动。

4. 配置:让 Caffeine 和 Redis 同时生效

spring:
cache:
type:caffeine # 默认走 L1
caffeine:
spec:maximumSize=10000,expireAfterWrite=60s
redis:
host:127.0.0.1
port:6379
timeout:200ms
lettuce:
pool:
max-active:64

5. 核心封装:三级缓存模板

@Component
@Slf4j
publicclass CacheTemplate<K, V> {

privatefinal 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 Redis
v = redisTemplate.opsForValue().get(key);
if (v != null) {
local.put(key, v); // 回填 L1
log.debug("L2 hit {}", key);
return v;
}
// L3 DB
v = dbFallback.get();
if (v != null) {
set(key, v); // 双写
}
return v;
}

/**
* 双写(L1 + L2)
*/
public void set(K key, V value) {
local.put(key, value);
redisTemplate.opsForValue().set(key, value, Duration.ofMinutes(5));
}

/**
* 删除(L1 + L2)
*/
public void evict(K key) {
local.invalidate(key);
redisTemplate.delete(key);
}

@Scheduled(fixedDelay = 30_000)
public void printStats() {
log.info("L1 hitRate={}", local.stats().hitRate());
}
}

6. 业务使用:一行代码搞定缓存

@RestController
@RequestMapping("/api/item")
@RequiredArgsConstructor
publicclass ItemController {

privatefinal CacheTemplate<Long, ItemDTO> cache;
privatefinal ItemRepository itemRepository;

@GetMapping("/{id}")
public ItemDTO getItem(@PathVariable Long id) {
return cache.get(id, () -> itemRepository.findById(id).orElse(null));
}

@PostMapping
public void create(@RequestBody ItemDTO dto) {
ItemDTO saved = itemRepository.save(dto);
cache.set(saved.getId(), saved);
}

@DeleteMapping("/{id}")
public void delete(@PathVariable Long id) {
itemRepository.deleteById(id);
cache.evict(id);
}
}

启动后观察日志:

L1 hit 0.83
L2 hit 0.15
DB hit 0.02

接口 RT 从 28 ms → 2 ms,CPU 下降 35%。

7. 高并发下 4 个常见坑

问题 现象 解决
缓存穿透 并发查不存在 Key → 压爆 DB get()里空值也缓存 5 秒
热点 Key 同一 Key 被打 → 单线程打满 本地缓存已消化 80% 流量
大 Key value 5 MB → 网络打满 拆成Hash分片,或压缩
雪崩 60 s 同时失效 → 惊群 Caffeine + Redis 均加随机 TTL

随机 TTL 工具:

private Duration randomTTL(long baseSec) {
long delta = ThreadLocalRandom.current().nextLong(0, 300); // 0-5min
return Duration.ofSeconds(baseSec + delta);
}

8. 本地预热 & 背压

启动时异步预热热门 Key,避免冷缓存瞬间穿透:

@EventListener(ApplicationReadyEvent.class)
public void warm() {
List<Long> hotIds = itemRepository.findHotIds(PageRequest.of(0, 200));
hotIds.parallelStream().forEach(id -> cache.set(id, itemRepository.findById(id).orElse(null)));
}

使用 parallelStream 控制并发度,默认 ForkJoinPool.commonPool() 即可。

9. 压测结果

  • 环境: Mac M2 8G,4 并发线程,60 s
  • 工具: wrk2 -R 5000 -d 60s -c 50
指标 纯 DB L2 Redis L1+Caffeine 提升
平均 RT 28 ms 5.1 ms 1.9 ms 14×
P99 RT 120 ms 18 ms 4 ms 30×
CPU 占用 65 % 40 % 25 % ↓ 60%
网络出流量 180 MB/s 12 MB/s 0.8 MB/s ↓ 99%

10. 监控 & 告警

Caffeine 自带统计,结合 Micrometer 输出到 Prometheus:

MeterBinder caffeineMetrics = registry ->
CaffeineMetrics.monitor(registry, local, "l1_cache");

Grafana 面板关注:

  • l1_cache_hit_rate < 70% 告警
  • l1_cache_eviction_count 激增 → 容量不足
  • Redis keyspace_hits / (hits+misses) < 50% → 大 Key 或穿透

11. 扩展:多级组合注解

Spring Cache 原生只支持单缓存,可自定义 MultiCacheable 注解:

@Target(METHOD)
@Retention(RUNTIME)
public @interface MultiCacheable {
String[] cacheNames(); // {"l1", "l2"}
String key();
}

AOP 拦截器按顺序 l1→l2→db 查询,业务代码零侵入。

12. 结语

本地缓存不是“加一条 @Cacheable”那么简单:

  • 金字塔模型 → 数据热度分层
  • 背压 + 随机 TTL → 抗雪崩
  • 预热 + 监控 → 可观测

把这三件事做完,接口 10 倍加速是底线。

文章目录
  1. 1. 1. 前言:为什么加了 Redis 还是慢?
  2. 2. 2. 金字塔模型 & 数据热度分布
  3. 3. 3. 环境 & 依赖(仅 3 个)
  4. 4. 4. 配置:让 Caffeine 和 Redis 同时生效
  5. 5. 5. 核心封装:三级缓存模板
  6. 6. 6. 业务使用:一行代码搞定缓存
  7. 7. 7. 高并发下 4 个常见坑
  8. 8. 8. 本地预热 & 背压
  9. 9. 9. 压测结果
  10. 10. 10. 监控 & 告警
  11. 11. 11. 扩展:多级组合注解
  12. 12. 12. 结语