一篇文章搞定Spring Cache

Spring Cache 提供了一种灵活而强大的缓存抽象,使得在应用中集成缓存变得更加容易。让我们从一些基本概念开始,然后深入讨论 Spring Cache 的用法和配置。

基本概念

  1. 缓存注解: Spring Cache 提供了一组注解,用于在方法级别声明缓存行为。其中包括 @Cacheable@CachePut@CacheEvict 等。这些注解允许你控制方法的缓存行为,如何从缓存中读取、写入和清除数据。
  2. CacheManager: CacheManager 是 Spring Cache 的核心接口,负责创建和管理缓存实例。Spring Cache 提供了默认的 ConcurrentMapCacheManager,同时也支持集成其他缓存提供者,如 Ehcache、Caffeine、Redis 等。
  3. 缓存键(Key): 缓存中的每个条目都有一个唯一的键,用于在缓存中标识数据。在 Spring Cache 中,键可以使用 SpEL 表达式构建,以根据方法参数、返回值等动态生成。

基本使用示例

让我们通过一个简单的示例来演示 Spring Cache 的基本使用。假设有一个服务类,其中包含一个根据用户ID获取用户信息的方法,我们希望将用户信息缓存起来:

import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    @Cacheable(value = "userCache", key = "#userId")
    public String getUserInfo(String userId) {
        // 模拟从数据库或其他数据源获取用户信息的操作
        System.out.println("Fetching user info for userId: " + userId);
        return "User info for userId: " + userId;
    }
}

在上面的示例中,@Cacheable 注解表示 getUserInfo 方法的结果应该被缓存,缓存的名称为 "userCache",键为方法的参数 userId。如果多次调用 getUserInfo 方法并传递相同的 userId,则只有第一次会执行方法体,后续的调用将直接从缓存中获取结果。

配置缓存管理器

在Spring中,配置常见的缓存管理器通常涉及使用Spring Cache抽象,并选择适当的缓存提供者。以下是一些常见的缓存管理器配置示例,包括Ehcache、Caffeine、Redis等。

1. 使用 Ehcache 作为缓存管理器

首先,确保项目中引入了Ehcache的相关依赖:

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

然后,在Spring Boot的配置文件中添加Ehcache的配置,比如ehcache.xml

<!-- src/main/resources/ehcache.xml -->

<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:noNamespaceSchemaLocation="http://www.ehcache.org/ehcache.xsd">
    <defaultCache
        maxElementsInMemory="100"
        eternal="false"
        timeToIdleSeconds="120"
        timeToLiveSeconds="120"
        overflowToDisk="false"
        />
    <!-- 其他缓存配置 -->
</ehcache>

然后,在Spring Boot的配置类中配置Ehcache的CacheManager

import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.ehcache.EhCacheCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableCaching
public class CacheConfig {

    @Bean
    public CacheManager cacheManager() {
        return new EhCacheCacheManager(ehCacheCacheManager().getObject());
    }

    @Bean
    public EhCacheManagerFactoryBean ehCacheCacheManager() {
        EhCacheManagerFactoryBean factoryBean = new EhCacheManagerFactoryBean();
        factoryBean.setConfigLocation(new ClassPathResource("ehcache.xml"));
        factoryBean.setShared(true);
        return factoryBean;
    }
}

2. 使用 Caffeine 作为缓存管理器

添加以下依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
</dependency>

接下来,在 Spring Boot 项目的配置类中配置 Caffeine Cache 作为缓存管理器:

import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
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();
        cacheManager.setCaffeine(caffeineCacheBuilder());
        return cacheManager;
    }

    private Caffeine<Object, Object> caffeineCacheBuilder() {
        return Caffeine.newBuilder()
                .expireAfterWrite(10, TimeUnit.MINUTES) // 设置缓存失效时间为10分钟
                .maximumSize(100); // 设置缓存的最大条目数
    }
}

在上述配置中,CaffeineCacheManager 被配置为缓存管理器,通过 caffeineCacheBuilder 方法配置了 Caffeine Cache 的一些属性,比如失效时间和最大条目数。现在,你可以在服务类中使用 @Cacheable 注解来声明缓存:

import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

@Service
public class MyService {

    @Cacheable(value = "userCache", key = "#userId")
    public String getUserInfo(String userId) {
        // 模拟从数据库或其他数据源获取用户信息的操作
        System.out.println("Fetching user info for userId: " + userId);
        return "User info for userId: " + userId;
    }
}

3. 使用 Redis 作为缓存管理器

当使用 Redis 作为缓存管理器时,首先确保项目中引入了 Redis 相关的依赖。在 Maven 中,可以添加以下依赖:

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

