从根儿上把接口加速 10 倍:Spring Boot 3 + 本地缓存「金字塔」实战

从根儿上把接口加速 10 倍:Spring Boot 3 + 本地缓存「金字塔」实战

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

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

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

远程 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 个)

xml 复制代码
<!-- 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 同时生效

yaml 复制代码
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. 核心封装:三级缓存模板

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 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. 业务使用:一行代码搞定缓存

java 复制代码
@RestController
@RequestMapping("/api/item")
@RequiredArgsConstructor
public class ItemController {

    private final CacheTemplate<Long, ItemDTO> cache;
    private final 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 工具:

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

8. 本地预热 & 背压

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

java 复制代码
@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:

java 复制代码
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 注解:

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

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

12. 结语

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

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

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

下一篇带你用 Caffeine+Redis+Kafka 多级缓存一致性,记得关注!

相关推荐
她的男孩2 分钟前
企业接口照样拦得住:独立 Flyway、@RequiresFeature 与离线许可证
java·spring boot·后端
十年Java程序媛3 分钟前
Lambda 与函数式接口|别只会复制 ()->{},底层规则和坑一次性讲清
java·spring boot·后端
yunwei374 分钟前
eBPF 入门开发实践教程一:Hello World,基本框架和开发流程
linux·后端·性能优化
全栈Agent 小李8 分钟前
【无标题】
前端·后端·agent·ai编程·全栈·cursor·mcp
高频因子挖掘机10 分钟前
历史 K 线突然少一天?用交易日历、停牌信息和数据校验逐步排查
后端·github·api
行者全栈架构师34 分钟前
从 55% 到 6%:一个快餐营养规划器的算法迭代实录
后端·算法·架构
余槐i41 分钟前
无慢查询 RT 却飙升:cProfile 定位 FastAPI 事件循环阻塞
后端·python·性能优化·fastapi·asyncio
AINative软件工程1 小时前
LLM 应用的 Adaptive Batching 工程实践:动态合批把吞吐提升 3 倍,但延迟的坑你踩过吗
后端·llm·ai编程
parser1 小时前
Python 装饰器:从语法糖、闭包到 @wraps(上篇)
后端
高频因子挖掘机1 小时前
股票池一大就请求缓慢?量化系统批量获取行情的设计与优化
后端·github·api