跳到主要内容

SpringBoot Cache 的使用

什么是 Spring Cache

Spring Cache 本身是一个缓存体系的抽象实现,并没有具体的缓存能力,要使用 Spring Cache 还需要具体的缓存实现来完成。

Spring Boot 集成了多种 cache 的实现,如果你没有在配置类中声明 CacheManager,那么 SpringBoot 会按顺序在下面的实现类中寻找:

  1. Generic
  2. JCache (JSR-107) (EhCache 3, Hazelcast, Infinispan, and others)
  3. EhCache 2.x
  4. Hazelcast
  5. Infinispan
  6. Couchbase
  7. Redis
  8. Caffeine
  9. Simple

不过注意,如果使用的是 Redis 不需要使用这个 spring-boot-starter-cache 它被内置到 spring-boot-starter-data-redis 这个包里面去了

如果什么都没有配置默认使用的是 Simple 即,使用 ConcurrentHashMap 来作为存储缓存

官方查询图书的案例

配置环境

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

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

创建实体

这里直接使用 Lombok 省的写 Getter 和 Setter

@Data
@AllArgsConstructor
public class Book {
private String isbn;
private String title;
}

创建 Repository

这个案例直接使用一些延迟来模拟数据库了

public interface BookRepository {
Book getByIsbn(String isbn);
}

实现这个接口(可以看到在每次取得数据时,这里加入个 Sleep 来模拟延迟)

package com.example.caching;

import org.springframework.stereotype.Component;

