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 的工作流程如下:

  1. 当一个带有缓存注解的方法被调用时,Spring 会拦截该方法。
  2. Spring 根据注解中的配置(例如缓存名称、键生成策略)尝试从缓存中查找数据。
  3. 如果缓存命中(找到数据),则直接从缓存返回数据,不执行方法体。
  4. 如果缓存未命中,则执行方法体,并将方法的返回值存储到缓存中。
  5. 对于更新或删除缓存的注解,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,则会忽略 valuecacheNamescacheManager 属性。
  • condition (String):指定一个 SpEL 表达式,用于条件性地启用缓存。只有当表达式的结果为 true 时,才会进行缓存操作。
  • unless (String):指定一个 SpEL 表达式,用于条件性地禁用缓存。只有当表达式的结果为 true 时,才会阻止将方法结果放入缓存。
  • sync (boolean): 是否开启异步模式, 默认为false。当开启sync=true时,多个线程同时访问同一个缓存时,只有一个线程会执行方法,其他线程会阻塞等待,直到缓存中有数据。

2.2. 工作原理

  1. 缓存查找:当一个带有 @Cacheable 注解的方法被调用时,Spring 首先会根据 valuecacheNames 属性确定要使用的缓存。
  2. 键生成:然后,Spring 会根据 key 属性(或默认的键生成策略)生成一个缓存键。
  3. 缓存命中检查:Spring 使用生成的键在缓存中查找相应的条目。
  4. 缓存命中:如果找到匹配的条目,Spring 直接从缓存中返回该条目的值,而不会执行方法体。
  5. 缓存未命中:如果没有找到匹配的条目,Spring 会执行方法体。
  6. 缓存更新:方法执行完成后,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. 工作原理

  1. 方法执行:当一个带有 @CachePut 注解的方法被调用时,Spring 会执行方法体。
  2. 键生成:方法执行完成后,Spring 会根据 key 属性(或默认的键生成策略)生成一个缓存键。
  3. 缓存更新: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",并使用方法参数 productid 属性作为缓存键。无论缓存中是否已经存在该产品,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. 工作原理

  1. 键生成:如果 allEntriesfalse,Spring 会根据 key 属性(或默认的键生成策略)生成一个缓存键。
  2. 缓存清除
    • 如果 allEntriesfalse,Spring 会根据生成的键从缓存中移除相应的条目。
    • 如果 allEntriestrue,Spring 会清空整个缓存。
  3. 方法执行: 如果beforeInvocationfalse(默认),方法体会执行。

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 注解,并设置 allEntriestrue。当该方法被调用时,Spring 会清空 "products" 缓存中的所有条目。
  • deleteProductBeforeInvocation 方法设置了 beforeInvocationtrue, 则会在方法执行前就清除缓存。

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:访问方法参数 productid 属性。
  • #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,可以显著提高应用程序的性能,减少数据库负载,提升用户体验。

THE END