Spring Cache 使用教程:配置与注解详解 – wiki基地


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 的基石。它定义了一系列接口和类,如 CacheCacheManager,屏蔽了底层缓存实现的差异。
  • 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 会先根据缓存键到指定的缓存中查找数据。如果找到,则直接返回缓存中的值,方法不再执行;如果没找到,则执行方法,并将方法的返回值存入缓存,然后再返回该值。这是一种“读优先”的策略。

  • 常用属性:

    • valuecacheNames: 必填。指定缓存的名称(一个或多个)。可以使用字符串数组 { "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: 引用方法执行后的返回值 (仅在 unlesscacheResolver/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

  • 作用: 标记一个方法,表示该方法执行后需要从缓存中移除一个或多个条目。主要用于缓存失效或删除场景。

  • 常用属性:

    • valuecacheNames: 必填。指定要操作的缓存名称。
    • key: 可选。指定要移除的缓存条目的键。可以使用 SpEL 表达式。
    • condition: 可选。一个 SpEL 表达式,如果表达式结果为 false,则不执行缓存移除操作。此表达式在方法执行判断。
    • allEntries: 可选。布尔值,默认为 false。如果设置为 true,则清除指定缓存中的所有条目,忽略 key 属性。
    • beforeInvocation: 可选。布尔值,默认为 false
      • false (默认): 在方法成功执行清除缓存。如果方法执行过程中抛出异常,则不清除缓存。
      • true: 在方法执行立即清除缓存。无论方法是否成功执行,都会执行清除操作。这对于确保事务回滚时缓存也被清理非常有用。
  • SpEL 表达式:@Cacheable (#resultcondition 中不可用,因为方法还没执行;在 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

}
``
配置 RedisCacheManager 时,通常需要考虑序列化问题(如何将 Java 对象转换为字节流存储到 Redis,以及如何反序列化)。常用的序列化器有
StringRedisSerializer(用于 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>

配置:

  1. 创建 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>
    


    “`

  2. 配置 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. 条件缓存 (conditionunless)

conditionunless 属性提供了根据 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() + "'");
    // 清空错误通常也可以忽略
}

}
``
Spring 会自动检测并使用应用上下文中注册的
CacheErrorHandler` Bean。

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 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。


发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部