@Component
public class SimpleBookRepository implements BookRepository {

@Override
public Book getByIsbn(String isbn) {
simulateSlowService();
return new Book(isbn, "Some book");
}

// Don't do this at home
private void simulateSlowService() {
try {
long time = 3000L;
Thread.sleep(time);
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
}

}

直接使用 Repository

package com.example.caching;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class CachingApplication {

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

}

然后调用上面这个 SimpleBookRepository,注意这里继承的这个 CommandLineRunner 接口,它的作用是能在 SpringApplication 启动后执行某些代码

package com.example.caching;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

@Component
public class AppRunner implements CommandLineRunner {

private static final Logger logger = LoggerFactory.getLogger(AppRunner.class);

private final BookRepository bookRepository;

public AppRunner(BookRepository bookRepository) {
this.bookRepository = bookRepository;
}

@Override
public void run(String... args) throws Exception {
logger.info(".... Fetching books");
// 注意,这里就第一个和第二个不同,后面的都是一样的
logger.info("isbn-1234 -->" + bookRepository.getByIsbn("isbn-1234"));
logger.info("isbn-4567 -->" + bookRepository.getByIsbn("isbn-4567"));
logger.info("isbn-1234 -->" + bookRepository.getByIsbn("isbn-1234"));
logger.info("isbn-4567 -->" + bookRepository.getByIsbn("isbn-4567"));
logger.info("isbn-1234 -->" + bookRepository.getByIsbn("isbn-1234"));
logger.info("isbn-1234 -->" + bookRepository.getByIsbn("isbn-1234"));
}

}

然后运行后就可以观察到每隔三秒才 “查询” 到一本书

这种时候就可以请出下面的主角,Cache 了

使用 Cache

这时对上面的 SimpleBookRepository 进行改造一下

package com.example.caching;

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

@Component
public class SimpleBookRepository implements BookRepository {

@Override
@Cacheable("books")
public Book getByIsbn(String isbn) {
simulateSlowService();
return new Book(isbn, "Some book");
}

// Don't do this at home
private void simulateSlowService() {
try {
long time = 3000L;
Thread.sleep(time);
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
}

}

然后需要在启动类上加入这个 @EnableCaching 注解

package com.example.caching;

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

@SpringBootApplication
@EnableCaching
public class CachingApplication {

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

}

这个 @EnableCaching 注解会激活一个 post-processor 来检查每一个公共方法上是否存在缓存注解,如果找到这样的注释,则会自动创建一个代理来拦截方法调用并相应地处理缓存行为。

这几个缓存注解分别是 @Cacheable@CachePut@CacheEvict

更多细节参考自 官方文档 对这个的说明

然后 Spring Boot 会自动创建一个合适的 CacheManager 来管理缓存(就是上面那些具体的缓存实现),默认什么都没有配置的情况下 Spring Boot 使用的是 ConcurrentHashMap 来作为缓存存储

测试这个用例,可以发现速度显著提升

缓存注解和工具类一览

上面的教程简单介绍了 Spring Cache 怎么用,这里开始详细介绍它包含的一些工具

名称解释
Cache缓存接口,定义缓存操作。实现有:RedisCache、EhCacheCache、ConcurrentMapCache等
CacheManager缓存管理器,管理各种缓存(cache)组件
@Cacheable主要针对方法配置,能够根据方法的请求参数对其进行缓存
@CacheEvict清空缓存
@CachePut保证方法被调用,又希望结果被缓存。与@Cacheable区别在于是否每次都调用方法,常用于更新
@EnableCaching开启基于注解的缓存
keyGenerator缓存数据时key生成策略
serialize缓存数据时value序列化策略
@CacheConfig统一配置本类的缓存注解的属性

注解的主要参数

@Cacheable/@CachePut/@CacheEvict 主要的参数

// 1、value	缓存的名称,在 spring 配置文件中定义,必须指定至少一个
@Cacheable(value="mycache") 或者
@Cacheable(value={"cache1","cache2"}


// 2、key 缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合
@Cacheable(value="testcache",key="#id")


// 3、condition 缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存/清除缓存
@Cacheable(value="testcache",condition="#userName.length()>2")


// 4、unless 否定缓存。当条件结果为TRUE时,就不会缓存。
@Cacheable(value="testcache",unless="#userName.length()>2")


// 5、allEntries(@CacheEvict ) 是否清空所有缓存内容,缺省为 false,如果指定为 true,则方法调用后将立即清空所有缓存
@CacheEvict(value="testcache",allEntries=true)


// 6、beforeInvocation(@CacheEvict) 是否在方法执行前就清空,缺省为 false,如果指定为 true,则在方法还没有执行的时候就清空缓存,缺省情况下,如果方法执行抛出异常,则不会清空缓存
@CacheEvict(value="testcache",beforeInvocation=true)

SpEL上下文数据

Spring Cache 提供了一些供我们使用的 SpEL 上下文数据,下表直接摘自 Spring 官方文档:

名称位置描述示例
methodNameroot对象当前被调用的方法名#root.methodname
methodroot对象当前被调用的方法#root.method.name
targetroot对象当前被调用的目标对象实例#root.target
targetClassroot对象当前被调用的目标对象的类#root.targetClass
argsroot对象当前被调用的方法的参数列表#root.args[0]
cachesroot对象当前方法调用使用的缓存列表#root.caches[0].name
Argument Name执行上下文当前被调用的方法的参数,如 findArtisan(Artisan artisan),可以通过 #artsian.id 获得参数#artsian.id
result执行上下文方法执行后的返回值(仅当方法执行后的判断有效,如 unless cacheEvict 的 beforeInvocation=false)#result

注意:

1、当我们要使用 root 对象的属性作为 key 时我们也可以将 #root 省略,因为 Spring 默认使用的就是 root 对象的属性。 如

@Cacheable(key = "targetClass + methodName +#p0")

2、使用方法参数时我们可以直接使用 #参数名 或者 #p参数index。 如:

@Cacheable(value="users", key="#id")

@Cacheable(value="users", key="#p0")

SpEL提供的运算符

| 类型       | 运算符                                         |
| ---------- | ---------------------------------------------- |
| 关系 | <,>,<=,>=,==,!=,lt,gt,le,ge,eq,ne |
| 算术 | +,- ,* ,/,%,^ |
| 逻辑 | &&,\|\|,!,and,or,not,between,instanceof |
| 条件 | ?: (ternary),?: (elvis) |
| 正则表达式 | matches |
| 其他类型 | ?.,?[…],![…],\^[…], $[…] |

各个注解详解

缓存 @Cacheable

@Cacheable 注解会先查询是否已经有缓存,有会使用缓存,没有则会执行方法并缓存。

@Cacheable(value = "emp" ,key = "targetClass + methodName +#p0")
public List<NewJob> queryAll(User uid) {
return newJobDao.findAllByUid(uid);
}

此处的 value 是必需的,它指定了你的缓存存放在哪块命名空间。

此处的 key 是使用的 spEL 表达式(就是表示缓存的 Key,不写则按照方法的所有参数进行组合)。

这里有一个小坑,如果把 methodName 换成 method 运行会报错,观察它们的返回类型,原因在于 methodName 是 String 而 method 是 Method。

此处的 User 实体类一定要实现序列化 public class User implements Serializable,否则会报 java.io.NotSerializableException 异常。

打开 @Cacheable 注解的源码,可以看到该注解提供的其他属性,如:

String[] cacheNames() default {}; //和value注解差不多,二选一
String keyGenerator() default ""; //key的生成器。key/keyGenerator二选一使用
String cacheManager() default ""; //指定缓存管理器
String cacheResolver() default ""; //或者指定获取解析器
String condition() default ""; //条件符合则缓存
String unless() default ""; //条件符合则不缓存
boolean sync() default false; //是否使用异步模式

配置 @CacheConfig

当我们需要缓存的地方越来越多,可以使用 @CacheConfig(cacheNames = {"myCache"}) 注解来统一指定 value 的值,这时可省略 value,如果你在你的方法依旧写上了 value,那么依然以方法的 value 值为准。

如下所示:

@CacheConfig(cacheNames = {"myCache"})
public class BotRelationServiceImpl implements BotRelationService {
@Override
@Cacheable(key = "targetClass + methodName +#p0")//此处没写value
public List<BotRelation> findAllLimit(int num) {
return botRelationRepository.findAllLimit(num);
}
.....
}

查看它的其它属性

String keyGenerator() default "";  //key的生成器。key/keyGenerator二选一使用
String cacheManager() default ""; //指定缓存管理器
String cacheResolver() default ""; //或者指定获取解析器

更新 @CachePut

@CachePut 注解的作用 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存,和 @Cacheable 不同的是,它每次都会触发真实方法的调用。简单来说就是既调用方法,又更新缓存数据。修改了数据库的某个数据,同时更新缓存。这个注解是先调用目标方法,然后将结果缓存起来。

CachePut 既然每次都会执行,那还有缓存的意义吗?

只有当结合 @Cacheable 才有用

  • @CachePut 负责更新缓存
  • @Cacheable 负责查询缓存

但需要注意的是该注解的 value 和 key 必须与要更新的缓存相同,也就是与 @Cacheable 相同。示例:

@CachePut(value = "emp", key = "targetClass + #p0")
public NewJob update(NewJob job) {
NewJob newJob = newJobDao.findAllById(job.getId());
newJob.update(job);
return job;
}

@Cacheable(value = "emp", key = "targetClass +#p0")
public NewJob save(NewJob job) {
newJobDao.save(job);
return job;
}

如上所示,它们为同一个方法,如果调用了 update 方法,则会更新缓存,而一般情况下调用 save 则会直接返回缓存

查看它的其它属性:

String[] cacheNames() default {}; //与value二选一
String keyGenerator() default ""; //key的生成器。key/keyGenerator二选一使用
String cacheManager() default ""; //指定缓存管理器
String cacheResolver() default ""; //或者指定获取解析器
String condition() default ""; //条件符合则缓存
String unless() default ""; //条件符合则不缓存

清除 @CacheEvict

@CacheEvict 的作用 主要针对方法配置,能够根据一定的条件对缓存进行清空 。

// allEntries	是否清空所有缓存内容,缺省为 false,如果指定为 true,则方法调用后将立即清空所有缓存
@CacheEvict(value="testcache",allEntries=true)

// beforeInvocation 是否在方法执行前就清空,缺省为 false,如果指定为 true,则在方法还没有执行的时候就清空缓存,缺省情况下,如果方法执行抛出异常,则不会清空缓存
@CacheEvict(value="testcache",beforeInvocation=true)

使用例:

@Cacheable(value = "emp",key = "#p0.id")
public NewJob save(NewJob job) {
newJobDao.save(job);
return job;
}

//清除一条缓存,key为要清空的数据
@CacheEvict(value="emp",key="#id")
public void delete(int id) {
newJobDao.deleteAllById(id);
}

//方法调用后清空所有缓存
@CacheEvict(value="accountCache",allEntries=true)
public void deleteAll() {
newJobDao.deleteAll();
}

//方法调用前清空所有缓存
@CacheEvict(value="accountCache",beforeInvocation=true)
public void deleteAll() {
newJobDao.deleteAll();
}

其他属性:

String[] cacheNames() default {}; //与value二选一
String keyGenerator() default ""; //key的生成器。key/keyGenerator二选一使用
String cacheManager() default ""; //指定缓存管理器
String cacheResolver() default ""; //或者指定获取解析器
String condition() default ""; //条件符合则清空

组合 @Caching

组合多个 Cache 注解使用,此时就需要 @Caching 组合多个注解标签了。

@Caching(cacheable = {
@Cacheable(value = "emp",key = "#p0"),
...
},
put = {
@CachePut(value = "emp",key = "#p0"),
...
},evict = {
@CacheEvict(value = "emp",key = "#p0"),
....
})
public User save(User user) {
....
}

直接操作 CacheManager

上文说过这个 Spring Cache 会自动注册一个 CacheManager,实际上可以直接操作这个 CacheManager 来增删改 Cache 的

使用示例

下面这个例子有点乱,它是 SpringSecurity 认证那里使用到的例子,这里直接搬过来了,等以后用到这力再重构笔记

封装一个 CacheName 枚举对象

@AllArgsConstructor
public enum CacheName {

USER("USER"),
PERMISSION("PERMISSION");

private final String cacheName;

public String getCacheName() {
return cacheName;
}
}

上面的这个 getCacheName 是每个枚举对象的方法

创建一个接口,方便操作

public interface Cache {

<T> T get(CacheName cacheName, String key, Class<T> clazz);

void put(CacheName cacheName, String key, Object value);

void remove(CacheName cacheName, String key);
}

编写具体的实现类,可以看到下面的 CacheManager 就是 Spring Boot 自动注入进来的

@Slf4j
@Service("caffeineCache")
public class CaffeineCache implements Cache {
@Autowired
private CacheManager caffeineCacheManager;


@Override
public <T> T get(CacheName cacheName, String key, Class<T> clazz) {
log.debug("{} get -> cacheName [{}], key [{}], class type [{}]", this.getClass().getName(), cacheName, key, clazz.getName());
return Objects.requireNonNull(caffeineCacheManager.getCache(cacheName.getCacheName())).get(key, clazz);
}

@Override
public void put(CacheName cacheName, String key, Object value) {
log.debug("{} put -> cacheName [{}], key [{}], value [{}]", this.getClass().getName(), cacheName, key, value);
Objects.requireNonNull(caffeineCacheManager.getCache(cacheName.getCacheName())).put(key, value);
}

@Override
public void remove(CacheName cacheName, String key) {
log.debug("{} remove -> cacheName [{}], key [{}]", this.getClass().getName(), cacheName, key);
Objects.requireNonNull(caffeineCacheManager.getCache(cacheName.getCacheName())).evict(key);
}
}

然后就能直接操作这个 Cache 了

caffeineCache.remove(CacheName.USER, AuthProvider.getLoginAccount());

Reference

参考资料 官方教程 Caching Data with Spring 这里主要参考官方的这个教程 参考资料 史上最全的Spring Boot Cache使用与整合