什么是 Spring Cache?
Spring Cache是Spring框架提供的一层缓存抽象,旨在简化应用程序中的缓存管理。通过使用Spring Cache,开发者能够在方法级别方便地定义缓存策略,提高应用性能、响应速度,并减轻底层数据源的负载。该框架提供一系列注解,如@Cacheable、@CacheEvict、@CachePut,以及对多种底层缓存实现的支持,如EhCache、Redis等。它为应用程序提供了一种统一、方便、灵活的缓存管理方式,允许开发者通过简单的配置实现复杂的缓存逻辑,同时与Spring框架紧密集成。这种抽象层的存在使得在更改底层缓存框架时更为轻松,同时提供了一致的配置接口和更强大的高级特性。
Spring Cache 常用 API
@Cacheable
@Cacheable: 用于标记一个方法的结果应该被缓存。当在该方法被调用时,Spring首先检查缓存中是否已经有了预期的结果,如果有,直接返回缓存中的结果而不执行实际的方法体。
javaCopy code@Cacheable(cacheNames = "exampleCache", key = "'exampleKey'") public String getCachedData() { // 执行实际的方法体,结果会被缓存 }
@CacheEvict
@CacheEvict: 用于标记一个方法在执行后清空缓存。可以指定清空的缓存名称和清空的条件。
javaCopy code@CacheEvict(cacheNames = "exampleCache", key = "'exampleKey'") public void clearCache() { // 执行实际的方法体,之后清空缓存 }
@CachePut
@CachePut: 用于标记一个方法的结果应该被放入缓存,常用于在方法执行后更新缓存。
javaCopy code@CachePut(cacheNames = "exampleCache", key = "'exampleKey'") public String updateCache() { // 执行实际的方法体,结果会被放入缓存 }
@Caching
@Caching: 允许同时应用多个缓存操作注解。
javaCopy code@Caching( evict = { @CacheEvict(cacheNames = "cache1", key = "'key1'"), @CacheEvict(cacheNames = "cache2", key = "'key2'") } ) public void clearMultipleCaches() { // 执行实际的方法体,清空多个缓存 }
Spring Boot 整合 Spring Cache (Redis 缓存)
完整项目源码:youlai-boot
项目依赖 pom.xml
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency>
项目配置 application.yml
spring: data: redis: database: 6 host: localhost port: 6379 password: 123456 timeout: 10s lettuce: pool: # 连接池最大连接数 默认8 ,负数表示没有限制 max-active: 8 # 连接池最大阻塞等待时间(使用负值表示没有限制) 默认-1 max-wait: -1 # 连接池中的最大空闲连接 默认8 max-idle: 8 # 连接池中的最小空闲连接 默认0 min-idle: 0 cache: # 缓存类型 redis、none(不使用缓存) type: redis # 缓存时间(单位:ms) redis: time-to-live: 3600000 # 缓存null值,防止缓存穿透 cache-null-values: true
自动装配配置类 RedisCacheConfig
package com.youlai.system.config; import org.springframework.boot.autoconfigure.cache.CacheProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cache.annotation.EnableCaching; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.cache.RedisCacheConfiguration; import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.cache.RedisCacheWriter; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.serializer.RedisSerializationContext; import org.springframework.data.redis.serializer.RedisSerializer; /** * Redis 缓存配置 * * @author haoxr * @since 2023/12/4 */ @EnableCaching @EnableConfigurationProperties(CacheProperties.class) @Configuration public class RedisCacheConfig { /** * 自定义 RedisCacheManager * <p> * 修改 Redis 序列化方式,默认 JdkSerializationRedisSerializer * * @param redisConnectionFactory {@link RedisConnectionFactory} * @param cacheProperties {@link CacheProperties} * @return {@link RedisCacheManager} */ @Bean public RedisCacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory, CacheProperties cacheProperties){ return RedisCacheManager.builder(RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory)) .cacheDefaults(redisCacheConfiguration(cacheProperties)) .build(); } /** * 自定义 RedisCacheConfiguration * * @param cacheProperties {@link CacheProperties} * @return {@link RedisCacheConfiguration} */ @Bean RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) { RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig(); config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(RedisSerializer.string())); config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(RedisSerializer.json())); CacheProperties.Redis redisProperties = cacheProperties.getRedis(); if (redisProperties.getTimeToLive() != null) { config = config.entryTtl(redisProperties.getTimeToLive()); } if (!redisProperties.isCacheNullValues()) { config = config.disableCachingNullValues(); } if (!redisProperties.isUseKeyPrefix()) { config = config.disableKeyPrefix(); } config = config.computePrefixWith(name -> name + ":");// 覆盖默认key双冒号 CacheKeyPrefix#prefixed return config; } }
Spring Boot 路由缓存实战
完整项目源码:youlai-boot
获取路由数据缓存
获取路由列表,通过 @Cacheable 注解,将方法返回的路由列表数据缓存至 Redis。当再次请求时,不再进入方法体,而是直接从 Redis 中读取缓存数据并返回,以提高性能。
/** * 获取路由列表 */ @Override @Cacheable(cacheNames = "menu", key = "'routes'") // cacheNames 为必填项,key 需要使用引号,否则会被识别为变量。 public List<RouteVO> listRoutes() { List<RouteBO> menuList = this.baseMapper.listRoutes(); return buildRoutes(SystemConstants.ROOT_NODE_ID, menuList); }
更新路由缓存失效
若需要在路由信息更新时使缓存失效,可以使用 @CacheEvict 注解,它用于在方法执行之后(默认)从缓存中移除条目。
/** * 新增/修改菜单 */ @Override @CacheEvict(cacheNames = "menu", key = "'routes'",beforeInvocation = true) public boolean saveMenu(MenuForm menuForm) { String path = menuForm.getPath(); MenuTypeEnum menuType = menuForm.getType(); // 如果是目录 if (menuType == MenuTypeEnum.CATALOG) { if (menuForm.getParentId() == 0 && !path.startsWith("/")) { menuForm.setPath("/" + path); // 一级目录需以 / 开头 } menuForm.setComponent("Layout"); } // 如果是外链 else if (menuType == MenuTypeEnum.EXTLINK) { menuForm.setComponent(null); } SysMenu entity = menuConverter.form2Entity(menuForm); String treePath = generateMenuTreePath(menuForm.getParentId()); entity.setTreePath(treePath); return this.saveOrUpdate(entity); }
更新菜单后,Redis 中缓存的路由数据将被清除。再次获取路由时,才会将新的路由数据缓存到 Redis 中。
Spring Cache 问题整理
@Cacheable 缓存的 key 双冒号
默认情况下 @Cacheable 使用双冒号 :: 拼接 cacheNames 和 key
@Cacheable(cacheNames = "menu", key = "'routes'")
参考源码 RedisCacheConfiguration#prefixCacheNameWith
如果需要将双冒号改成单个冒号,需要重写 RedisCacheConfiguration#computePrefixWith 方法
config = config.computePrefixWith(name -> name + ":");//覆盖默认key双冒号 CacheKeyPrefix#prefixed
结语
Spring Cache 为 Spring 应用程序提供了简便的缓存管理抽象,通过注解如 @Cacheable、@CacheEvict、@CachePut,开发者能方便定义缓存策略。整合Spring Boot 与 Redis 缓存实例展示了如何配置、使用Spring Cache,提升应用性能。通过实战演示菜单路由缓存,突显 @Cacheable 和 @CacheEvict 的实际应用,以及解决 @Cacheable 默认key双冒号的问题。
开源项目
SpringCloud + Vue3 微服务商城
Github | Gitee | |
后端 | youlai-mall🍃 | youlai-mall🍃 |
前端 | mall-admin🌺 | mall-admin🌺 |
移动端 | mall-app🍌 | mall-app🍌 |
SpringBoot 3+ Vue3 单体权限管理系统
Github | Gitee | |
后端 | youlai-boot🍃 | youlai-boot🍃 |
前端 | vue3-element-admin🌺 | vue3-element-admin🌺 |