当前位置: 首页 > news >正文

Spring Boot缓存实战:@Cacheable注解详解与性能优化

缓存是提升应用性能最有效的手段之一。

在Spring Boot项目中,@Cacheable注解为我们提供了一种声明式的缓存解决方案,让我们能够以极简的方式实现高性能的缓存逻辑。

本文将深入探讨@Cacheable的使用方法、工作原理和最佳实践。

一、为什么需要缓存?

在数据驱动的应用中,我们经常遇到这样的场景:

@RestController
public class ProductController {@Autowiredprivate ProductService productService;@GetMapping("/products/{id}")public Product getProduct(@PathVariable Long id) {// 每次请求都查询数据库// 高并发时数据库压力巨大!return productService.findById(id);}
}

对于热门商品,短时间内可能被请求成千上万次。如果每次都要查询数据库,不仅响应慢,数据库也可能被压垮。

缓存的价值

  • 减少数据库查询,提升响应速度(从毫秒级到微秒级)
  • 降低数据库负载,提高系统吞吐量
  • 提升用户体验,减少等待时间

二、@Cacheable注解的核心作用

@Cacheable就像是应用的"智能备忘录":

@Service
public class UserService {@Cacheable(value = "users", key = "#id")public User findById(Long id) {// 只有第一次调用会执行这里// 后续相同参数的调用直接返回缓存结果return userRepository.findById(id).orElse(null);}
}

工作流程

  1. 首次调用:执行方法体,将结果存入缓存
  2. 后续调用:直接返回缓存结果,跳过方法执行

三、Spring缓存架构解析

Spring的缓存抽象层让我们可以无缝切换不同的缓存实现:

[@Cacheable注解] → [Spring Cache Abstraction] → [CacheManager] → [Redis/Caffeine/Ehcache等]

这种设计让业务代码与具体缓存实现解耦,便于测试和维护。

四、@Cacheable详细使用指南

1. 基础配置

启用缓存

@SpringBootApplication
@EnableCaching  // 关键:启用缓存功能
public class Application {public static void main(String[] args) {SpringApplication.run(Application.class, args);}
}

选择缓存依赖

