高级篇 05. 多级缓存 - JVM 进程缓存之实现业务缓存

📚 高级篇 05. 多级缓存 - JVM 进程缓存之实现业务缓存

一、 架构图解:JVM 缓存拦截流程

在改造代码之前,我们先明确一下加入 Caffeine 之后的请求处理路径:

  1. 前端发来查询商品详情的请求(例如查询 id = 10001 的商品)。

  2. 请求到达 Tomcat 中的 ItemController,随后进入 ItemService

  3. ItemService 首先去查 Caffeine 缓存

    • 如果命中 (Cache Hit): 直接返回内存中的对象数据(耗时不到 1 毫秒),结束!
    • 如果未命中 (Cache Miss): 去 MySQL 数据库查询,查到数据后,将其写入 Caffeine 缓存,然后再返回给前端。

二、 实战改造:注入 Caffeine Bean

在 Spring Boot 项目中,为了统一管理缓存的配置(比如商品缓存的容量、库存缓存的容量可能不同),我们通常会创建一个专门的配置类来定义 Cache 对象,并将它们注册为 Spring 的 Bean。

第 1 步:引入依赖

确保你的 pom.xml 中已经有了 Caffeine 的依赖:

XML

xml 复制代码
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
</dependency>

第 2 步:编写配置类 (CaffeineConfig.java)

由于在真实的电商系统中,商品的基本信息(标题、图片)商品的库存信息 变化频率完全不同(库存几乎每秒都在变),我们强烈建议将它们分管在两个不同的缓存实例中

Java

kotlin 复制代码
package com.heima.item.config;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.heima.item.pojo.Item;
import com.heima.item.pojo.ItemStock;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class CaffeineConfig {

    /**
     * 商品基本信息缓存 (变化频率低)
     */
    @Bean
    public Cache<Long, Item> itemCache() {
        return Caffeine.newBuilder()
                .initialCapacity(100)       // 初始容量
                .maximumSize(10_000)        // 最大容量,防止 OOM
                // 可以配置基于大小或时间的驱逐策略
                .build();
    }

    /**
     * 商品库存信息缓存 (变化频率极高)
     */
    @Bean
    public Cache<Long, ItemStock> itemStockCache() {
        return Caffeine.newBuilder()
                .initialCapacity(100)
                .maximumSize(10_000)
                .build();
    }
}

三、 实战改造:业务层优雅集成

配置好了 Bean,接下来我们在 ItemService 中注入这些 Cache,并运用上一节学到的终极优雅写法 get(key, function)

第 3 步:改造 ItemService (核心逻辑)

打开你的商品查询业务类,进行如下改造:

Java

kotlin 复制代码
package com.heima.item.service.impl;

import com.github.benmanes.caffeine.cache.Cache;
import com.heima.item.mapper.ItemMapper;
import com.heima.item.pojo.Item;
import com.heima.item.service.IItemService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class ItemServiceImpl implements IItemService {

    @Autowired
    private ItemMapper itemMapper;

    // 1. 注入我们在 Config 中配置的商品缓存 Bean
    @Autowired
    private Cache<Long, Item> itemCache;

    @Override
    public Item queryItemById(Long id) {
        // 2. 极其优雅的缓存处理逻辑 (一行代码搞定查缓存 + 查库 + 存缓存)
        // 逻辑:用 id 去 itemCache 里找。
        // 找不到?那就自动执行后面的 Lambda 表达式 (去数据库查 selectById(id)),然后存入缓存并返回。
        return itemCache.get(id, key -> {
            System.out.println("❌ 本地缓存未命中,正在查询 MySQL 数据库,id: " + key);
            return itemMapper.selectById(key);
        });
    }
}

(库存逻辑也是同理,注入 itemStockCache 并在查询库存的方法中使用即可。)


四、 见证奇迹:测试与验证

改造完成后,重启你的 Spring Boot 项目。

  1. 第一次请求前端页面(查询 id=10001 的商品):

    你会发现控制台打印了:❌ 本地缓存未命中,正在查询 MySQL 数据库,id: 10001,并且看到了一条打印出的 SQL 语句。这说明第一次请求穿透到了数据库。

  2. 疯狂刷新页面(第二次、第三次、第一万次):

    你会发现无论你按多少次 F5,控制台再也没有任何 SQL 语句打印出来!

    请求响应时间从原来的几十毫秒,瞬间骤降到了几毫秒甚至不到 1 毫秒

因为从第二次开始,所有的流量全被 Tomcat JVM 内存里的 Caffeine 拦截并光速返回了。数据库此时稳如泰山!


五、 大厂高频拷问:JVM 缓存的致命缺陷是什么?

在面试中,千万不要把 JVM 缓存吹得完美无缺。你需要主动抛出它的致命缺陷,展现你的全局架构观:

🗣️ 面试官发难: "既然 Caffeine 本地缓存这么快,我们以后所有项目只用它不就行了,为什么还要大费周章去搭 Redis 集群?"

💡 你的满分破局解析:

"本地缓存虽然速度达到了极致的纳秒级,但它有两个致命的缺陷,决定了它只能作为多级缓存中的前置防线,而不能完全替代 Redis:

  1. 内存容量极度受限: JVM 的堆内存通常配置只有几个 G 到十几个 G,还要留给业务对象和垃圾回收。把海量的业务数据全塞在 JVM 里一定会触发频繁的 Full GC,甚至 OOM 导致服务器宕机。而 Redis 分片集群可以无限横向扩容到 TB 级别。
  2. 极其头疼的数据一致性问题 (集群数据漂移): 在微服务架构下,商品服务通常会部署多个实例(例如 Tomcat 1、Tomcat 2)。如果后台修改了商品价格,通常只会清除其中一台 Tomcat 的本地缓存,或者请求还没来得及同步。此时,用户刷新页面,请求被网关随机路由到 Tomcat 1(看到新价格)和 Tomcat 2(看到旧价格),数据出现了严重的幻读现象

因此,本地缓存只适合存放极度热点且容量较小的数据。对于海量数据,我们依然需要一个统一的、全局可见的分布式缓存(Redis)来作为数据共享的兜底底座。"


学习总结

这节课,你的 Spring Boot 已经进化出了极速响应的能力。Caffeine 就像是给 Tomcat 穿上了一件防弹衣,完美解决了 Redis 热点 Key 可能导致的单节点雪崩问题。


相关推荐
devlei3 小时前
从源码泄露看AI Agent未来:深度对比Claude Code原生实现与OpenClaw开源方案
android·前端·后端
努力的小郑5 小时前
Canal 不难,难的是用好:从接入到治理
后端·mysql·性能优化
Victor3565 小时前
MongoDB(87)如何使用GridFS?
后端
Victor3565 小时前
MongoDB(88)如何进行数据迁移?
后端
小红的布丁6 小时前
单线程 Redis 的高性能之道
redis·后端
GetcharZp6 小时前
Go 语言只能写后端?这款 2D 游戏引擎刷新你的认知!
后端
宁瑶琴7 小时前
COBOL语言的云计算
开发语言·后端·golang
普通网友7 小时前
阿里云国际版服务器,真的是学生党的性价比之选吗?
后端·python·阿里云·flask·云计算
IT_陈寒8 小时前
Vue的这个响应式问题,坑了我整整两小时
前端·人工智能·后端
Soofjan9 小时前
Go 内存回收-GC 源码1-触发与阶段
后端