Spring Cache

Spring Cache

一、Spring Cache 是什么?

简单来说,Spring Cache 是 Spring 框架提供的一套缓存抽象层(不是具体的缓存实现)

你可以把它理解成一个“缓存操作的统一接口”。它本身并不提供缓存存储的功能,而是定义了一组标准的注解和API,允许你以声明式的方式轻松地将缓存功能添加到你的应用程序中。至于底层到底用哪种缓存技术(比如 Redis、Ehcache、Caffeine 等),你可以自由选择和切换,而无需修改你的核心业务代码。

核心思想: 通过注解,把方法调用的结果缓存起来。当下次用相同的参数调用该方法时,直接返回缓存中的结果,而不再真正执行方法体。

二、为什么要用 Spring Cache?它有什么用?

在业务开发中,我们经常会遇到一些“热点数据”:

  • 不经常修改的配置信息。
  • 首页的商品分类列表。
  • 用户的基本信息。

如果每次有请求来,都去数据库查询这些数据,会给数据库造成巨大的压力,并且响应速度也会变慢。

Spring Cache 的作用就是解决这个问题,它的主要优点如下:

  1. 提升性能: 将频繁读取但很少变更的数据放入缓存,极大减少数据库访问,加快应用响应速度。
  2. 减少负载: 降低了数据库的并发访问压力。
  3. 声明式缓存: 通过简单的注解(如 @Cacheable)即可实现缓存逻辑,与业务代码解耦,代码侵入性低,非常优雅。
  4. 易于扩展: 可以轻松切换不同的缓存提供商,适应不同的业务规模(从单机应用到分布式集群)。

三、核心概念与使用方法

1. 核心注解

Spring Cache 主要提供了以下几个注解:

注解 作用 说明
@EnableCaching 开启缓存支持 必须放在配置类(如 @SpringBootApplication 主类)上,表示启用缓存功能。
@Cacheable 在方法执行前查询缓存 核心注解。如果缓存中存在,则直接返回缓存结果;否则执行方法,并将结果存入缓存。
@CacheEvict 清除缓存 通常用在更新或删除方法上,当数据变更后,清除指定的缓存。
@CachePut 更新缓存 无论缓存是否存在,都会执行方法,并将方法返回值更新/放入缓存。
@Caching 组合多个缓存操作 用于在一个方法上同时使用多个 @Cacheable@CacheEvict@CachePut
@CacheConfig 类级别的共享缓存配置 在类级别统一配置缓存的名称等公共属性,避免在每个方法上重复写。

2. 缓存管理器 CacheManager

这是 Spring Cache 抽象的核心接口。它负责创建、管理和配置具体的 Cache 实例。你需要为项目配置一个 CacheManager 的实现。

  • Spring Boot 会自动配置: 只要你引入了相关的缓存 Starter(如 spring-boot-starter-data-redis),Spring Boot 会自动为你配置好对应的 CacheManager
  • 常用实现:
    • SimpleCacheManager: 使用简单的 Collection 来存储缓存,主要用于测试。
    • ConcurrentMapCacheManager: 使用 JDK 的 ConcurrentMap 实现,默认的内存缓存。
    • EhCacheCacheManager: 使用 EhCache 作为缓存实现。
    • RedisCacheManager: 使用 Redis 作为缓存实现(分布式缓存)。

四、详细代码示例(基于 Spring Boot)

让我们通过一个完整的例子来学习。假设我们有一个用户服务 UserService

步骤 1:添加依赖和启用缓存

首先,在 pom.xml 中,我们使用默认的内存缓存(ConcurrentMap):

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

然后,在主应用类上使用 @EnableCaching

@SpringBootApplication
@EnableCaching // 关键!开启缓存功能
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

步骤 2:编写业务服务,使用缓存注解

@Service
@CacheConfig(cacheNames = "users") // 类级别配置,这个类里所有方法的缓存都属于 "users" 这个缓存区
public class UserService {

    // 模拟数据库
    private Map<Long, User> userDB = new HashMap<>();

    {
        // 初始化一些测试数据
        userDB.put(1L, new User(1L, "Alice"));
        userDB.put(2L, new User(2L, "Bob"));
    }

    /**
     * 根据ID查询用户
     * @Cacheable 表示会先检查缓存
     * key: 缓存键,使用SpEL表达式。这里表示使用方法参数id作为键。
     * unless: 条件,如果方法返回null,则不缓存结果。
     */
    @Cacheable(key = "#id", unless = "#result == null")
    public User getUserById(Long id) {
        // 模拟数据库查询耗时
        System.out.println("===> 查询数据库,id: " + id);
        return userDB.get(id);
    }

    /**
     * 更新用户信息
     * @CacheEvict 表示方法执行成功后,清除指定的缓存。
     * key: 指定要清除的缓存键,即要更新的用户ID对应的缓存。
     * allEntries: 如果为true,会清除整个"users"缓存区的所有条目。这里我们选择精确清除。
     * beforeInvocation: 如果为true,在方法执行前就清除缓存。适用于避免并发场景下的脏数据,但需谨慎。
     */
    @CacheEvict(key = "#user.id")
    public void updateUser(User user) {
        System.out.println("===> 更新用户,id: " + user.getId());
        userDB.put(user.getId(), user);
        // 更新数据库后,缓存中对应id的数据已经过时,所以需要清除。
    }

