Spring Cache 注解详解:@Cacheable、@CachePut、@CacheEvict
Spring Cache 注解详解:@Cacheable、@CachePut、@CacheEvict
在现代应用程序开发中,缓存是提高性能和减少数据库负载的关键技术。Spring Framework 通过其强大的缓存抽象(Spring Cache Abstraction)提供了对缓存的声明式支持。通过使用注解,开发者可以轻松地将缓存逻辑集成到应用程序中,而无需编写繁琐的缓存管理代码。
本文将深入探讨 Spring Cache 中最常用的三个注解:@Cacheable
、@CachePut
和 @CacheEvict
。我们将详细解释每个注解的用途、属性、工作原理,并通过示例代码展示如何在实际应用中使用它们。
1. Spring Cache 抽象概述
在深入了解各个注解之前,让我们先简要回顾一下 Spring Cache 抽象的核心概念:
- 缓存管理器(CacheManager):负责管理一个或多个缓存。Spring 提供了多种
CacheManager
实现,例如ConcurrentMapCacheManager
(用于基于内存的缓存)、EhCacheCacheManager
(用于 Ehcache)、RedisCacheManager
(用于 Redis)等。 - 缓存(Cache):存储数据的实际位置。每个缓存都有一个唯一的名称。
- 键(Key):用于在缓存中唯一标识一个条目的值。Spring 使用 SpEL(Spring Expression Language)表达式来生成键。
- 值(Value):存储在缓存中的实际数据。
Spring Cache 的工作流程如下:
- 当一个带有缓存注解的方法被调用时,Spring 会拦截该方法。
- Spring 根据注解中的配置(例如缓存名称、键生成策略)尝试从缓存中查找数据。
- 如果缓存命中(找到数据),则直接从缓存返回数据,不执行方法体。
- 如果缓存未命中,则执行方法体,并将方法的返回值存储到缓存中。
- 对于更新或删除缓存的注解,Spring 会在方法执行后更新或删除缓存中的相应条目。
2. @Cacheable 注解
@Cacheable
注解是最常用的缓存注解。它用于标记一个方法的结果应该被缓存。当一个带有 @Cacheable
注解的方法被调用时,Spring 会首先检查缓存中是否已经存在与该方法调用相对应的条目。如果存在,则直接从缓存返回结果,而不会执行方法体。如果不存在,则执行方法体,并将方法的返回值存储到缓存中,以便后续的调用可以直接从缓存中获取结果。
2.1. 属性
@Cacheable
注解具有以下主要属性:
value
/cacheNames
(String[]):指定一个或多个缓存的名称。这是@Cacheable
注解的必需属性。key
(String):指定用于生成缓存键的 SpEL 表达式。如果未指定,Spring 将使用默认的键生成策略(基于方法参数)。keyGenerator
(String):指定自定义的KeyGenerator
bean 的名称。如果指定了keyGenerator
,则会忽略key
属性。cacheManager
(String):指定自定义的CacheManager
bean 的名称。如果未指定,Spring 将使用默认的CacheManager
。cacheResolver
(String):指定自定义的CacheResolver
bean 的名称。CacheResolver
用于动态解析缓存名称。如果指定了cacheResolver
,则会忽略value
、cacheNames
和cacheManager
属性。condition
(String):指定一个 SpEL 表达式,用于条件性地启用缓存。只有当表达式的结果为true
时,才会进行缓存操作。unless
(String):指定一个 SpEL 表达式,用于条件性地禁用缓存。只有当表达式的结果为true
时,才会阻止将方法结果放入缓存。sync
(boolean): 是否开启异步模式, 默认为false
。当开启sync=true
时,多个线程同时访问同一个缓存时,只有一个线程会执行方法,其他线程会阻塞等待,直到缓存中有数据。
2.2. 工作原理
- 缓存查找:当一个带有
@Cacheable
注解的方法被调用时,Spring 首先会根据value
或cacheNames
属性确定要使用的缓存。 - 键生成:然后,Spring 会根据
key
属性(或默认的键生成策略)生成一个缓存键。 - 缓存命中检查:Spring 使用生成的键在缓存中查找相应的条目。
- 缓存命中:如果找到匹配的条目,Spring 直接从缓存中返回该条目的值,而不会执行方法体。
- 缓存未命中:如果没有找到匹配的条目,Spring 会执行方法体。
- 缓存更新:方法执行完成后,Spring 将方法的返回值与生成的键一起存储到缓存中。
2.3. 示例
```java
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
@Service
public class ProductService {
@Cacheable(value = "products", key = "#id")
public Product getProductById(Long id) {
// 模拟从数据库中查询产品
System.out.println("Fetching product from database for id: " + id);
return new Product(id, "Product " + id, 99.99);
}
@Cacheable(value = "products", key = "#root.methodName + '_' + #name")
public Product findProductByName(String name){
// 模拟从数据库查询
System.out.println("根据名称" + name + "查询商品");
return new Product(123L, name, 199.99);
}
@Cacheable(value = "products", key = "#id", condition = "#id > 10")
public Product getProductByIdIfGreaterThanTen(Long id){
System.out.println("只有当id大于10时才会查询和缓存");
return new Product(id, "Product " + id, 10.99);
}
@Cacheable(value = "products", key = "#id", unless = "#result.price < 20")
public Product getProductByIdUnlessPriceLessThanTwenty(Long id){
System.out.println("查询商品,除非价格小于20");
return new Product(id, "Product " + id, 20.99);
}
}
```
在上面的示例中:
getProductById
方法使用@Cacheable
注解,指定了缓存名称为 "products",并使用方法参数id
作为缓存键。findProductByName
方法使用#root.methodName
获取了方法名,结合#name
参数进行缓存。getProductByIdIfGreaterThanTen
使用了condition
属性,只有当id
参数大于10时才会进行查询和缓存。getProductByIdUnlessPriceLessThanTwenty
方法使用了unless
属性,只有当返回结果Product
对象的price
属性小于20时,不会缓存结果。
3. @CachePut 注解
@CachePut
注解用于将方法的返回值更新到缓存中。与 @Cacheable
不同,@CachePut
注解不会阻止方法的执行,它总是会执行方法体,并将方法的返回值更新到缓存中。@CachePut
主要用于更新缓存中的数据,而不会影响方法的执行。
3.1. 属性
@CachePut
注解的属性与 @Cacheable
注解基本相同:
value
/cacheNames
(String[]):指定一个或多个缓存的名称。key
(String):指定用于生成缓存键的 SpEL 表达式。keyGenerator
(String):指定自定义的KeyGenerator
bean 的名称。cacheManager
(String):指定自定义的CacheManager
bean 的名称。cacheResolver
(String):指定自定义的CacheResolver
bean 的名称。condition
(String):指定一个 SpEL 表达式,用于条件性地启用缓存更新。unless
(String):指定一个 SpEL 表达式, 阻止更新缓存。
3.2. 工作原理
- 方法执行:当一个带有
@CachePut
注解的方法被调用时,Spring 会执行方法体。 - 键生成:方法执行完成后,Spring 会根据
key
属性(或默认的键生成策略)生成一个缓存键。 - 缓存更新:Spring 将方法的返回值与生成的键一起存储到缓存中,如果缓存中已经存在相同的键,则会覆盖原有的值。
3.3. 示例
```java
import org.springframework.cache.annotation.CachePut;
import org.springframework.stereotype.Service;
@Service
public class ProductService {
@CachePut(value = "products", key = "#product.id")
public Product updateProduct(Product product) {
// 模拟更新数据库中的产品
System.out.println("Updating product in database: " + product);
return product;
}
}
```
在上面的示例中,updateProduct
方法使用 @CachePut
注解,指定了缓存名称为 "products",并使用方法参数 product
的 id
属性作为缓存键。无论缓存中是否已经存在该产品,updateProduct
方法都会执行,并将更新后的 Product
对象存储到缓存中。
4. @CacheEvict 注解
@CacheEvict
注解用于从缓存中移除一个或多个条目。它可以根据指定的键或条件来移除缓存条目,也可以清空整个缓存。
4.1. 属性
@CacheEvict
注解具有以下主要属性:
value
/cacheNames
(String[]):指定一个或多个缓存的名称。key
(String):指定用于生成缓存键的 SpEL 表达式。keyGenerator
(String):指定自定义的KeyGenerator
bean 的名称。cacheManager
(String):指定自定义的CacheManager
bean 的名称。cacheResolver
(String):指定自定义的CacheResolver
bean 的名称。condition
(String):指定一个 SpEL 表达式,用于条件性地启用缓存清除。allEntries
(boolean):是否清空整个缓存。默认为false
。如果设置为true
,则会忽略key
属性。beforeInvocation
(boolean): 默认是在方法执行之后移除缓存中的数据。如果设置为true
,则会在方法执行之前移除缓存中的数据。
4.2. 工作原理
- 键生成:如果
allEntries
为false
,Spring 会根据key
属性(或默认的键生成策略)生成一个缓存键。 - 缓存清除:
- 如果
allEntries
为false
,Spring 会根据生成的键从缓存中移除相应的条目。 - 如果
allEntries
为true
,Spring 会清空整个缓存。
- 如果
- 方法执行: 如果
beforeInvocation
为false
(默认),方法体会执行。
4.3. 示例
```java
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.stereotype.Service;
@Service
public class ProductService {
@CacheEvict(value = "products", key = "#id")
public void deleteProduct(Long id) {
// 模拟从数据库中删除产品
System.out.println("Deleting product from database with id: " + id);
}
@CacheEvict(value = "products", allEntries = true)
public void clearAllProducts() {
// 清空所有产品缓存
System.out.println("Clearing all products from cache");
}
@CacheEvict(value = "products", key = "#id", beforeInvocation = true)
public void deleteProductBeforeInvocation(Long id) {
System.out.println("Deleting product from database and cache with id (before invocation): " + id);
}
}
```
在上面的示例中:
deleteProduct
方法使用@CacheEvict
注解,指定了缓存名称为 "products",并使用方法参数id
作为缓存键。当该方法被调用时,Spring 会从缓存中移除与该id
对应的产品。clearAllProducts
方法使用@CacheEvict
注解,并设置allEntries
为true
。当该方法被调用时,Spring 会清空 "products" 缓存中的所有条目。deleteProductBeforeInvocation
方法设置了beforeInvocation
为true
, 则会在方法执行前就清除缓存。
5. 组合使用缓存注解
在实际应用中,我们经常需要组合使用多个缓存注解来实现更复杂的缓存逻辑。例如,我们可以使用 @Cacheable
来缓存方法的查询结果,使用 @CachePut
来更新缓存,使用 @CacheEvict
来删除缓存。
```java
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
@Service
public class ProductService {
@Cacheable(value = "products", key = "#id")
public Product getProductById(Long id) {
// 模拟从数据库中查询产品
System.out.println("Fetching product from database for id: " + id);
return new Product(id, "Product " + id, 99.99);
}
@CachePut(value = "products", key = "#product.id")
public Product updateProduct(Product product) {
// 模拟更新数据库中的产品
System.out.println("Updating product in database: " + product);
return product;
}
@CacheEvict(value = "products", key = "#id")
public void deleteProduct(Long id) {
// 模拟从数据库中删除产品
System.out.println("Deleting product from database with id: " + id);
}
}
```
在这个例子中,getProductById
方法使用 @Cacheable
注解来缓存查询结果,updateProduct
方法使用 @CachePut
注解来更新缓存,deleteProduct
方法使用 @CacheEvict
注解来删除缓存。这样,我们就实现了一个完整的 CRUD(创建、读取、更新、删除)操作的缓存逻辑。
6. SpEL 表达式
Spring Cache 使用 SpEL(Spring Expression Language)表达式来生成缓存键和定义条件。SpEL 是一种功能强大的表达式语言,可以访问方法参数、方法返回值、对象属性、系统属性等。
以下是一些常用的 SpEL 表达式示例:
#id
:访问方法参数id
。#product.id
:访问方法参数product
的id
属性。#result
:访问方法的返回值。#root.methodName
:访问当前方法的名称。#root.args[0]
:访问方法的第一个参数。#systemProperties['java.version']
:访问系统属性java.version
。
7. 总结
Spring Cache 抽象为 Java 应用程序提供了简单而强大的缓存支持。通过使用 @Cacheable
、@CachePut
和 @CacheEvict
等注解,开发者可以轻松地将缓存逻辑集成到应用程序中,而无需编写繁琐的缓存管理代码。
本文详细介绍了这三个注解的用途、属性、工作原理,并通过示例代码展示了如何在实际应用中使用它们。希望本文能够帮助你更好地理解和使用 Spring Cache,提高应用程序的性能和可伸缩性。
在使用 Spring Cache 时,还需要注意以下几点:
- 选择合适的缓存管理器:根据应用程序的需求选择合适的
CacheManager
实现。 - 设计合理的缓存键:确保缓存键的唯一性和可读性。
- 避免缓存雪崩、缓存穿透和缓存击穿:了解这些常见的缓存问题,并采取相应的措施来避免它们。
- 监控缓存性能:定期监控缓存的命中率、使用情况等指标,以便及时发现和解决问题。
通过合理地使用 Spring Cache,可以显著提高应用程序的性能,减少数据库负载,提升用户体验。