Spring Cache 使用教程:配置与注解详解
在现代应用程序开发中,性能是至关重要的一个方面。频繁地访问数据库、调用外部服务或执行耗时计算是常见的性能瓶颈。缓存(Caching)是一种有效的优化手段,它通过将计算结果或数据存储在快速访问的存储介质(如内存)中,减少对原始数据源的访问次数,从而显著提升应用程序的响应速度和吞吐量。
Spring 框架提供了一个强大的缓存抽象(Cache Abstraction),它允许开发者在不修改底层代码逻辑的情况下,通过简单的配置或注解即可为方法添加缓存功能。Spring Cache Abstraction 的核心优势在于它屏蔽了底层缓存实现的细节,开发者可以选择多种缓存提供者(如EhCache, Redis, Caffeine, ConcurrentMap等)而无需改动业务代码,极大地提高了代码的可维护性和灵活性。
本文将深入探讨 Spring Cache 的使用,从基础概念到详细的配置与注解,帮助您全面掌握如何在 Spring 应用中有效地利用缓存。
1. Spring Cache 核心概念
在深入配置和注解之前,理解 Spring Cache 的几个核心概念至关重要:
- Cache Abstraction (缓存抽象): 这是 Spring Cache 的基石。它定义了一系列接口和类,如
Cache
和CacheManager
,屏蔽了底层缓存实现的差异。 - CacheManager (缓存管理器): 负责管理所有的
Cache
实例。它是 Spring Cache 抽象的入口点。应用程序通过CacheManager
获取或创建Cache
实例,并对缓存进行各种操作。Spring 提供了多种CacheManager
实现,对应不同的缓存提供者。 - Cache (缓存实例): 代表一个具体的缓存区域,通常是一个键值对的集合。每个
Cache
都有一个唯一的名称。数据被存储在特定的Cache
实例中。 - Key (缓存键): 用于唯一标识缓存中的一个条目。Spring Cache 提供了默认的键生成策略,也可以自定义。
- Value (缓存值): 存储在缓存中的数据。通常是方法的返回值。
- Cache Annotations (缓存注解): 这是 Spring Cache 最常用的使用方式。通过在方法或类上添加注解,声明该方法的缓存行为(读取、写入、删除)。
2. 启用 Spring Cache
要在 Spring 应用中使用缓存,首先需要在配置类上启用缓存功能。这通过 @EnableCaching
注解实现。
“`java
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableCaching // 启用缓存功能
public class CacheConfig {
// 这里可以配置 CacheManager bean,后面会详细介绍
}
“`
@EnableCaching
注解通常与 @Configuration
注解一起使用。它会激活 Spring 的缓存注解处理器,扫描应用中使用了缓存注解(如 @Cacheable
, @CachePut
, @CacheEvict
等)的 Bean,并为这些 Bean 创建代理,以便在方法执行时拦截并应用缓存逻辑。
在启用了 @EnableCaching
后,Spring 会尝试自动配置一个合适的 CacheManager
。如果classpath下存在特定的缓存库(如 EhCache, Redis, Caffeine 等),Spring Boot 会基于这些库提供默认的 CacheManager
配置。如果没有检测到特定的缓存库,或者您想使用简单的内存缓存,Spring Boot 默认会配置一个 ConcurrentMapCacheManager
。
3. 核心缓存注解详解
Spring Cache 提供了多个核心注解来声明方法的缓存行为。我们将逐一详细介绍它们。
3.1 @Cacheable
-
作用: 标记一个方法,表示该方法的返回值可以被缓存。在方法执行前,Spring 会先根据缓存键到指定的缓存中查找数据。如果找到,则直接返回缓存中的值,方法不再执行;如果没找到,则执行方法,并将方法的返回值存入缓存,然后再返回该值。这是一种“读优先”的策略。
-
常用属性:
value
或cacheNames
: 必填。指定缓存的名称(一个或多个)。可以使用字符串数组{ "cache1", "cache2" }
。key
: 可选。指定缓存键的生成策略。可以使用 SpEL (Spring Expression Language) 表达式。默认策略会根据方法参数生成键。condition
: 可选。一个 SpEL 表达式,如果表达式结果为false
,则该方法不进行缓存操作(不从缓存读取,也不写入缓存)。此表达式在方法执行前判断。unless
: 可选。一个 SpEL 表达式,如果表达式结果为true
,则方法的返回值不被存入缓存。此表达式在方法执行后判断。通常用于排除某些不适合缓存的结果,例如返回null
或空集合。sync
: 可选。布尔值,默认为false
。如果设置为true
,则表示如果多个线程同时访问同一个缓存键,只有一个线程会实际执行方法,其他线程会等待方法执行完成后从缓存中获取结果。这有助于避免缓存击穿(Cache Penetration)问题,但会牺牲一定的并发性。
-
SpEL 表达式在
@Cacheable
中的应用:#paramName
: 引用方法参数,如#userId
,#product.id
.#root.methodName
: 引用当前方法名。#root.args
: 引用方法参数数组。#root.target
: 引用目标 Bean 实例。#root.targetClass
: 引用目标 Bean 的 Class。#root.method
: 引用目标方法。#result
: 引用方法执行后的返回值 (仅在unless
和cacheResolver
/keyGenerator
/errorHandler
等属性中使用)。
-
示例:
“`java
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;import java.util.List;
@Service
public class ProductService {// 缓存查询所有产品的结果,缓存名称为 "products",使用默认键生成策略 @Cacheable("products") public List<Product> getAllProducts() { System.out.println("Executing getAllProducts - fetching from DB"); // 模拟从数据库或其他数据源获取数据 return getProductsFromDatabase(); } // 根据产品ID查询产品,缓存名称为 "product",缓存键使用方法参数 productId @Cacheable(value = "product", key = "#productId") public Product getProductById(Long productId) { System.out.println("Executing getProductById - fetching product " + productId + " from DB"); // 模拟根据ID查询产品 return getProductFromDatabase(productId); } // 根据用户ID和产品ID查询用户收藏的产品,缓存名称为 "userFavorites", // 缓存键使用用户ID和产品ID的组合 @Cacheable(value = "userFavorites", key = "#userId + '-' + #productId") public Product getUserFavoriteProduct(Long userId, Long productId) { System.out.println("Executing getUserFavoriteProduct for user " + userId + " and product " + productId); // 模拟查询用户收藏 return getUserFavoriteFromDatabase(userId, productId); } // 缓存结果,但如果返回值为 null 则不缓存 @Cacheable(value = "product", key = "#productId", unless = "#result == null") public Product getProductByIdUnlessNull(Long productId) { System.out.println("Executing getProductByIdUnlessNull - fetching product " + productId + " from DB"); return getProductFromDatabase(productId); // 假设可能返回 null } // 条件性缓存:只有 productId 大于 100 时才缓存 @Cacheable(value = "product", key = "#productId", condition = "#productId > 100") public Product getProductByIdIfLarge(Long productId) { System.out.println("Executing getProductByIdIfLarge - fetching product " + productId + " from DB"); return getProductFromDatabase(productId); } // 模拟数据库操作方法 private List<Product> getProductsFromDatabase() { /* ... */ return null; } private Product getProductFromDatabase(Long productId) { /* ... */ return null; } private Product getUserFavoriteFromDatabase(Long userId, Long productId) { /* ... */ return null; }
}
“`
3.2 @CachePut
-
作用: 标记一个方法,表示该方法始终会被执行,并将方法的返回值存入指定的缓存。它主要用于缓存更新场景,确保方法执行后缓存是最新的数据。
-
常用属性: 与
@Cacheable
类似,包括value
/cacheNames
,key
,condition
,unless
。不同之处在于,unless
在@CachePut
中表示如果条件为true
,则方法的返回值不被存入缓存。 -
重要区别与
@Cacheable
:@Cacheable
: 方法执行前检查缓存。如果命中,方法不执行。@CachePut
: 方法始终执行。方法执行后更新缓存。
-
SpEL 表达式: 同
@Cacheable
。 -
示例:
“`java
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;// … Product class …
@Service
public class ProductService {// ... @Cacheable methods ... // 更新产品信息,并将最新的产品信息存入缓存 // 注意这里的key要与@Cacheable(value="product", key="#product.id") 的key一致 @CachePut(value = "product", key = "#product.id") public Product updateProduct(Product product) { System.out.println("Executing updateProduct - updating product " + product.getId() + " in DB"); // 模拟更新数据库中的产品 Product updatedProduct = updateProductInDatabase(product); System.out.println("Updated product in cache with new data."); return updatedProduct; // 返回更新后的产品对象,它会被放入缓存 } // ... simulate database methods ... private Product updateProductInDatabase(Product product) { /* ... */ return product; }
}
``
updateProduct(someProduct)
在这个例子中,当你调用方法时,
updateProductInDatabase会被执行,数据库中的产品会被更新。然后,该方法的返回值 (更新后的
someProduct) 会被放入名为
product的缓存中,使用
someProduct.getId()作为键。下次再调用
getProductById(someProduct.getId())` 时,就会直接从缓存中获取最新的数据。
3.3 @CacheEvict
-
作用: 标记一个方法,表示该方法执行后需要从缓存中移除一个或多个条目。主要用于缓存失效或删除场景。
-
常用属性:
value
或cacheNames
: 必填。指定要操作的缓存名称。key
: 可选。指定要移除的缓存条目的键。可以使用 SpEL 表达式。condition
: 可选。一个 SpEL 表达式,如果表达式结果为false
,则不执行缓存移除操作。此表达式在方法执行前判断。allEntries
: 可选。布尔值,默认为false
。如果设置为true
,则清除指定缓存中的所有条目,忽略key
属性。beforeInvocation
: 可选。布尔值,默认为false
。false
(默认): 在方法成功执行后清除缓存。如果方法执行过程中抛出异常,则不清除缓存。true
: 在方法执行前立即清除缓存。无论方法是否成功执行,都会执行清除操作。这对于确保事务回滚时缓存也被清理非常有用。
-
SpEL 表达式: 同
@Cacheable
(#result
在condition
中不可用,因为方法还没执行;在unless
中可用,但@CacheEvict
没有unless
属性)。 -
示例:
“`java
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;// … Product class …
@Service
public class ProductService {// ... @Cacheable and @CachePut methods ... // 根据产品ID删除产品,并从缓存中移除对应条目 @CacheEvict(value = "product", key = "#productId") public void deleteProduct(Long productId) { System.out.println("Executing deleteProduct - deleting product " + productId + " from DB"); // 模拟从数据库删除产品 deleteProductFromDatabase(productId); System.out.println("Evicted product " + productId + " from cache."); } // 添加新产品,并清除所有产品列表缓存(因为列表已不再最新) @CacheEvict(value = "products", allEntries = true) public Product addProduct(Product product) { System.out.println("Executing addProduct - adding new product to DB"); Product newProduct = addProductToDatabase(product); System.out.println("Evicted all entries from 'products' cache."); return newProduct; } // 删除产品,如果删除失败则不清除缓存(默认行为) // 如果需要无论成功失败都清除,将 beforeInvocation 设置为 true @CacheEvict(value = "product", key = "#productId", beforeInvocation = false) // false 是默认值,可省略 public void deleteProductSafely(Long productId) { System.out.println("Executing deleteProductSafely for product " + productId); // 模拟删除,可能抛出异常 deleteProductFromDatabasePossiblyFailing(productId); System.out.println("Evicted product " + productId + " after successful deletion."); } // 模拟数据库操作方法 private void deleteProductFromDatabase(Long productId) { /* ... */ } private Product addProductToDatabase(Product product) { /* ... */ return product; } private void deleteProductFromDatabasePossiblyFailing(Long productId) { /* ... */ }
}
“`
3.4 @Caching
-
作用: 在同一个方法上应用多个缓存注解。例如,一个操作可能既需要更新某个缓存条目,又需要清除另一个缓存(如列表缓存)。
-
常用属性:
cacheable
:@Cacheable
注解数组。put
:@CachePut
注解数组。evict
:@CacheEvict
注解数组。
-
示例:
“`java
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Caching;
import org.springframework.stereotype.Service;// … Product class …
@Service
public class ProductService {// ... other methods ... // 更新产品,同时更新单个产品缓存,并清除所有产品列表缓存 @Caching( put = { @CachePut(value = "product", key = "#product.id") }, evict = { @CacheEvict(value = "products", allEntries = true) } ) public Product updateProductAndInvalidateList(Product product) { System.out.println("Executing updateProductAndInvalidateList for product " + product.getId()); // 模拟更新数据库 Product updatedProduct = updateProductInDatabase(product); System.out.println("Product " + product.getId() + " updated, cache 'product' refreshed, cache 'products' cleared."); return updatedProduct; } private Product updateProductInDatabase(Product product) { /* ... */ return product; }
}
“`
4. 配置 CacheManager
如前所述,Spring Cache Abstraction 需要一个具体的 CacheManager
实现。Spring Boot 会自动尝试配置,但您也可以手动配置以指定缓存类型或进行更精细的控制。
配置 CacheManager
通常是定义一个 @Bean
方法,返回 org.springframework.cache.CacheManager
接口的实现类。
4.1 使用 ConcurrentMapCacheManager
(默认简单内存缓存)
这是 Spring Boot 在没有其他缓存库时的默认选项。您可以显式配置它。
“`java
import org.springframework.cache.CacheManager;
import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.cache.annotation.EnableCaching;
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
// 创建一个 ConcurrentMapCacheManager,可以指定预定义的缓存名称
// ConcurrentMapCacheManager 会在需要时自动创建缓存,即使不在此指定名称
ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager("products", "product", "userFavorites");
return cacheManager;
}
}
``
ConcurrentMapCacheManager使用
ConcurrentMap` 作为底层存储,适用于简单的单应用实例场景或测试。它不具备过期、淘汰策略等高级功能。
4.2 配置 RedisCacheManager (分布式缓存)
Redis 是一个流行的内存数据库,常用于分布式缓存。使用 Redis 作为 Spring Cache 的提供者需要添加 Spring Data Redis 依赖,并配置 Redis 连接。
依赖:
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> <!-- 包含 Spring Cache Abstraction -->
</dependency>
(如果使用 Spring Boot,通常 spring-boot-starter-cache
会传递依赖 spring-context-support
,而 spring-boot-starter-data-redis
会传递依赖 Redis 客户端库如 Lettuce 或 Jedis)
配置:
“`java
import org.springframework.cache.CacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
@Configuration
@EnableCaching
public class RedisCacheConfig {
@Bean
@Primary // 当有多个 CacheManager 时,指定这是主要的那个
public CacheManager redisCacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
// 设置缓存过期时间 (例如 30 分钟)
.entryTtl(Duration.ofMinutes(30))
// 禁止缓存 null 值
.disableCachingNullValues()
// 设置 key 和 value 的序列化方式
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())); // 使用 JSON 序列化 value
return RedisCacheManager.builder(connectionFactory)
.initialCacheNames(Set.of("products", "product")) // 预设一些缓存名称,不预设也可以,RedisCacheManager 会动态创建
.withCacheConfiguration("product", config.entryTtl(Duration.ofMinutes(60))) // 为特定的缓存设置不同的配置 (例如 "product" 缓存过期时间为 60 分钟)
.withInitialCacheConfigurations(Map.of("userFavorites", config.entryTtl(Duration.ofDays(1)))) // 也可以使用 Map 设置多个
.cacheDefaults(config) // 设置默认的缓存配置
.build();
}
// 确保您在 application.properties 或 application.yml 中配置了 Redis 连接信息
// spring.redis.host=localhost
// spring.redis.port=6379
}
``
StringRedisSerializer
配置 RedisCacheManager 时,通常需要考虑序列化问题(如何将 Java 对象转换为字节流存储到 Redis,以及如何反序列化)。常用的序列化器有(用于 key),
GenericJackson2JsonRedisSerializer(使用 Jackson 库将对象序列化为 JSON),
JdkSerializationRedisSerializer` (使用 Java 原生序列化,但有兼容性问题且效率不高)。建议使用 JSON 序列化,它更具可读性和跨语言兼容性。
您还可以为不同的缓存 (cacheNames
) 设置不同的过期时间或其他配置。
4.3 配置 CaffeineCacheManager (高性能内存缓存)
Caffeine 是一个高性能的 Java 缓存库,它是 Guava Cache 的升级版。使用 Caffeine 作为 Spring Cache 提供者需要添加 Caffeine 依赖。
依赖:
xml
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
配置:
“`java
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.cache.CacheManager;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.cache.annotation.EnableCaching;
import java.util.concurrent.TimeUnit;
@Configuration
@EnableCaching
public class CaffeineCacheConfig {
@Bean
public CacheManager caffeineCacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
// 配置默认的缓存策略
cacheManager.setCaffeine(Caffeine.newBuilder()
.initialCapacity(100) // 初始容量
.maximumSize(500) // 最大缓存条目数
.expireAfterAccess(10, TimeUnit.MINUTES) // 访问后10分钟过期
.recordStats()); // 开启统计功能 (可选)
// 也可以为特定的缓存名称设置不同的配置
// cacheManager.setCaches("products", "product"); // 预设缓存名称
// 另一种设置不同缓存配置的方式
// CaffeineCacheManager cacheManager = new CaffeineCacheManager("products", "product");
// cacheManager.setCaffeine(Caffeine.newBuilder().expireAfterWrite(1, TimeUnit.HOURS)); // 设置默认过期时间
// cacheManager.getCache("product").put("key", "value"); // 获取并配置特定缓存 (需要先在构造函数或 setCaches 设置名称)
// 更灵活的方式是使用 CaffeineSpec 字符串配置
// cacheManager.setCacheSpecification("initialCapacity=100,maximumSize=500,expireAfterAccess=10m,recordStats");
return cacheManager;
}
}
“`
Caffeine 提供了丰富的缓存策略选项,如基于时间(写入后、访问后)和基于大小的过期/淘汰策略。您可以根据实际需求进行配置。
4.4 配置 EhCacheCacheManager (老牌内存缓存)
EhCache 是一个历史悠久的 Java 缓存库,功能强大。需要添加 EhCache 依赖和配置 ehcache.xml
文件。
依赖:
xml
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>2.10.6</version> <!-- 或更新版本 -->
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId> <!-- 包含 EhCacheCacheManager -->
</dependency>
配置:
-
创建
ehcache.xml
文件,通常放在src/main/resources
目录下。“`xml
<diskStore path="java.io.tmpdir"/> <!-- 可选,用于溢写到磁盘 --> <defaultCache maxElementsInMemory="10000" eternal="false" timeToIdleSeconds="120" timeToLiveSeconds="120" overflowToDisk="false" diskPersistent="false" diskExpiryThreadIntervalSeconds="120" memoryStoreEvictionPolicy="LRU"> </defaultCache> <cache name="products" maxElementsInMemory="500" eternal="false" timeToLiveSeconds="300" memoryStoreEvictionPolicy="LRU"> </cache> <cache name="product" maxElementsInMemory="1000" eternal="false" timeToLiveSeconds="600" memoryStoreEvictionPolicy="LRU"> </cache>
“` -
配置
CacheManager
Bean:“`java
import net.sf.ehcache.config.CacheConfiguration;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.ehcache.EhCacheCacheManager;
import org.springframework.cache.ehcache.EhCacheManagerFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;@Configuration
@EnableCaching
public class EhCacheConfig {@Bean public EhCacheManagerFactoryBean ehcacheManagerFactoryBean() { EhCacheManagerFactoryBean ehCacheManagerFactoryBean = new EhCacheManagerFactoryBean(); ehCacheManagerFactoryBean.setConfigLocation(new ClassPathResource("ehcache.xml")); // 指定 ehcache.xml 配置文件位置 ehCacheManagerFactoryBean.setShared(true); // 设置为共享模式 (推荐) return ehCacheManagerFactoryBean; } @Bean public CacheManager ehCacheCacheManager(EhCacheManagerFactoryBean ehcacheManagerFactoryBean) { return new EhCacheCacheManager(ehcacheManagerFactoryBean.getObject()); }
}
``
EhCacheManagerFactoryBean
通过加载
ehcache.xml配置,然后将其包装在
EhCacheCacheManager` 中供 Spring Cache 使用。
4.5 使用 GenericCacheManager (组合多个 CacheManager)
如果您想同时使用多种缓存技术(例如,内存缓存用于快速访问少量数据,Redis 用于分布式缓存大量数据),可以使用 GenericCacheManager
组合多个 Cache
。但更常见的做法可能是根据不同的业务场景或数据类型,配置不同的 CacheManager
,并在 @Cacheable
等注解中使用 cacheManager
属性指定使用哪个 CacheManager
。
“`java
// 假设你已经配置了 concurrentMapCacheManager 和 redisCacheManager
// 在注解中使用 cacheManager 属性指定
@Cacheable(value = “product”, key = “#productId”, cacheManager = “redisCacheManager”)
public Product getProductByIdFromRedis(Long productId) { / … / }
@Cacheable(value = “tempData”, key = “#itemId”, cacheManager = “concurrentMapCacheManager”)
public TempData getTempDataFromMemory(Long itemId) { / … / }
“`
5. 自定义键生成策略
Spring Cache 提供了默认的键生成策略 (SimpleKeyGenerator
):
* 如果方法没有参数,键为 SimpleKey.EMPTY
。
* 如果方法只有一个参数,键为该参数本身。
* 如果方法有多个参数,键为包含所有参数的 SimpleKey
对象(考虑参数顺序)。
这种默认策略对于大多数简单场景已经足够,但有时您可能需要更复杂的键生成逻辑,例如:
* 只使用部分参数作为键。
* 使用参数对象的某个属性作为键。
* 将多个参数组合成一个字符串作为键。
* 在分布式环境中需要一个更具识别性的键(例如包含应用实例ID)。
您可以实现 org.springframework.cache.interceptor.KeyGenerator
接口来创建自定义键生成器。
“`java
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.cache.interceptor.SimpleKey;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.Arrays;
@Component(“myCustomKeyGenerator”) // 注册为 Spring Bean
public class MyCustomKeyGenerator implements KeyGenerator {
@Override
public Object generate(Object target, Method method, Object... params) {
// 示例:使用方法名和所有参数的哈希码组合作为键
// 实际应用中应根据业务逻辑生成更具意义的键
StringBuilder keyBuilder = new StringBuilder();
keyBuilder.append(method.getName());
if (params.length > 0) {
keyBuilder.append("-").append(Arrays.deepHashCode(params));
}
System.out.println("Generated custom key: " + keyBuilder.toString());
return keyBuilder.toString();
// 或者,如果方法只有一个 Product 参数,使用 Product 的 ID
/*
if (params.length == 1 && params[0] instanceof Product) {
Product product = (Product) params[0];
return "product-" + product.getId();
}
return SimpleKeyGenerator.generateKey(params); // 退回到默认策略
*/
}
}
“`
然后在缓存注解中使用 keyGenerator
属性引用自定义的 Bean:
“`java
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
@Service
public class ProductService {
// 使用自定义的 keyGenerator 生成键
@Cacheable(value = "product", keyGenerator = "myCustomKeyGenerator")
public Product getProductComplex(Long id, String type) {
System.out.println("Executing getProductComplex with ID " + id + " and type " + type);
// ... fetch logic ...
return new Product(id, "Product " + id + " Type " + type);
}
// ... simulate fetch method ...
}
“`
6. 条件缓存 (condition
和 unless
)
condition
和 unless
属性提供了根据 SpEL 表达式动态决定是否进行缓存操作的能力。
-
condition
: 在方法执行前判断。如果为false
,则跳过整个缓存操作(不检查缓存,不执行方法,不写入缓存)。- 示例:
@Cacheable(value="product", key="#productId", condition="#productId > 0")
– 只有productId
大于 0 时才进行缓存操作。
- 示例:
-
unless
: 在方法执行后判断(仅对@Cacheable
和@CachePut
有意义)。如果为true
,则方法的返回值不会被写入缓存。- 示例:
@Cacheable(value="product", key="#productId", unless="#result == null || #result.status == 'ERROR'")
– 如果方法返回null
或返回对象的status
是 ‘ERROR’,则不缓存该结果。 - 示例:
@CachePut(value="product", key="#product.id", unless="#result.price < 0")
– 如果更新后的产品价格小于 0,则不将该结果存入缓存。
- 示例:
7. Cache 错误处理
Spring Cache 允许您定义一个 CacheErrorHandler
来处理缓存操作过程中发生的异常(如 Redis 连接中断)。默认情况下,缓存操作中的异常会被传播,可能导致方法执行失败。通过自定义 CacheErrorHandler
,您可以选择忽略这些错误,记录日志,或者执行其他恢复逻辑。
实现 CacheErrorHandler
接口,并将其注册为 Bean。
“`java
import org.springframework.cache.Cache;
import org.springframework.cache.interceptor.CacheErrorHandler;
import org.springframework.stereotype.Component;
@Component
public class CustomCacheErrorHandler implements CacheErrorHandler {
@Override
public void handleCacheGetError(RuntimeException exception, Cache cache, Object key) {
// 处理缓存读取 (get) 时的错误
System.err.println("Cache get error: " + exception.getMessage() + " in cache '" + cache.getName() + "' for key '" + key + "'");
// 您可以选择记录日志,发送警报,或者忽略错误
// 如果忽略错误,方法会像没有缓存一样继续执行(可能访问数据源)
// throw exception; // 如果不想忽略,可以再次抛出异常
}
@Override
public void handleCachePutError(RuntimeException exception, Cache cache, Object key, Object value) {
// 处理缓存写入 (put) 时的错误
System.err.println("Cache put error: " + exception.getMessage() + " in cache '" + cache.getName() + "' for key '" + key + "' with value '" + value + "'");
// 通常写入错误可以忽略,不影响方法执行
}
@Override
public void handleCacheEvictError(RuntimeException exception, Cache cache, Object key) {
// 处理缓存删除 (evict) 时的错误
System.err.println("Cache evict error: " + exception.getMessage() + " in cache '" + cache.getName() + "' for key '" + key + "'");
// 删除错误通常也可以忽略
}
@Override
public void handleCacheClearError(RuntimeException exception, Cache cache) {
// 处理缓存清空 (clear) 时的错误 (allEntries = true)
System.err.println("Cache clear error: " + exception.getMessage() + " in cache '" + cache.getName() + "'");
// 清空错误通常也可以忽略
}
}
``
CacheErrorHandler` Bean。
Spring 会自动检测并使用应用上下文中注册的
8. 缓存策略选择与最佳实践
- 何时使用
@Cacheable
: 用于读操作频繁且结果相对稳定的方法。目的是减少重复计算或数据库访问。 - 何时使用
@CachePut
: 用于写操作(新增、更新)方法。目的是在数据更新后,确保缓存中的数据也是最新的。 - 何时使用
@CacheEvict
: 用于删除或可能导致缓存数据失效的操作。目的是清除不再有效或可能导致数据不一致的缓存条目。 - 粒度: 缓存方法的粒度要适中。不要缓存过于频繁变化的数据,也不要将整个大型对象图一股脑缓存,考虑缓存DTOs或必要的数据片段。
- 缓存过期与淘汰: 配置合适的 TTL (Time To Live) 和 TTI (Time To Idle) 策略,以及内存淘汰策略 (LRU, LFU等),避免缓存数据过旧或缓存占用过多内存。
- 缓存一致性: 在分布式环境中,缓存一致性是一个挑战。使用 Redis 等分布式缓存可以缓解部分问题,但仍然需要仔细设计更新和失效策略。
@CacheEvict(beforeInvocation = true)
在事务场景下有助于保证数据和缓存的一致性。 - 避免缓存穿透/击穿/雪崩:
- 穿透: 查询一个不存在的数据,每次都穿透到数据源。可以通过缓存空结果或布隆过滤器解决。Spring Cache 的
unless="#result == null"
可以缓存null
,但要确保底层缓存实现支持(如 Redis)。 - 击穿: 某个热点数据过期,大量请求同时穿透到数据源。
@Cacheable(sync = true)
可以部分解决,或者使用分布式锁。 - 雪崩: 大量缓存同时过期,导致所有请求都穿透到数据源。可以通过设置不同的缓存过期时间,或者在过期后异步刷新缓存来缓解。
- 穿透: 查询一个不存在的数据,每次都穿透到数据源。可以通过缓存空结果或布隆过滤器解决。Spring Cache 的
- 监控: 监控缓存的命中率、丢失率、各项操作耗时等指标,以便调整缓存策略和配置。许多缓存提供者和 Spring Boot Actuator 都提供了监控支持。
- 序列化: 确保缓存的数据是可序列化的,并且选择合适的序列化方式(如 JSON 或 Kryo)以提高效率和兼容性。
9. 总结
Spring Cache Abstraction 提供了一套强大且易用的机制来为 Spring 应用程序添加缓存功能。通过 @EnableCaching
启用缓存,并利用 @Cacheable
, @CachePut
, @CacheEvict
, @Caching
等注解,开发者可以声明性地控制方法的缓存行为。结合灵活的 SpEL 表达式,可以实现复杂的缓存键生成和条件判断。
更重要的是,Spring Cache Abstraction 允许您轻松地切换或配置不同的缓存提供者(如 ConcurrentMap, EhCache, Redis, Caffeine 等),而无需修改业务代码,极大地提高了应用的灵活性和可维护性。
掌握 Spring Cache 的使用,能够帮助您有效提升应用的性能,降低后端数据源的压力,是构建高性能 Spring 应用不可或缺的技能。希望这篇详细教程能帮助您更好地理解和应用 Spring Cache。