Spring Cache 详解 – wiki基地


Spring Cache 详解:构建高性能应用的利器

在现代企业级应用开发中,性能优化始终是一个核心关注点。面对高并发访问和海量数据处理,如何有效降低数据库压力、缩短响应时间成为了开发者必须解决的难题。缓存(Caching)作为一种经典的优化手段,通过将热点数据存储在内存或其他快速存储介质中,显著提升了数据读取速度,从而极大地改善了应用的整体性能。

Spring Framework 提供了一个强大的缓存抽象层(Spring Cache Abstraction),它为开发者提供了一套统一的 API 和注解,使得在 Spring 应用中集成各种缓存解决方案变得异常简单,而无需关心底层具体的缓存实现细节(如 EhCache、Caffeine、Redis、JCache/JSR-107等)。本文将深入探讨 Spring Cache 的方方面面,帮助读者全面掌握这一提升应用性能的利器。

一、为什么选择 Spring Cache?

直接使用底层的缓存 API(例如 EhCache 的 CacheManager 或 Redis 的 Jedis)来管理缓存数据是可行的,但这样做会带来一些问题:

  1. 代码侵入性强: 业务逻辑代码中会充斥着缓存操作的代码(如 cache.get()cache.put()cache.evict()),使得业务逻辑与缓存逻辑高度耦合,难以维护和测试。
  2. 切换缓存实现困难: 如果需要更换缓存提供者,需要修改大量业务代码,成本高昂。
  3. 缺乏统一的标准: 不同的缓存提供者 API 各异,增加了学习成本。

Spring Cache 抽象层正是为了解决这些问题而诞生的。它提供了一个基于注解或 AOP 的、与具体缓存实现无关的编程模型。开发者只需要在业务方法上标注相应的注解,Spring 就会在运行时自动管理缓存的存取,将开发者从繁琐的缓存操作细节中解放出来。

Spring Cache 的优势总结:

  • 降低耦合: 将缓存逻辑从业务代码中分离,提高了代码的可读性和可维护性。
  • 灵活性高: 可以轻松切换不同的缓存实现,只需修改配置,无需改动业务代码。
  • 开发效率: 基于注解的声明式缓存极大地简化了缓存开发工作。
  • 功能强大: 支持丰富的配置选项,如自定义缓存 key、缓存条件判断、缓存结果过滤等。
  • 与 Spring 生态集成: 无缝集成到 Spring IoC 容器和 AOP 体系中。

二、Spring Cache 的核心概念与工作原理

Spring Cache 抽象层的核心在于其基于 AOP(面向切面编程)的实现机制。当带有 Spring Cache 注解的方法被调用时,Spring AOP 会拦截这个方法调用,并在方法执行之前或之后执行缓存逻辑。

核心组件包括:

  1. Cache Abstraction (缓存抽象): Spring Cache 提供了一套通用的接口,如 CacheManagerCache
    • CacheManager: 缓存管理器,是 Spring Cache 的核心接口,负责管理和获取 Cache 实例。一个应用可以配置一个或多个 CacheManager Bean。
    • Cache: 缓存实例,代表一个具体的缓存区域或命名空间,例如一个名为 “users” 的缓存。它提供了用于存取数据的基本方法,如 get(key)put(key, value)evict(key)clear() 等。
  2. Cache Annotations (缓存注解): Spring 提供了多个核心注解,用于在方法上声明缓存行为。
    • @Cacheable: 用于标注在方法上,表示方法的返回值可以被缓存。在方法执行前,先根据缓存 key 查找缓存。如果找到,则直接返回缓存的值,不再执行方法体;如果没有找到,则执行方法体,并将方法的返回值存入缓存。
    • @CachePut: 用于标注在方法上,表示方法的返回值需要更新到缓存中。与 @Cacheable 不同,它 总会 执行方法体,然后将方法的返回值按指定的 key 放入缓存。常用于更新操作。
    • @CacheEvict: 用于标注在方法上,表示需要从缓存中移除数据。在方法执行之前或之后,根据指定的 key 或条件从缓存中移除一个或多个条目。常用于删除或更新操作导致缓存失效的场景。
    • @Caching: 用于分组多个 Spring Cache 注解。当一个方法需要同时应用多个不同类型的缓存操作时(例如,更新一个对象并同时清除一个列表缓存),可以使用 @Caching 将它们组合起来。
  3. AOP Interceptor (AOP 拦截器): Spring Cache 的核心实现是基于 AOP 拦截器。当 Spring 容器检测到 @EnableCaching 注解并扫描到带有缓存注解的方法时,它会自动创建一个 AOP 代理。当调用这些被代理的方法时,拦截器会介入,执行缓存的存取、更新或淘汰逻辑。
  4. KeyGenerator (Key 生成器): 用于根据方法的参数、目标对象等信息生成缓存的 key。Spring 提供了默认的 KeyGenerator,也可以自定义。
  5. CacheErrorHandler (缓存错误处理器): 用于处理缓存操作过程中发生的异常(如网络问题、序列化失败等)。默认情况下,异常会被简单地记录日志,并继续执行方法体(对于 @Cacheable),或者忽略缓存操作。可以自定义错误处理器来改变这种行为。