接下来,在 Spring Boot 项目的配置文件中配置 Redis 连接信息:

# src/main/resources/application.properties

spring.redis.host=localhost
spring.redis.port=6379

然后,在 Spring Boot 项目的配置类中配置 Redis Cache 作为缓存管理器:

import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;

@Configuration
@EnableCaching
public class CacheConfig {

    @Bean
    public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
        return RedisCacheManager.builder(redisConnectionFactory)
                .build();
    }
}

在这个配置中,RedisCacheManager 被配置为缓存管理器,并使用了 RedisConnectionFactory 连接到 Redis 服务器。现在,你可以在服务类中使用 @Cacheable 注解来声明缓存:

import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

@Service
public class MyService {

    @Cacheable(value = "userCache", key = "#userId")
    public String getUserInfo(String userId) {
        // 模拟从数据库或其他数据源获取用户信息的操作
        System.out.println("Fetching user info for userId: " + userId);
        return "User info for userId: " + userId;
    }
}

4.使用Guava Cache 作为缓存容器时

添加以下依赖:

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>30.1-jre</version> <!-- 请根据最新版本调整 -->
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

然后,可以在 Spring Boot 项目的配置类中配置 Guava Cache 作为缓存管理器:

import com.google.common.cache.CacheBuilder;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.guava.GuavaCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

@Configuration
public class CacheConfig {

    @Bean
    public CacheManager cacheManager() {
        GuavaCacheManager cacheManager = new GuavaCacheManager();
        cacheManager.setCacheBuilder(guavaCacheBuilder());
        return cacheManager;
    }

    private CacheBuilder<Object, Object> guavaCacheBuilder() {
        return CacheBuilder.newBuilder()
                .expireAfterWrite(10, TimeUnit.MINUTES) // 设置缓存失效时间为10分钟
                .maximumSize(100); // 设置缓存的最大条目数
    }
}

@Service
public class MyService {

    @Cacheable(value = "userCache", key = "#userId")
    public String getUserInfo(String userId) {
        // 模拟从数据库或其他数据源获取用户信息的操作
        System.out.println("Fetching user info for userId: " + userId);
        return "User info for userId: " + userId;
    }
}

在上述配置中,GuavaCacheManager 被配置为缓存管理器,通过 guavaCacheBuilder 方法配置了 Guava Cache 的一些属性,比如失效时间和最大条目数。这样,你就可以在 Spring Boot 项目中使用 Guava Cache 作为缓存容器了。在实际应用中,可以根据具体的业务需求调整 Guava Cache 的配置参数。

5.使用多种缓存管理器

在 Spring 中,一个项目中可以有多个 CacheManager,并且可以在不同的地方使用不同的 CacheManager 实例。这种情况下,每个 CacheManager 可以配置不同的缓存提供者,例如使用 Ehcache、Guava Cache 或者 Redis 等。

在实际项目中,有时候需要使用多个 CacheManager 的场景可能包括:

  1. 多种缓存需求: 不同的业务模块可能有不同的缓存需求,使用不同的 CacheManager 可以更好地满足各自的要求。
  2. 本地缓存与分布式缓存: 有些数据可能适合存储在本地缓存中,而有些可能需要存储在分布式缓存中。这时可以使用两个不同的 CacheManager 分别配置本地缓存和分布式缓存。
  3. 缓存过期策略: 不同的缓存数据可能需要不同的过期策略,使用不同的 CacheManager 可以灵活配置每个缓存的过期时间。

在配置多个 CacheManager 时,需要确保为它们分配唯一的名称,以便在使用时能够正确地引用。例如:

@Configuration
@EnableCaching
public class CacheConfig {

    @Bean
    public CacheManager localCacheManager() {
        // 配置本地缓存管理器
        return new ConcurrentMapCacheManager("localCache");
    }

    @Bean
    public CacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) {
        // 配置使用 Redis 的缓存管理器
        return RedisCacheManager.builder(redisConnectionFactory).build();
    }
}

在上述示例中,通过 localCacheManagerredisCacheManager 分别配置了本地缓存管理器和 Redis 缓存管理器。可以在需要的地方使用 @Cacheable 注解时指定使用哪个缓存管理器,例如:

@Service
public class MyService {

    @Cacheable(value = "localCache", key = "#userId", cacheManager = "localCacheManager")
    public String getLocalUserInfo(String userId) {
        // 从本地缓存获取用户信息
        // ...
    }

