Spring Boot 缓存实现方案,缓存是提升性能、降低数据库压力的重要手段(单机应用推荐 Spring Cache + Caffeine)
在 Spring Boot 项目中,缓存是提升性能、降低数据库压力的重要手段。下面结合你提供的资金选项查询业务(/capital/options/all接口),对 Spring Boot 项目中使用缓存的方方面面做一个详细总结,涵盖 Spring Cache 抽象、本地缓存 Caffeine、分布式缓存 Redis 以及 MyBatis 自带的二级缓存,并给出最佳实践建议。
一、缓存的使用场景与业务分析
你的业务接口/capital/options/all用于查询资金相关的所有下拉选项,包括资金账户、来源、指标来源等 7 张配置表。这类数据的特点是:
读多写极少:配置数据基本固定,只在后台管理修改时更新。
数据量小:每张表可能只有几十到几百条记录。
响应要求高:前端可能频繁调用该接口。
因此,非常适合引入缓存,将全量查询结果缓存起来,避免每次请求都查询数据库。
二、Spring Boot 中的缓存方案概览
Spring Boot 支持多种缓存方案,主要分为两类:
基于 Spring 缓存抽象(
spring-boot-starter-cache)通过注解(
@Cacheable、@CacheEvict等)声明式地控制缓存。可插拔的缓存实现:Caffeine(本地)、Redis(分布式)、Ehcache 等。
与业务代码解耦,切换实现无需修改业务逻辑。
MyBatis 自带的二级缓存
MyBatis 框架内置的缓存机制,作用于 Mapper 级别。
无需额外依赖,直接在 Mapper 上配置即可。
缓存粒度较细,但无法直接缓存组合后的 DTO。
下面将分别详细说明这两种方案在业务中的应用。
三、方案一:Spring Cache 抽象(推荐)
1. 核心概念
@Cacheable:标记在方法上,表示结果可缓存。首次调用后缓存结果,后续直接返回缓存值。@CacheEvict:清除缓存,通常在更新操作后调用,保证数据一致性。@CachePut:每次都会执行方法,并更新缓存(不常用)。CacheManager:缓存管理器,负责创建和管理缓存实例。
2. 集成步骤(以 Caffeine 本地缓存为例)
2.1 添加依赖
xml
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency> <dependency> <groupId>com.github.ben-manes.caffeine</groupId> <artifactId>caffeine</artifactId> </dependency>
2.2 启用缓存
在启动类或任意@Configuration类上加@EnableCaching。
java
@SpringBootApplication @EnableCaching public class Application { ... }2.3 配置缓存管理器(可选但推荐)
在application.yml中指定缓存类型和基本属性:
yaml
spring: cache: type: caffeine caffeine: spec: maximumSize=500, expireAfterWrite=10m # 最大500条,写入10分钟后过期
或者通过 Java 配置类进行更细粒度的控制。
2.4 在 Service 方法上添加缓存注解
针对你的业务,改造CapitalInfoService:
java
@Service public class CapitalInfoService { // 注入各个 Mapper @Cacheable(value = "capitalOptions", key = "'all'") public CapitalAllOptionDTO queryCapitalAllOption() { // 原有的7次查询逻辑 return allOptionDTO; } @CacheEvict(value = "capitalOptions", key = "'all'") public void refreshCapitalOptions() { // 空方法,用于手动清空缓存(例如在更新配置后调用) } // 更新配置的方法示例 @CacheEvict(value = "capitalOptions", key = "'all'") public void updateAccount(CapitalAccount account) { accountMapper.updateById(account); } }2.5 注意事项
缓存键:
key = "'all'"使用固定字符串,因为该方法无参且返回全量数据。如果方法有参数,可以动态生成键(如#id)。序列化:如果将来要切换 Redis,需要让
CapitalAllOptionDTO及其包含的实体类实现Serializable。缓存清理:任何对配置表的修改操作(增、删、改)都必须清除缓存,否则用户会看到旧数据。可以在每个更新方法上加
@CacheEvict,或统一调用refreshCapitalOptions()。空值缓存:如果数据库为空,默认不会缓存空结果,可能导致缓存穿透。可以用
unless = "#result == null"避免。
3. 切换为 Redis(分布式缓存)
如果应用需要多实例部署,只需更换依赖和配置,业务代码无需修改:
移除 Caffeine 依赖,添加
spring-boot-starter-data-redis。在
application.yml中配置 Redis 连接和 TTL:
yaml
spring: cache: type: redis redis: time-to-live: 600000 # 10分钟(毫秒) redis: host: localhost port: 6379
Spring Boot 会自动配置RedisCacheManager,之前的@Cacheable注解继续生效。
4. Spring Cache 的优势
注解驱动:简洁明了,与业务代码分离。
灵活:可缓存任意方法的返回值,包括组合 DTO。
可扩展:轻松切换本地/分布式缓存。
支持多种缓存实现:Caffeine、Redis、Ehcache 等。
四、方案二:MyBatis 二级缓存
1. 核心概念
MyBatis 内置了两级缓存:
一级缓存(SqlSession 级别):默认开启,同一会话中重复查询不查库。
二级缓存(Mapper 级别):跨会话共享,需手动开启。
二级缓存的作用域是同一个namespace(即同一个 Mapper),缓存的是 Mapper 中查询方法的返回结果(如List<CapitalAccount>)。
2. 在业务中的应用
如果使用 MyBatis 二级缓存,你需要为 7 个 Mapper 分别开启缓存:
java
@Mapper @CacheNamespace(eviction = FifoCache.class, flushInterval = 60000, size = 512) public interface CapitalAccountMapper extends BaseMapper<CapitalAccount> { }然后,在 Service 方法中,每次调用accountMapper.selectList(null)时,如果二级缓存中有数据,就不会查询数据库。
3. 优缺点分析
优点:
无需额外依赖,MyBatis 自带。
配置简单,只需在 Mapper 上加注解。
自动维护一致性:执行当前 Mapper 的 DML 操作时自动清空该 Mapper 的缓存。
缺点:
无法缓存组合后的 DTO,每次调用
queryCapitalAllOption()仍需执行 7 次查询(虽然可能命中缓存)。缓存粒度太细,不能跨 Mapper 共享。
分布式环境下,各实例缓存独立,会导致数据不一致。
需要实体类实现
Serializable(如果缓存可读写)。与 Spring 事务结合时,需要注意缓存清空的时机(必须在事务提交后清空,MyBatis 默认在更新后清空,但如果事务回滚,缓存已被清空,可能造成不一致)。
4. 适合场景
单机应用,对某个 Mapper 的查询有较高重复率。
无需跨表组合缓存的简单场景。
对于你的业务(一次返回 7 张表的组合数据),MyBatis 二级缓存虽然能减少数据库查询,但仍有多次缓存读取和对象组装开销,不如 Spring Cache 直接缓存最终 DTO 高效。
五、两种方案的对比与选择
| 特性 | Spring Cache + Caffeine/Redis | MyBatis 二级缓存 |
|---|---|---|
| 缓存粒度 | 任意方法返回值(如 DTO) | Mapper 级别的单个查询结果 |
| 配置复杂度 | 需添加依赖和配置,但注解简单 | 无需额外依赖,仅 Mapper 配置 |
| 数据一致性 | 需手动@CacheEvict,可精细控制 | 执行 DML 时自动清空当前 Mapper 缓存 |
| 分布式支持 | 支持(切换 Redis) | 不支持(仅本地) |
| 与业务解耦 | 完全解耦,可作用于 Service 层 | 与 Mapper 耦合,无法控制组合逻辑 |
| 适用场景 | 适合组合查询、跨表结果缓存 | 适合单表简单查询的重复缓存 |
针对你的业务,强烈推荐使用 Spring Cache 抽象 + Caffeine(单机)或+ Redis(分布式)。理由如下:
直接缓存最终的
CapitalAllOptionDTO,一次方法调用只需查一次数据库(缓存失效后),性能最佳。注解使用灵活,未来可无缝切换缓存实现。
代码改动小,只需在 Service 方法上加注解,并在更新操作上加
@CacheEvict。
六、最佳实践总结(结合你的业务)
1. 实施步骤
添加依赖:
spring-boot-starter-cache和caffeine。启用缓存:启动类加
@EnableCaching。配置缓存:
application.yml中设置spring.cache.caffeine.spec=maximumSize=500, expireAfterWrite=10m。注解 Service 方法:
queryCapitalAllOption()上加@Cacheable(value = "capitalOptions", key = "'all'")。所有更新配置的方法(如
updateAccount、addSource等)上加@CacheEvict(value = "capitalOptions", key = "'all'")。
测试验证:首次调用查库,第二次直接返回缓存。
2. 注意事项
缓存过期时间:根据数据更新频率设置合理的
expireAfterWrite,例如 10 分钟或 30 分钟。即使有@CacheEvict,过期时间作为兜底,防止缓存永久占用内存。缓存清理:务必在所有修改操作中清除缓存,否则会出现脏数据。如果修改操作分散在多个 Service,可以统一调用一个刷新方法(
@CacheEvict标注的空方法)。序列化:如果未来可能切换 Redis,提前让 DTO 和实体类实现
Serializable。缓存键冲突:如果同一缓存区域有其他方法,使用不同的 key 区分。
监控与调优:可以通过 Actuator 端点查看缓存命中率,调整缓存大小和过期时间。
3. 扩展思考
分布式部署:当应用需要水平扩展时,将缓存实现切换为 Redis,注意配置 Redis 的序列化方式(如 Jackson2JsonRedisSerializer)以便跨语言读取。
缓存穿透/雪崩:配置空值缓存(
unless)和随机过期时间防止雪崩。多级缓存:对于极高并发场景,可考虑 Caffeine 作为一级缓存,Redis 作为二级缓存,但会增加复杂度。
七、总结
Spring Boot 项目中使用缓存,应根据业务场景选择合适的方案:
对于组合查询、跨表结果,首选Spring Cache 抽象,它提供了声明式编程模型,能够灵活地缓存任意方法的返回值,并且支持多种缓存实现,方便未来扩展。
对于简单的单表查询重复缓存,MyBatis 二级缓存也是一个轻量级的选择,但需注意其局限性和分布式问题。
在你的资金选项业务中,采用 Spring Cache + Caffeine 的实践方案,不仅能够显著提升接口性能,还能保持代码简洁、易于维护。缓存是性能优化的利器,但也要注意数据一致性、过期策略和分布式环境下的挑战,合理设计才能发挥最大价值。