  • 测试环境(内存缓存):
<!-- Spring Boot已内置ConcurrentMap缓存 -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-cache</artifactId>
</dependency>
  • 生产环境(Redis):
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
  • 高性能本地缓存
<dependency><groupId>com.github.ben-manes.caffeine</groupId><artifactId>caffeine</artifactId>
</dependency>

2. 核心属性详解

cacheNames/value - 指定缓存空间
@Cacheable("users")        // 单缓存空间
@Cacheable({"users", "temp"}) // 多缓存空间
key - 缓存键设计
// SpEL表达式定义key
@Cacheable(value = "users", key = "#id")
public User findById(Long id) { ... }@Cacheable(value = "orders", key = "#userId + ':' + #orderType")
public List<Order> findOrders(Long userId, String orderType) { ... }// 调用对象方法
@Cacheable(value = "products", key = "#product.category + ':' + #product.id")
public Product detail(Product product) { ... }
condition - 条件缓存
// 只缓存id大于100的用户
@Cacheable(value = "users", key = "#id", condition = "#id > 100")
public User findUserConditional(Long id) { ... }// 只缓存管理员用户
@Cacheable(value = "users", condition = "#result != null and #result.role == 'ADMIN'")
public User findAdminUser(Long id) { ... }
unless - 结果过滤
// 结果不为空时才缓存
@Cacheable(value = "users", unless = "#result == null")
public User findUserUnless(Long id) { ... }// 不缓存异常结果
@Cacheable(value = "data", unless = "#result == null or #result.hasErrors()")
public DataResult computeData() { ... }

3. 完整实战示例

@Service
@CacheConfig(cacheNames = "userService") // 类级别缓存配置
public class UserService {@Autowiredprivate UserRepository userRepository;// 基础缓存@Cacheable(key = "'user:' + #id")public User findById(Long id) {log.info("查询数据库用户: {}", id);return userRepository.findById(id).orElse(null);}// 条件缓存 + 复杂key@Cacheable(key = "T(org.springframework.util.DigestUtils).md5DigestAsHex(('#page:' + #page + ':size:' + #size + ':condition:' + #condition).getBytes())", condition = "#page < 5") // 只缓存前5页public Page<User> findUsers(int page, int size, String condition) {log.info("分页查询用户: page={}, size={}", page, size);return userRepository.findByCondition(condition, PageRequest.of(page, size));}// 组合条件缓存@Cacheable(key = "'user_profile:' + #userId", condition = "#userId != null", unless = "#result == null or #result.isBanned()")public UserProfile getUserProfile(Long userId) {log.info("查询用户详情: {}", userId);return userRepository.findProfileById(userId);}
}

五、缓存生命周期管理

完整的缓存方案需要配套注解来管理缓存生命周期:

1. @CacheEvict - 清理缓存

// 更新后清除单个用户缓存
@CacheEvict(value = "users", key = "#user.id")
public void updateUser(User user) {userRepository.save(user);
}// 方法执行前清理
@CacheEvict(value = "users", key = "#id", beforeInvocation = true)
public void deleteUser(Long id) {userRepository.deleteById(id);
}// 清理整个缓存空间
@CacheEvict(value = "users", allEntries = true)
public void reloadAllUsers() {// 重新加载数据
}

2. @CachePut - 更新缓存

// 总是执行方法体,并更新缓存
@CachePut(value = "users", key = "#user.id")
public User saveUser(User user) {User saved = userRepository.save(user);log.info("保存用户并更新缓存: {}", saved.getId());return saved;
}

3. @Caching - 组合操作

// 同时操作多个缓存
@Caching(evict = {@CacheEvict(value = "users", key = "#user.id"),@CacheEvict(value = "user_list", allEntries = true)},put = {@CachePut(value = "user_stats", key = "#user.department")}
)
public User updateUserWithStats(User user) {return userRepository.save(user);
}

六、生产环境最佳实践

1. Redis缓存配置

# application.yml
spring:cache:type: redisredis:time-to-live: 3600000    # 1小时过期cache-null-values: false # 不缓存null值redis:host: localhostport: 6379password: database: 0

2. 自定义缓存配置

@Configuration
@EnableCaching
public class CacheConfig {@Beanpublic CacheManager cacheManager(RedisConnectionFactory factory) {RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofHours(1)).disableCachingNullValues().serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())).serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));return RedisCacheManager.builder(factory).cacheDefaults(config).withInitialCacheConfigurations(getCacheConfigurations()).build();}private Map<String, RedisCacheConfiguration> getCacheConfigurations() {Map<String, RedisCacheConfiguration> configMap = new HashMap<>();// 用户数据缓存2小时configMap.put("users", RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofHours(2)));// 配置数据缓存24小时  configMap.put("configs", RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofHours(24)));return configMap;}
}

3. 缓存异常处理