    /**
     * 新增用户
     * 新增后,通常不需要立即缓存,因为还没有查询请求。
     * 但可以考虑清除一些列表缓存(这里不演示),或者使用@CachePut立即缓存。
     * 这里我们选择不进行缓存操作。
     */
    public void addUser(User user) {
        System.out.println("===> 新增用户,id: " + user.getId());
        userDB.put(user.getId(), user);
    }

    /**
     * 删除用户
     * @CacheEvict 删除成功后,清除该用户的缓存。
     */
    @CacheEvict(key = "#id")
    public void deleteUserById(Long id) {
        System.out.println("===> 删除用户,id: " + id);
        userDB.remove(id);
    }
}

步骤 3:编写测试类验证

@SpringBootTest
class UserServiceTest {

    @Autowired
    private UserService userService;

    @Test
    void testCache() {
        System.out.println("=== 第一次查询id=1的用户 ===");
        User user1 = userService.getUserById(1L); // 会打印 "查询数据库"
        System.out.println("用户信息: " + user1);

        System.out.println("\n=== 第二次查询id=1的用户 ===");
        User user2 = userService.getUserById(1L); // 不会打印,直接从缓存返回
        System.out.println("用户信息: " + user2);

        System.out.println("\n=== 更新id=1的用户 ===");
        user1.setName("Alice Updated");
        userService.updateUser(user1); // 会清除缓存中key=1的数据

        System.out.println("\n=== 第三次查询id=1的用户(缓存已被清除,会再次查询数据库) ===");
        User user3 = userService.getUserById(1L); // 会再次打印 "查询数据库"
        System.out.println("用户信息: " + user3);

        System.out.println("\n=== 查询一个不存在的用户id=999 ===");
        User user999 = userService.getUserById(999L); // 会查询数据库,但结果为null,因为unless条件,不会缓存null值
        System.out.println("用户信息: " + user999);
    }
}

预期输出:

=== 第一次查询id=1的用户 ===
===> 查询数据库,id: 1
用户信息: User(id=1, name=Alice)

=== 第二次查询id=1的用户 ===
用户信息: User(id=1, name=Alice) // 注意:没有打印"查询数据库"

=== 更新id=1的用户 ===
===> 更新用户,id: 1

=== 第三次查询id=1的用户(缓存已被清除,会再次查询数据库) ===
===> 查询数据库,id: 1
用户信息: User(id=1, name=Alice Updated)

=== 查询一个不存在的用户id=999 ===
===> 查询数据库,id: 999
用户信息: null

通过这个测试,你可以清晰地看到 @Cacheable@CacheEvict 是如何工作的。


五、进阶用法与注意事项

  1. 条件缓存:

    • condition: 在方法执行判断,满足条件才使用缓存。
    • unless: 在方法执行判断,满足条件则不缓存结果(如上面的 unless = "#result == null")。
    // 只缓存id大于2的用户
    @Cacheable(key = "#id", condition = "#id > 2")
    public User getUserCondition(Long id) { ... }
    
  2. 组合键(KeyGenerator):
    默认的键是方法参数。如果参数复杂或多个参数共同决定唯一性,可以自定义键生成器。

    // 使用SpEL构造复杂key
    @Cacheable(key = "T(String).format('user:%s:%s', #id, #name)")
    public User getUserByIdAndName(Long id, String name) { ... }
    
  3. @CachePut 的使用场景:
    当你希望方法总是被执行,并且结果总是被更新到缓存时使用。例如,在查询并更新用户后,希望最新的数据立刻进入缓存。

    @CachePut(key = "#user.id")
    public User saveOrUpdateUser(User user) {
        // ... 保存或更新逻辑
        return user; // 这个返回值会被放入缓存
    }
    
  4. 切换缓存实现(如集成 Redis):
    非常简单!只需要修改 pom.xml,Spring Boot 的自动配置会搞定一切。

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    

    然后在 application.yml 中配置 Redis 连接信息:

    spring:
      redis:
        host: localhost
        port: 6379
    

    这样,你的缓存就会自动存到 Redis 里了,代码完全不用改!

总结

Spring Cache 是一个强大而优雅的缓存抽象工具,它通过声明式注解极大地简化了缓存功能的集成。你只需要关注 “什么时候缓存”(@Cacheable“什么时候清缓存”(@CacheEvict 这两个核心问题,而底层的复杂实现都由 Spring 和对应的缓存提供商来处理。

希望这个详细的讲解对你有帮助!这是入门和日常使用最核心的知识点,多动手写写例子,就能很快掌握。

posted @ 2025-12-14 16:09  binlicoder  阅读(37)  评论(0)    收藏  举报