    @Cacheable(value = "redisCache", key = "#userId", cacheManager = "redisCacheManager")
    public String getRedisUserInfo(String userId) {
        // 从 Redis 缓存获取用户信息
        // ...
    }
}

总的来说,虽然一个项目可以有多个 CacheManager,但需要根据具体需求和业务场景来决定是否需要配置多个 CacheManager,以及如何使用它们。

常见的缓存管理器的区别以及怎么选型

常见的缓存管理器有不同的实现,如 Ehcache、Caffeine、Guava Cache、Redis 等,它们之间有一些区别,选型时需要考虑项目的需求和特点。以下是一些常见缓存管理器的区别和选型考虑:

  1. Ehcache:
  • 本地缓存: Ehcache 是一个本地缓存库,适用于单节点的应用程序。它提供了丰富的配置选项,包括缓存过期策略、最大条目数等。
  • 分布式: Ehcache 也有分布式版本(Terracotta),可以用于构建分布式缓存。
  1. Caffeine:
  • 本地缓存: Caffeine 是一个高性能的本地缓存库,设计用于提供快速的、无锁的缓存。它支持过期策略、最大条目数等。
  • 异步加载: Caffeine 支持异步加载数据,可用于优化缓存的冷启动问题。
  1. Guava Cache:
  • 本地缓存: Guava Cache 是 Google Guava 提供的本地缓存实现,类似于 Caffeine。它提供了基本的过期策略和最大条目数的配置。
  • 功能全面: Guava Cache 包含一些额外的功能,如监听缓存的变化、自动加载等。
  1. Redis:
  • 分布式缓存: Redis 是一种分布式缓存数据库,适用于分布式系统。它可以存储大量数据,并提供灵活的过期策略、持久性、集群支持等。
  • 数据类型支持: Redis 不仅可以作为缓存,还可以存储复杂的数据结构,如字符串、列表、哈希、集合等。
选型考虑因素:
  1. 项目规模: 对于小规模的单节点应用,本地缓存(如 Ehcache、Caffeine、Guava Cache)可能已经足够。对于大规模分布式系统,可能需要选择分布式缓存(如 Redis)。
  2. 性能需求: 如果追求高性能、低延迟,可以考虑使用 Caffeine 或 Guava Cache。它们都是本地缓存,具有快速的读写性能。
  3. 分布式需求: 如果应用部署在多个节点上,需要考虑使用支持分布式的缓存管理器,比如 Redis。Ehcache 的分布式版本也是一个选择。
  4. 数据持久性: 如果需要在系统重启后保留缓存数据,可以选择 Redis,因为它支持数据的持久化。
  5. 数据复杂性: 如果缓存的数据结构较为简单,且只需要基本的过期策略和最大条目数配置,本地缓存可能更轻量。如果需要更丰富的功能,如监听缓存变化、异步加载等,可以选择 Guava Cache。
  6. 系统架构: 考虑项目的整体架构和技术栈。如果已经在项目中使用了某个缓存管理器,且满足需求,可以继续使用,避免引入额外的复杂性。

常用注解

  1. **@Cacheable**
  • 用于声明一个方法的结果应该被缓存,如果缓存中存在相同的键(根据 SpEL 表达式生成的),则直接返回缓存的结果,不执行方法体。 (缓存中有则返回无读取数据后加入缓存并返回)
  • 示例:
@Cacheable(value = "userCache", key = "#userId")
public String getUserInfo(String userId) {
    // 方法体逻辑
}
  1. **@CachePut**
  • 用于声明一个方法的结果应该被缓存,不论缓存中是否已存在相同的键,都会执行方法体,并将结果更新到缓存中。 (更新缓存)
  • 示例:
@CachePut(value = "userCache", key = "#userId")
public String updateUserInfo(String userId, String newInfo) {
    // 方法体逻辑
}
  1. **@CacheEvict**
  • 用于声明一个方法执行后应清除缓存。可以通过 allEntries 属性清除缓存中的所有条目,也可以通过 beforeInvocation 属性指定在方法执行前还是执行后清除缓存。 (删除缓存)
  • 示例:
@CacheEvict(value = "userCache", key = "#userId")
public void clearUserInfoCache(String userId) {
    // 方法体逻辑
}
  1. **@Caching**
  • 用于将多个缓存注解组合在一起,可以在同一个方法上同时声明多个缓存注解。 (批量删除缓存)
  • 示例:
@Caching(
    cacheable = {
        @Cacheable(value = "userCache", key = "#userId"),
        @Cacheable(value = "emailCache", key = "#email")
    },
    put = {
        @CachePut(value = "userCache", key = "#userId"),
        @CachePut(value = "emailCache", key = "#email")
    }
)
public String getUserInfoByEmailOrId(String userId, String email) {
    // 方法体逻辑
}