工作流程简述 (以 @Cacheable 为例):

  1. 方法被调用。
  2. AOP 拦截器捕获方法调用。
  3. 拦截器根据方法签名、参数、@Cacheable 注解的配置(如 key 表达式、keyGenerator)生成缓存 key。
  4. 拦截器根据 @Cacheable 注解指定的缓存名称 (value 属性) 获取对应的 Cache 实例(通过 CacheManager)。
  5. 拦截器使用生成的 key 从 Cache 实例中查找缓存数据。
  6. 如果找到缓存数据:
    • 如果缓存数据有效,直接将缓存的值作为方法的返回值返回。方法体 不会 执行。
  7. 如果未找到缓存数据 或 缓存数据已失效:
    • 执行原始方法体。
    • 获取方法的返回值。
    • 根据 @Cacheable 注解的配置(如 unless 表达式)判断是否需要缓存。
    • 如果需要缓存,拦截器使用生成的 key 和方法的返回值,通过 Cache 实例将数据存入缓存。
    • 将方法的返回值返回。

三、启用 Spring Cache 与基本配置

在 Spring Boot 应用中启用 Spring Cache 非常简单,只需在主应用类或配置类上添加 @EnableCaching 注解即可。

“`java
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;

@SpringBootApplication
@EnableCaching // 启用 Spring Cache
public class YourApplication {

public static void main(String[] args) {
    SpringApplication.run(YourApplication.class, args);
}

}
“`

在添加 @EnableCaching 后,Spring 会自动寻找一个 CacheManager Bean。如果找不到,并且没有配置特定的缓存提供者,Spring Boot 会自动配置一个简单的基于内存的 ConcurrentMapCacheManager

配置 CacheManager:

为了使用更专业的缓存解决方案,你需要配置一个对应的 CacheManager Bean。以下是几种常见 CacheManager 的配置示例:

  1. SimpleCacheManager (简单内存缓存): 主要用于开发和测试。
    “`java
    import org.springframework.cache.CacheManager;
    import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;

    @Configuration
    @EnableCaching
    public class CacheConfig {

    @Bean
    public CacheManager cacheManager() {
        // 创建一个简单的ConcurrentMapCacheManager
        // 可以指定默认的缓存名称,或在这里手动添加多个ConcurrentMapCache
        return new ConcurrentMapCacheManager("users", "products");
    }
    

    }
    ``ConcurrentMapCacheManager默认会根据Cache名称按需创建ConcurrentMapCache` 实例。

  2. RedisCacheManager (分布式缓存): 常用于生产环境,需要引入 Spring Data Redis 依赖。
    首先添加依赖(Maven):
    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 抽象 -->
    </dependency>

    然后配置 RedisCacheManager Bean。Spring Boot 提供了自动配置,但在某些情况下你可能需要自定义配置,例如序列化方式或缓存过期时间。
    “`java
    import org.springframework.cache.CacheManager;
    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.connection.RedisConnectionFactory;
    import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
    import org.springframework.data.redis.serializer.RedisSerializationContext.SerializationPair;
    import org.springframework.data.redis.serializer.StringRedisSerializer;

    import java.time.Duration;

    @Configuration
    @EnableCaching
    public class CacheConfig {

    @Bean
    public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                // 配置key序列化器
                .serializeKeysWith(SerializationPair.fromSerializer(new StringRedisSerializer()))
                // 配置value序列化器 (推荐使用JSON)
                .serializeValuesWith(SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
                // 设置默认缓存过期时间 (例如:30分钟)
                .entryTtl(Duration.ofMinutes(30))
                // 禁用空值缓存 (如果不需要缓存null)
                .disableCachingNullValues();
    
        return RedisCacheManager.builder(connectionFactory)
                .cacheDefaults(config)
                // 可以为特定的缓存名称配置不同的过期时间等
                // .withCacheConfiguration("users", config.entryTtl(Duration.ofHours(1)))
                .transactionAware() // 可选,使缓存操作感知Spring事务
                .build();
    }
    

    }
    “`
    注意:使用 Redis 等分布式缓存时,序列化 是一个重要的考虑因素。需要确保存入缓存的对象能够被正确地序列化和反序列化。

  3. CaffeineCacheManager: 高性能的本地内存缓存,是 Guava 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 java.util.concurrent.TimeUnit;

    @Configuration
    @EnableCaching
    public class CacheConfig {

    @Bean
    public CacheManager cacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager();
        // 配置 Caffeine 缓存策略,例如设置最大容量、过期时间
        cacheManager.setCaffeine(Caffeine.newBuilder()
                .initialCapacity(100) // 初始容量
                .maximumSize(1000) // 最大缓存条目数
                .expireAfterWrite(10, TimeUnit.MINUTES)); // 写入后10分钟过期
        // 可以指定允许创建的缓存名称
        // cacheManager.setCacheNames(Arrays.asList("users", "products"));
        return cacheManager;
    }
    

    }
    ``
    Spring Boot 对 Caffeine 有很好的自动配置支持,通常你只需要添加依赖并在
    application.propertiesapplication.yml中配置即可,无需手动创建CaffeineCacheManager` Bean。

四、核心缓存注解详解与示例

接下来详细介绍 Spring Cache 的核心注解及其常用属性。

4.1 @Cacheable

用于方法级别的读缓存。

常用属性:

  • value (或 cacheNames): 必须指定,一个字符串数组,表示方法返回值要存入的缓存名称。例如 {"users", "people"} 表示同时存入 “users” 和 “people” 两个缓存。
  • key: 可选,用于指定生成缓存 key 的 SpEL (Spring Expression Language) 表达式。默认的 key 生成策略会使用方法的所有参数哈希值作为 key。
  • condition: 可选,一个 SpEL 表达式,只有当表达式计算结果为 true 时,方法才会被缓存。常用于根据方法参数或返回值动态决定是否缓存。
  • unless: 可选,一个 SpEL 表达式,只有当表达式计算结果为 false 时,方法返回值才会被缓存。常用于排除某些不适合缓存的结果,例如 null 或空集合。
  • sync: 可选,布尔值,默认为 false。如果设置为 true,则在多个线程同时请求同一个 key 时,只有一个线程会执行方法体并写入缓存,其他线程会阻塞等待结果。这有助于解决缓存击穿(”thundering herd”)问题。仅适用于 @Cacheable
  • cacheManager: 可选,指定使用的 CacheManager Bean 名称。
  • keyGenerator: 可选,指定使用的 KeyGenerator Bean 名称。

示例:

“`java
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class UserService {

// 简单的根据ID获取用户,缓存到名为"users"的缓存中
@Cacheable(value = "users")
public User getUserById(Long userId) {
    System.out.println("Executing getUserById for userId: " + userId + " - Not from cache");
    // 模拟数据库查询延迟
    try { Thread.sleep(500); } catch (InterruptedException e) {}
    return new User(userId, "User " + userId);
}

// 使用SpEL指定key,根据用户名和年龄生成key
@Cacheable(value = "users", key = "#username + ':' + #age")
public User getUserByUsernameAndAge(String username, int age) {
    System.out.println("Executing getUserByUsernameAndAge for username: " + username + ", age: " + age + " - Not from cache");
    // 模拟数据库查询
    return new User(null, username); // 简化示例
}

// 使用condition,只缓存ID小于100的用户
@Cacheable(value = "users", condition = "#userId < 100")
public User getUserIfIdLessThan100(Long userId) {
    System.out.println("Executing getUserIfIdLessThan100 for userId: " + userId + " - Not from cache");
    return new User(userId, "User " + userId);
}

// 使用unless,不缓存返回值为null的用户
@Cacheable(value = "users", unless = "#result == null")
public User getUserAllowNull(Long userId) {
    System.out.println("Executing getUserAllowNull for userId: " + userId + " - Not from cache");
     if (userId % 2 == 0) {
         return new User(userId, "User " + userId);
     } else {
         return null; // 模拟有时返回null
     }
}

// 结合key和unless,只缓存非空列表且列表不为空的情况
@Cacheable(value = "products", key = "'allProducts'")
public List<Product> getAllProducts() {
    System.out.println("Executing getAllProducts - Not from cache");
    // 模拟数据库查询
    // return null; // 如果返回null,不会缓存 (除非unelss条件允许)
    // return Collections.emptyList(); // 如果返回空列表,可以通过unless过滤
    return List.of(new Product(1L, "Product A"), new Product(2L, "Product B"));
}

}