@Service
public class SafeCacheService {@Cacheable(value = "safeData", key = "#id", unless = "#result == null")public String getDataSafely(String id) {try {return fetchDataFromExternalService(id);} catch (Exception e) {log.error("获取数据失败: {}", id, e);// 异常时不缓存,返回null触发unless条件return null; }}
}

七、常见问题与解决方案

1. 缓存穿透

问题:查询不存在的数据,导致每次都要访问数据库。

解决方案

@Cacheable(value = "users", key = "#id", unless = "#result == null")
public User findById(Long id) {User user = userRepository.findById(id).orElse(null);// 对于null值,可以缓存空对象或使用布隆过滤器return user != null ? user : new NullUser();
}

2. 缓存雪崩

问题:大量缓存同时失效,请求直接打到数据库。

解决方案

// 设置不同的过期时间
@Configuration
public class CacheConfig {@Beanpublic CacheManager cacheManager(RedisConnectionFactory factory) {return RedisCacheManager.builder(factory).cacheDefaults(RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofMinutes(30 + new Random().nextInt(30)))) // 30-60分钟随机过期.build();}
}

3. 缓存击穿

问题:热点key失效瞬间,大量请求直接访问数据库。

解决方案

@Cacheable(value = "hotProducts", key = "#id", sync = true) // 使用sync同步加载
public Product getHotProduct(Long id) {return productRepository.findById(id).orElse(null);
}

八、性能对比测试

让我们通过实际测试看看缓存的效果:

@SpringBootTest
class CachePerformanceTest {@Autowiredprivate UserService userService;@Testvoid testCachePerformance() {Long userId = 1L;// 第一次查询(无缓存)long start1 = System.currentTimeMillis();userService.findById(userId);long time1 = System.currentTimeMillis() - start1;// 第二次查询(有缓存)long start2 = System.currentTimeMillis();userService.findById(userId);long time2 = System.currentTimeMillis() - start2;System.out.println("首次查询耗时: " + time1 + "ms");System.out.println("缓存查询耗时: " + time2 + "ms");System.out.println("性能提升: " + (time1 - time2) + "ms");}
}

典型结果

  • 数据库查询:50-200ms
  • 缓存查询:1-5ms
  • 性能提升:20-100倍

总结

@Cacheable注解是Spring Boot中实现声明式缓存的利器,通过简单的注解配置就能获得显著的性能提升。在实际项目中:

  1. 合理设计缓存键,避免键冲突和内存浪费
  2. 设置合适的过期时间,平衡数据一致性和性能
  3. 处理缓存异常,确保系统稳定性
  4. 配合监控工具,实时观察缓存命中率

掌握@Cacheable的使用,让你的Spring Boot应用性能飞起来!

http://www.dtcms.com/a/609308.html

相关文章:

  • LeetCode热题100--17. 电话号码的字母组合
  • C++初阶(07):STL简介
  • 中国哪些网站做软装建设彩票网站合法吗
  • 百度网站评分椒江做网站
  • yolov8目标检测训练在rk3588上部署
  • 学术数据可视化:高效图表工具助力科研数据精准呈现
  • 焦作高端网站建设宁波seo外包服务平台
  • Apple 官方提供 Xcode 周边实用工具集,包含CarPlay 模拟器,网速限制等 Additional Tools for Xcode 26.1,
  • 《隐藏(Hide)》
  • 基于Mask R-CNN的汽车防夹手检测与识别系统
  • 从正向困境到反向破局:详解地下城游戏的动态规划解法
  • 常州新北区网站建设东莞搜索排名提升
  • 专题:2025构建全自动驾驶汽车生态系统:中国智能驾驶行业全景研究报告|附80+份报告PDF、数据仪表盘汇总下载
  • uni-app 将 base64 图片编码转为 Blob 本地文件路径
  • Ethernaut Level 16: Preservation - Delegatecall与存储布局操纵
  • 1040视频app深圳网站建设seo推广优化
  • MySQL 中的 MVCC
  • Answer 开源平台搭建:cpolar 内网穿透服务助力全球用户社区构建
  • 从 Spring @Retryable 到 Kafka 原生重试:消息重试方案的演进与最佳实践
  • 做宣传用什么网站好网络设计与实施课程设计
  • 云盘做网站文件网站内容不被收录
  • 服务器部署,用 nginx 部署后页面刷新 404 问题,宝塔面板修改(修改 nginx.conf 配置文件)
  • 500额度claude4.5无线续杯教程
  • 身智能-一文详解视觉-语言-动作(VLA)大模型(3)
  • 【图像处理基石】 怎么让图片变成波普风?
  • MySQL 与 Redis 的数据一致性问题
  • YOLOv8-SOEP-RFPN-MFM水果智能分类与检测模型实现
  • 树莓派UBUNTU 24.04 PART 5 树莓派4b UBUNTU 系统安装miniconda、opencv、tensorflow
  • 学校网站建设开发商中信建设有限责任公司 电话
  • 24 小时知识导航:使用 cpolar 内网穿透服务访问 Perplexica