这些注解可以与 SpEL 表达式一起使用,以动态生成缓存键。通过在方法上使用这些注解,可以方便地配置和管理缓存,而无需手动编写缓存逻辑。要使用这些注解,你需要在配置类上添加 @EnableCaching 注解,以启用 Spring Cache 功能。

import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableCaching
public class CacheConfig {
    // 配置相关的 CacheManager 等
}

这只是 Spring Cache 的一小部分功能,你可以根据具体的需求和业务场景选择合适的注解来实现缓存。在实际使用中,还可以配置一些高级功能,比如缓存的失效策略、条件缓存、缓存的自定义过期时间等。

缓存键的多种写法

在 Spring Cache 中,key 属性是用于指定缓存键的关键属性,它是一个 SpEL(Spring Expression Language)表达式。这意味着你可以使用 SpEL 表达式动态地构建缓存键。以下是一些常见的 key 属性的写法:

  1. 基本写法:
  • 最简单的情况下,可以直接使用方法的参数作为缓存键:
@Cacheable(value = "userCache", key = "#userId")
public String getUserInfo(String userId) {
    // 方法体逻辑
}
  1. 多个参数:
  • 如果方法有多个参数,你可以通过使用 # 符号和参数名来引用这些参数:
@Cacheable(value = "userCache", key = "#userId + '-' + #userName")
public String getUserInfo(String userId, String userName) {
    // 方法体逻辑
}
  1. SpEL 表达式:
  • 使用 SpEL 表达式进行更复杂的键的构建,你可以在表达式中使用条件、方法调用等:
@Cacheable(value = "userCache", key = "#userId.concat('-').concat(#userName.toUpperCase())")
public String getUserInfo(String userId, String userName) {
    // 方法体逻辑
}
  1. 返回值作为键:
  • 有时,你可能想使用方法的返回值作为缓存键。可以使用 result 关键字引用方法的返回值:
@Cacheable(value = "userCache", key = "#result.userId")
public UserInfo getUserInfo(String userId) {
    // 方法体逻辑
}
  1. 自定义 SpEL 函数:
  • 你还可以定义自己的 SpEL 函数,以便在表达式中重用逻辑:
@Cacheable(value = "userCache", key = "#myUtil.generateKey(#userId)")
public String getUserInfo(String userId) {
    // 方法体逻辑
}

在这个例子中,假设你已经在项目中定义了 myUtil 这个工具类,并在该类中有一个 generateKey 方法。

高级功能简单介绍

  1. 缓存失效策略:
  • 使用 @Cacheable@CachePut@CacheEvict 注解时,可以通过 condition 属性设置缓存失效的条件。例如,只有满足某个条件时才进行缓存:
@Cacheable(value = "userCache", key = "#userId", condition = "#result != null && #result.isValid()")
public UserInfo getUserInfo(String userId) {
    // 方法体逻辑
}
  1. 条件缓存:
  • 使用 unless 属性设置条件,如果满足条件,则不会缓存结果。与 condition 相反,unless 表达式的值为 true 时,不缓存:
@Cacheable(value = "userCache", key = "#userId", unless = "#result.isDisabled()")
public UserInfo getUserInfo(String userId) {
    // 方法体逻辑
}
  1. 自定义缓存管理器:
  • 如果你想使用除默认的 ConcurrentMapCacheManager 之外的缓存管理器,可以自定义 CacheManager
@Configuration
@EnableCaching
public class CustomCacheConfig extends CachingConfigurerSupport {

    @Bean
    public CacheManager cacheManager() {
        // 自定义的缓存管理器
        return new MyCustomCacheManager();
    }
}
  1. 自定义缓存注解:
  • 通过自定义缓存注解,你可以在方法上使用自定义的缓存注解,提供更复杂的缓存逻辑。例如:
@MyCustomCacheable(value = "userCache", key = "#userId")
public String getUserInfo(String userId) {
    // 方法体逻辑
}

其中 @MyCustomCacheable 是一个自定义的缓存注解,你需要在配置类中注册它,并实现相应的缓存逻辑。

  1. 缓存的自定义过期时间:
  • 在缓存管理器配置中,你可以设置默认的缓存过期时间。但有时,你可能需要对不同的缓存键设置不同的过期时间,可以使用 @Cacheableexpire 属性:
@Cacheable(value = "userCache", key = "#userId", expire = 600) // 过期时间为 600 秒
public UserInfo getUserInfo(String userId) {
    // 方法体逻辑
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容