// 示例用的简单类
class User {
private Long id;
private String name;
// … getters/setters
public User(Long id, String name) { this.id = id; this.name = name; }
public Long getId() { return id; }
public String getName() { return name; }
@Override public String toString() { return “User{” + “id=” + id + “, name='” + name + ‘\” + ‘}’; }
}
class Product {
private Long id;
private String name;
public Product(Long id, String name) { this.id = id; this.name = name; }
public Long getId() { return id; }
public String getName() { return name; }
@Override public String toString() { return “Product{” + “id=” + id + “, name='” + name + ‘\” + ‘}’; }
}
“`

SpEL 表达式中的可用对象:

在 SpEL 表达式中,可以使用 # 开头的变量来访问方法相关的信息:

  • #root: 根对象,包含方法信息、目标对象等。
    • #root.method: 当前被调用的方法。
    • #root.target: 目标对象(Service 实例)。
    • #root.args: 方法参数数组。
  • #result: 方法的返回值(仅在 unless 属性和 @CachePut / @CacheEvictcondition/unless 属性中可用)。
  • #p<index>#a<index>: 方法的参数,按索引访问 (例如 #p0#a0 表示第一个参数)。
  • #p<name>: 方法的参数,按名称访问 (需要使用 -parameters 编译选项,或者在接口/抽象类中声明参数名称)。

4.2 @CachePut

用于方法级别的写缓存。它 总会 执行被标注的方法,然后将方法的返回值放入缓存中。

常用属性:

@Cacheable 类似,包括 value (或 cacheNames)、keyconditionunlesscacheManagerkeyGenerator

关键区别:

  • @Cacheable执行前 查缓存,如果命中则 不执行 方法。
  • @CachePut总是 执行方法,然后 执行后 更新缓存。

应用场景:

  • 数据更新操作:例如更新用户信息后,确保缓存中的该用户信息是最新的。
  • 创建新数据并放入缓存:例如创建新用户后,直接将新用户对象放入缓存。

示例:

“`java
import org.springframework.cache.annotation.CachePut;
import org.springframework.stereotype.Service;

@Service
public class UserService {

// ... (getUserById 等方法)

// 更新用户,并更新"users"缓存中对应ID的条目
@CachePut(value = "users", key = "#user.id") // key使用更新后的用户的ID
public User updateUser(User user) {
    System.out.println("Executing updateUser for userId: " + user.getId() + " - Always executes");
    // 模拟更新数据库操作
    user.setName(user.getName() + "_updated"); // 修改用户属性作为示例
    return user; // 返回更新后的用户对象,存入缓存
}

 // 创建用户,并将新用户对象放入"users"缓存
 // 假设createNewUser返回一个包含新ID的用户对象
 @CachePut(value = "users", key = "#result.id") // key使用返回值(新用户)的ID
 public User createNewUser(String name) {
     System.out.println("Executing createNewUser for name: " + name + " - Always executes");
     // 模拟创建数据库记录并获取生成的ID
     Long newId = System.currentTimeMillis(); // 模拟生成ID
     User newUser = new User(newId, name);
     return newUser; // 返回新用户,存入缓存
 }

}
``
updateUser示例中,key = “#user.id”是为了确保更新的是缓存中 ID 与入参user` 相同的条目。

4.3 @CacheEvict

用于方法级别的缓存淘汰/移除。

常用属性:

  • value (或 cacheNames): 必须指定,一个字符串数组,表示要从中移除数据的缓存名称。
  • key: 可选,一个 SpEL 表达式,用于指定要移除的缓存 key。
  • condition: 可选,一个 SpEL 表达式,只有当表达式计算结果为 true 时,缓存才会被移除。
  • beforeInvocation: 可选,布尔值,默认为 false。如果设置为 true,则在方法 执行前 移除缓存;如果设置为 false,则在方法 执行后 移除缓存。
  • allEntries: 可选,布尔值,默认为 false。如果设置为 true,则清除指定缓存中的所有条目,忽略 key 属性。
  • cacheManager: 可选,指定使用的 CacheManager Bean 名称。
  • keyGenerator: 可选,指定使用的 KeyGenerator Bean 名称。

应用场景:

  • 删除数据:删除某个对象后,清除缓存中对应的条目。
  • 批量更新/操作:一次操作可能影响多个缓存条目,或者需要清除整个缓存区域。

示例:

“`java
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable; // 引入Cacheable用于验证

import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class UserService {

// ... (getUserById 等方法, getAllProducts 方法)

// 根据ID删除用户,并移除"users"缓存中对应ID的条目
@CacheEvict(value = "users", key = "#userId")
public void deleteUser(Long userId) {
    System.out.println("Executing deleteUser for userId: " + userId + " - Evicting from cache");
    // 模拟数据库删除操作
}

// 清除所有用户缓存
@CacheEvict(value = "users", allEntries = true)
public void deleteAllUsers() {
    System.out.println("Executing deleteAllUsers - Evicting all entries from 'users' cache");
    // 模拟数据库删除所有用户
}

// 清除所有产品缓存,在方法执行前清除
@CacheEvict(value = "products", allEntries = true, beforeInvocation = true)
public void refreshAllProductsCache() {
    System.out.println("Executing refreshAllProductsCache - Evicting 'products' cache BEFORE execution");
    // 可以在这里重新加载数据并放入缓存,例如调用一个被@Cacheable修饰的方法
    // 或者让其他地方触发加载
     // 示例:假设这里只是一个触发器,清除缓存后由后续调用重新加载
}

 // 使用 condition,只在用户ID大于100时移除缓存
@CacheEvict(value = "users", key = "#userId", condition = "#userId > 100")
public void deleteUserConditional(Long userId) {
    System.out.println("Executing deleteUserConditional for userId: " + userId);
    // 模拟数据库删除操作
}

}
``beforeInvocation = true在某些场景下很有用,例如在一个事务方法中使用@CacheEvict。如果在方法执行后清除缓存 (beforeInvocation = false),而方法执行过程中发生异常导致事务回滚,但缓存已经被清除了,可能导致数据与缓存不一致。将beforeInvocation设置为true` 可以避免这种情况,但如果在方法执行前清除缓存,而方法因其他原因未能成功执行,缓存也会被错误地清除。选择哪种方式取决于具体的业务需求和风险权衡。

4.4 @Caching

用于在一个方法上组合多个 Spring Cache 注解。

常用属性:

  • cacheable: 一个 @Cacheable 数组。
  • put: 一个 @CachePut 数组。
  • evict: 一个 @CacheEvict 数组。

示例:

假设有一个操作既要更新某个对象(需要 @CachePut),又要清除另一个相关的列表缓存(需要 @CacheEvict)。

“`java
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Caching;
import org.springframework.stereotype.Service;

@Service
public class ProductService {

// ... (getProductById, getAllProducts)

// 更新产品信息,同时更新该产品的缓存,并清除所有产品列表缓存
@Caching(
    put = {
        @CachePut(value = "products", key = "#product.id") // 更新单个产品缓存
    },
    evict = {
        @CacheEvict(value = "allProducts", allEntries = true) // 清除产品列表缓存
    }
)
public Product updateProduct(Product product) {
    System.out.println("Executing updateProduct for id: " + product.getId());
    // 模拟数据库更新操作
    product.setName(product.getName() + "_updated");
    return product; // 返回更新后的产品对象
}

// 注意:allProducts 缓存和 products 缓存通常是不同的缓存名称,或者至少 key 不同
// @Cacheable(value = "allProducts", key = "'all'") // 假设 getAllProducts 方法是这样缓存的
// public List<Product> getAllProducts() { ... }

}
“`

五、高级特性

5.1 自定义 KeyGenerator

默认的 KeyGenerator 对于简单的参数组合通常是够用的。但如果需要更复杂的 key 生成逻辑,例如只使用部分参数、参数对象中的特定属性,或者结合 Spring Security 的用户信息等,可以实现 org.springframework.cache.interceptor.KeyGenerator 接口,并将其注册为一个 Bean。

“`java
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.cache.interceptor.SimpleKeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.lang.reflect.Method;

@Configuration
public class CustomKeyGeneratorConfig {

@Bean("customUserKeyGenerator") // 指定Bean名称,用于@Cacheable等的keyGenerator属性
public KeyGenerator customUserKeyGenerator() {
    return new KeyGenerator() {
        @Override
        public Object generate(Object target, Method method, Object... params) {
            // 假设方法签名是 getUserByIdAndTenantId(Long userId, String tenantId)
            // 我们想生成 key 为 "user:userId:tenantId"
            if (method.getName().equals("getUserByIdAndTenantId") && params.length == 2) {
                return "user:" + params[0] + ":" + params[1];
            }
            // 对于其他方法,回退到默认的 KeyGenerator
            return SimpleKeyGenerator.generateKey(params);
        }
    };
}

}
``
然后在
@Cacheable等注解中使用keyGenerator` 属性引用这个 Bean:

“`java
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

@Service
public class UserService {

// 使用自定义KeyGenerator
@Cacheable(value = "users", keyGenerator = "customUserKeyGenerator")
public User getUserByIdAndTenantId(Long userId, String tenantId) {
    System.out.println("Executing getUserByIdAndTenantId for userId: " + userId + ", tenantId: " + tenantId + " - Not from cache");
    // 模拟查询
    return new User(userId, "User " + userId + " under tenant " + tenantId);
}

// ... 其他方法继续使用默认KeyGenerator或通过key属性指定SpEL

}
“`

5.2 CacheErrorHandler

在分布式缓存环境下,缓存操作可能会因为网络、服务器故障等原因失败。默认情况下,Spring Cache 的异常处理器 SimpleCacheErrorHandler 会捕获异常并记录日志,但不会重新抛出异常。对于 @Cacheable,这意味着即使缓存查找失败,方法体仍然会执行。对于 @CachePut@CacheEvict,这意味着缓存更新或删除可能会默默失败。

如果你需要更复杂的错误处理逻辑(例如,在缓存读取失败时抛出自定义异常,或者在缓存写入失败时执行回退逻辑),可以实现 org.springframework.cache.interceptor.CacheErrorHandler 接口,并将其注册为一个 Bean。

“`java
import org.springframework.cache.Cache;
import org.springframework.cache.interceptor.CacheErrorHandler;
import org.springframework.cache.interceptor.CacheOperationInvocationContext;
import org.springframework.stereotype.Component;

@Component
public class CustomCacheErrorHandler implements CacheErrorHandler {

@Override
public void handleCacheGetError(RuntimeException exception, Cache cache, Object key) {
    System.err.println("Cache get error! Cache: " + cache.getName() + ", Key: " + key + ", Error: " + exception.getMessage());
    // 可以在这里记录日志,或者抛出自定义异常
    // throw new CustomCacheException("Failed to get data from cache", exception);
     // 默认行为是忽略,这里也选择忽略让方法继续执行
}

@Override
public void handleCachePutError(RuntimeException exception, Cache cache, Object key, Object value) {
     System.err.println("Cache put error! Cache: " + cache.getName() + ", Key: " + key + ", Value: " + value + ", Error: " + exception.getMessage());
     // 默认行为是忽略
}

@Override
public void handleCacheEvictError(RuntimeException exception, Cache cache, Object key) {
     System.err.println("Cache evict error! Cache: " + cache.getName() + ", Key: " + key + ", Error: " + exception.getMessage());
     // 默认行为是忽略
}

@Override
public void handleCacheClearError(RuntimeException exception, Cache cache) {
     System.err.println("Cache clear error! Cache: " + cache.getName() + ", Error: " + exception.getMessage());
     // 默认行为是忽略
}

}
``
Spring 会自动检测并使用这个
CacheErrorHandler` Bean。

5.3 缓存与事务的交互

在一个同时使用 @Transactional 和 Spring Cache 注解的方法中,它们的执行顺序和相互影响是一个容易产生问题的地方。

  • @Cacheable@Transactional: 如果 @Cacheable 在事务方法内部,并且缓存命中,方法体(包括事务代码)将不会执行。如果缓存未命中,方法体执行,事务会生效。潜在的问题是,如果方法执行成功,数据写入数据库,并成功写入缓存;但如果方法执行过程中或结束后发生异常导致事务回滚,数据库的数据并未提交,而缓存却已经写入了成功的数据,造成数据不一致。

    • 解决方案: 通常建议将缓存放在事务的 外部。即,调用一个非事务方法,该方法负责先查缓存;如果缓存未命中,再调用内部的事务方法去查询数据库。或者使用 beforeInvocation = true@CacheEvict 在事务开始前清除缓存(但如前所述有其自身的风险)。Spring Cache 的事务感知模式 (RedisCacheManager.transactionAware()) 可以让缓存操作在事务提交后才执行,但这需要底层缓存提供者和 CacheManager 的支持。
  • @CachePut@Transactional: 类似地,如果 @CachePut 方法在事务内部,方法体总是执行,然后缓存更新。如果事务回滚,数据库更新被撤销,但缓存可能已经被更新,再次造成不一致。

    • 解决方案: 同样建议将 @CachePut 放在事务外部,或者使用事务感知模式。
  • @CacheEvict@Transactional: 如果 @CacheEvict 方法在事务内部:

    • beforeInvocation = false (默认): 在方法执行后、事务提交前(取决于具体实现和顺序)尝试清除缓存。如果事务回滚,方法执行被撤销,但缓存清除可能已经发生(或未发生),取决于清除操作是否受事务控制以及异常发生时机。
    • beforeInvocation = true: 在方法执行前、事务开始前清除缓存。如果方法执行失败或事务回滚,缓存已被清除,可能导致不一致。
    • 解决方案: 最安全的方式是让 @CacheEvict 在事务 提交后 才执行。这可以通过事务感知模式实现,或者将 @CacheEvict 放在一个独立的、在事务提交后才调用的方法中。

总结:在使用 Spring Cache 和 @Transactional 时,需要仔细考虑缓存操作和事务提交/回滚的顺序,避免数据不一致问题。事务感知缓存(Transaction-Aware Caches)是 Spring 提供的解决方案之一,它确保缓存操作(特别是写操作,如 put 和 evict)仅在相应的 Spring 事务成功提交后才实际执行。

六、集成特定的缓存提供者

Spring Cache 抽象层本身不提供实际的缓存存储,需要依赖于具体的缓存库。集成特定的缓存提供者通常只需要两步:

  1. 添加相应的依赖。
  2. 配置对应的 CacheManager Bean。

Spring Boot 的自动配置简化了这一过程,对于常见的提供者(如 Redis、Caffeine),只需添加依赖并在配置文件中进行少量设置,Spring Boot 就会自动配置好相应的 CacheManager

例如,使用 Redis:
* 添加 spring-boot-starter-data-redisspring-boot-starter-cache 依赖。
* 在 application.propertiesapplication.yml 中配置 Redis 连接信息(host, port, password, etc.)。
yaml
spring:
redis:
host: localhost
port: 6379
cache:
type: redis # 明确指定缓存类型,虽然有 data-redis 依赖通常会自动选择

* 如果需要自定义序列化、TTL等,才需要手动配置 RedisCacheConfigurationRedisCacheManager Bean,如前面示例所示。

集成其他提供者(如 EhCache, JCache/JSR-107 based caches like Hazelcast, Infinispan)的模式是类似的:添加依赖,配置对应的 CacheManager。具体配置方式请参考 Spring Boot 官方文档或对应缓存提供者的 Spring 集成文档。

七、实际应用中的最佳实践与注意事项

  1. 选择合适的缓存策略 (@Cacheable vs @CachePut vs @CacheEvict):

    • 读取热点数据,方法结果稳定不变或不常变动:@Cacheable
    • 更新或创建数据后,需要确保缓存最新:@CachePut
    • 删除数据或发生某些事件导致缓存失效:@CacheEvict
    • 组合多种操作:@Caching
  2. 设计有效的 Key:

    • Key 必须能够唯一标识缓存中的数据。
    • Key 应该是稳定不变的(对于同一个逻辑数据),不应依赖于方法调用时的上下文(除非是业务需要)。
    • Key 对象需要实现 equals()hashCode() 方法,以便缓存系统正确比较和查找。对于分布式缓存,Key 和 Value 对象还需要是可序列化的。
    • 避免使用复杂对象作为 Key,尤其是分布式缓存,序列化成本高且可能出现兼容性问题。使用简单类型或由简单类型组成的字符串作为 Key 通常更安全高效。
  3. 理解缓存一致性问题: 缓存是数据库或其他数据源的副本。数据的更新/删除操作可能导致缓存中的数据与源数据不一致(缓存过期、缓存失效但未及时清除等)。

    • 对于强一致性要求高的场景,可能需要更复杂的缓存策略(如读写穿透、失效策略、双写一致性方案等),或者考虑放弃缓存或降低缓存粒度。
    • Spring Cache 主要提供的是最终一致性(Eventually Consistency)。通过 @CacheEvict 等手段手动管理失效是常见的做法。
  4. 处理 Null 值: 某些缓存实现(如 Caffeine、JSR-107)默认不允许缓存 null 值。Redis 通常允许缓存 null (以特定的方式存储)。Spring Cache 默认是支持缓存 null 值的,但可以通过配置 CacheManager 来禁用(例如 RedisCacheConfiguration.disableCachingNullValues())。根据业务需求决定是否缓存 null 值。

  5. 缓存粒度: 决定缓存是方法级别、对象级别还是更细粒度。Spring Cache 主要工作在方法级别。缓存整个方法返回值通常更简单,但如果返回值是一个大对象且只有部分属性经常访问,可能需要考虑更细粒度的缓存或手动缓存部分数据。

  6. 序列化 (分布式缓存): 使用 Redis 等分布式缓存时,缓存的 Key 和 Value 都需要在网络上传输和存储,因此必须是可序列化的。Java 自带的序列化 (java.io.Serializable) 效率和兼容性较差,通常推荐使用 JSON (Jackson, Gson)、Kryo 或 Protocol Buffers 等。确保配置的序列化器与你的数据类型兼容。

  7. 监控缓存性能: 关注缓存的命中率、存取延迟、缓存大小等指标,以便及时发现和解决性能问题。许多缓存提供者都提供了监控工具或暴露 JMX Metrics。

  8. 谨慎使用 allEntries = true: 清除整个缓存区域是一个代价较高的操作,可能会导致短时间内大量请求穿透到后端数据源,造成雪崩效应。只有在确实需要全局失效的场景下谨慎使用。

八、总结

Spring Cache 抽象层提供了一种声明式的、与底层实现无关的缓存使用方式,极大地简化了 Java 应用中缓存的开发和维护。通过 @Cacheable@CachePut@CacheEvict 等注解,开发者可以轻松地在方法级别应用缓存策略。配合灵活的 KeyGenerator、CacheErrorHandler 以及 SpEL 表达式,可以实现各种复杂的缓存需求。

虽然 Spring Cache 提供了便利,但在实际应用中,理解缓存的工作原理、底层提供者的特性、以及缓存一致性、事务交互等问题依然至关重要。合理地设计缓存策略和 Key,选择合适的缓存提供者,并进行必要的监控,才能真正发挥缓存的作用,构建高性能、可扩展的应用系统。

希望本文的详细介绍能帮助你更好地理解和使用 Spring Cache,为你的应用带来显著的性能提升!


发表评论

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

滚动至顶部