介绍
Spring 从 3.1 版本开始定义了 org.springframework.cache.Cache
和 org.springframework.cache.CacheManager
接口来统一不同的缓存技术,并支持使用 JCache(JSR107)注解简化我们开发。
关于 JSR107:
Java Caching 定义了 5 个核心接口,分别是 CachingProvider
、CacheManager
、Cache
、Entry
和 Expiry
。
CachingProvider
:可以用来创建、配置、获取、管理和控制多个CacheManager
。一个应用可以在运行期间访问多个CachingProvider
。CacheManager
: 可以用来创建、配置、获取、管理和控制多个唯一命名的Cache
,这些Cache
存在于CacheManager
的上下文中。一个CacheManager
仅被一个CachingProvider
所拥有。Cache
:是一个类似Map
数据结构并临时存储以key
为索引的值。一个Cache
仅被一个CacheManager
所拥有。Entry
:是一个存储在Cache
中的key-value
对。Expiry
:每一个存储在Cache
中的条目有一个定义的有效期。一旦超过这个时间,条目就变更为过期的状态。一旦过期,条目将不可访问、更新和删除。缓存有效期可以通过ExpiryPolicy
设置。
由于 JSR107 的使用相对来说比较繁杂,所以这里我们使用 Spring 提供的缓存抽象。
Cache
接口为缓存的组件规范定义,包含缓存的各种操作集合。 Cache
接口下 Spring 提供了各种 Cache
的实现,如 RedisCache
、EhCacheCache
、ConcurrentMapCache
等。
关于 Spring 缓存抽象的使用:
每次调用具有缓存功能的方法时,Spring 会检查指定目标方法是否已经被调用过,如果有就直接从缓存中获取方法调用后的结果,如果没有就调用方法并缓存结果后返回,下次调用就直接从缓存中获取。
使用 Spring 缓存抽象时我们需要关注以下两点:
- 确定方法需要被缓存以及它们的缓存策略。
- 从缓存中读取之前缓存存储的数据。
Cache | 缓存接口,定义缓存操作。实现有:RedisCache、EhCacheCache、ConcurrentMapCache 等。 |
@CacheManager | 缓存管理器,管理各种缓存(Cache)组件。 |
@Cacheable | 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存。 |
@CacheEvict | 清空缓存。 |
@CachePut | 保证方法被调用,又希望结果被缓存。 |
@EnableCaching | 开启基于注解的缓存。 |
keyGenerator | 缓存数据时 key 的生成策略。 |
serialize | 缓存数据时 value 的序列化策略。 |
CacheManager
是用来管理多个Cache
组件的,对缓存的 CRUD 操作实际上还是通过Cache
组件,而每一个Cache
组件有自己唯一名称。
使用示例
搭建测试环境
1、新建 SpringBoot 项目,引入如下场景启动器:Cache
、Web
、Mysql
、MyBatis
。
2、初始化测试数据:
-- user.sql
SET FOREIGN_KEY_CHECKS=0;
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(40) DEFAULT NULL,
`gender` int(11) DEFAULT NULL COMMENT '0:女 1:男',
`birthday` date DEFAULT NULL,
`address` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=COMPACT;
-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES ('1', '张三', '1', '1997-02-23', '北京');
INSERT INTO `user` VALUES ('2', '李四', '0', '1998-02-03', '武汉');
INSERT INTO `user` VALUES ('3', '王五', '1', '1996-06-04', '上海');
3、编写与表对应的 JavaBean:
// com.springboot.bean.User
import java.util.Date;
public class User {
private Integer id;
private String name;
private Integer gender;
private Date birthday;
private String address;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getGender() {
return gender;
}
public void setGender(Integer gender) {
this.gender = gender;
}
public Date getBirthday() {
return birthday;
}
public void setBirthday(Date birthday) {
this.birthday = birthday;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
", gender=" + gender +
", birthday=" + birthday +
", address='" + address + '\'' +
'}';
}
}
4、配置数据库连接信息:
# application.yml
spring:
datasource:
username: root
password: root
url: jdbc:mysql://192.168.202.136:3306/springboot_cache
driver-class-name: com.mysql.jdbc.Driver
5、编写测试 Mapper:
// com.springboot.mapper.UserMappper
package com.springboot.mapper;
import com.springboot.bean.User;
import org.apache.ibatis.annotations.*;
import java.util.List;
public interface UserMappper {
@Select("select * from user")
public List<User> getAll();
@Select("select * from user where id=#{id}")
public User getById(Integer id);
@Delete("delete from user where id=#{id}")
public void deleteById(Integer id);
@Options(useGeneratedKeys = true,keyProperty = "id")
@Insert("insert into user(name,gender,birthday,address) values(#{name},#{gender},#{birthday},#{address})")
public void add(User user);
@Update("update user set name=#{name},gender=#{gender},birthday=#{birthday},address=#{address} where id=#{id}")
public void update(User user);
@Select("select * from user where name=#{name}")
public User getByName(String name);
}
6、编写 Service 并使用注解缓存功能:
// com.springboot.service.UserService
import com.springboot.bean.User;
import com.springboot.mapper.UserMappper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserService {
@Autowired
private UserMappper userMappper;
@Cacheable(cacheNames = {"users"})
public List<User> getAll(){
System.out.println("UserService.getAll() 执行了");
return userMappper.getAll();
}
}
7、配置 mapper 包扫描,开启注解缓存:
// com.springboot.CacheTestApplication
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
@SpringBootApplication
@EnableCaching // 开启注解缓存
@MapperScan("com.springboot.mapper") // mapper 包扫描
public class CacheTestApplication {
public static void main(String[] args) {
SpringApplication.run(CacheTestApplication.class, args);
}
}
8、编写 Controller 并测试:
// com.springboot.controller.UserController
import com.springboot.bean.User;
import com.springboot.mapper.UserMappper;
import com.springboot.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/users")
public List<User> getAll(){
return userService.getAll();
/*
首次访问会输出以下内容:
UserService.getAll() 执行了
后续访问不会输出,说明实际上并没有执行 userService.getAll() 方法,即未查询数据库从中获取数据,而是从缓存中获取
缓存成功
*/
}
}
@Cacheable
主要针对方法配置,能够根据方法的请求参数对其结果进行缓存。
属性
cacheNames/value
:指定 Cache
组件名称。
key
:指定缓存数据所使用的 key
,默认 key
使用参数值。比如 id
为 1
,那么在 Cache
组件中的 key
就为 1
。
key
还可以通过 SpEL
表达式进行取值。规则如下:
名字 | 位置 | 描述 | 实力 |
---|---|---|---|
methodName | root object | 当前被调用的方法名 | #root.methodName |
method | root object | 当前被调用的方法对象 | #root.method.name |
target | root object | 当前被调用的目标对象 | #root.target |
targetClass | root object | 当前被调用的目标对象类 | #root.targetClass |
args | root object | 当前被调用方法的参数列表 | #root.args[0] |
caches | root object | 当前被调用方法使用的缓存组件列表,如 @Cacheable(value={"cache1","cache2"}) 就有两个缓存组件 | #root.caches[0].name |
argument name | evaluation context | 可以直接使用 #参数名 来获取对应参数值,也可以使用 #p0 或 #a0 的形式,0 代表参数索引 | #id、#a0、#p0 |
result | evaluation context | 获取方法执行后的返回值 | #result |
keyGenerator
:通过 IoC 容器中的生成器组件id
来指定key
的生成器,key
与keyGenerator
二选一使用。cacheManager
:指定缓存管理器 bean。cacheSolver
:指定缓存解析器 bean。condition
:指定条件,当符合指定条件时才进行缓存,也可使用 SpEL 表达式同key
。unless
:指定条件,当符合指定条件时不进行缓存,也可使用 SpEL 表达式同key
。sync
:是否使用异步模式,如果启用,那么unless
属性失效。
示例
// com.springboot.service.UserService#getById
@Cacheable(cacheNames = {"users"})
public User getById(Integer id) {
System.out.println("UserService.getById(" + id + ") 执行了");
return userMappper.getById(id);
}
// com.springboot.controller.UserController#getById
@GetMapping("/user/{id}")
public User getById(@PathVariable Integer id) {
return userService.getById(id);
/*
首次访问 http://localhost:8080/user/1 会输出一下内容:
UserService.getById(1) 执行了
后续访问不会输出,说明实际上并没有执行 userService.getById(1) 方法,即未查询数据库从中获取数据,而是从缓存中获取
而如果访问 http://localhost:8080/user/2 会输出一下内容:
UserService.getById(2) 执行了
后续访问也不会输出,即会自动根据参数的不同进行缓存
OK 缓存成功
*/
}
@CachePut
主要针对方法配置,目标方法正常执行完成后将其结果更新到缓存。
属性
同 @Cacheable
注解。
示例
// com.springboot.service.UserService#update
/*
注意:这里要保证 key 与 getById 方法所使用 key 的生成后的值相同,否则会出现更新缓存后通过 getById 获取数据依旧为旧数据的情况
*/
@CachePut(cacheNames = {"users"}, key = "#user.id")
public User update(User user) {
System.out.println("UserService.update(" + user + ") 执行了");
userMappper.update(user);
return user;
}
// com.springboot.controller.UserController#update
@PutMapping("/user")
public User update(User user){
return userService.update(user);
/*
1、以 get 方式访问 localhost:8080/user/1 会输出一下内容:
UserService.getById(1) 执行了
后续访问不会输出
2、以 put 方式访问 localhost:8080/user,修改数据,输出以下内容:
UserService.update(User{id=1, name='张三new', gender=0, birthday=Wed Apr 03 00:00:00 CST 1996, address='深圳'}) 执行了
每次访问都会输出
3、以 get 方式访问 localhost:8080/user/1 不会输出内容,返回数据是更新后的数据
*/
}
@CacheEvict
主要针对方法配置,默认在目标方法正常执行完成后清除缓存。
属性
beforeInvocation
:默认为false
,标识是否在目标方法执行之前清除缓存。allEntries
:默认为false
,标识是否清除缓存组件中所有内容。
其它同 @Cacheable
注解。
示例
// com.springboot.service.UserService#delete
/*
删除缓存,key 可以不指定,因为 key 默认就是以 id 为基础生成的
*/
@CacheEvict(cacheNames = {"users"}, key = "#id")
public void delete(Integer id) {
System.out.println("UserService.delete(" + id + ") 执行了");
}
// com.springboot.controller.UserController#delete
@DeleteMapping("/user/{id}")
public void delete(@PathVariable Integer id){
userService.delete(id);
/*
1、以 get 方式访问 localhost:8080/user/1 会输出以下内容
UserService.getById(1) 执行了
后续访问不会输出
2、以 delete 方式访问 localhost:8080/user/1 会输出以下内容
UserService.delete(1) 执行了
每次访问都会输出
3、以 get 方式再次访问 localhost:8080/user/1 会输出以下内容
UserService.getById(1) 执行了
即缓存被删除了重新执行方法获取了数据
*/
}
@Caching
主要针对方法配置,为 @Cacheable
、@CachePut
、@CacheEvict
的组合注解,常用于定制较复杂的缓存策略。
属性
cacheable
:放置@Cacheable
注解数组。put
:放置@CachePut
注解数组。evict
:放置@CacheEvict
注解数组。
示例
// com.springboot.service.UserService#getByName
@Caching(
cacheable = {
// 以 name 生成 key 进行缓存
@Cacheable(value = "users", key = "#name")
},
put = {
// 以 id 生成 key ,且执行结果不为空时更新缓存
@CachePut(value = "users", key = "#result.id", condition = "#result != null")
},
evict = {
// name 为 deleteAll 清除所有缓存
@CacheEvict(value = "users", condition = "#name=='deleteAll'", allEntries = true)
}
)
public User getByName(String name) {
System.out.println("UserService.getByName(" + name + ") 执行了");
return userMappper.getByName(name);
}
// com.springboot.controller.UserController#getByName
@GetMapping("/user/name/{name}")
public User getByName(@PathVariable String name){
return userService.getByName(name);
/*
1、以 get 方式请求 localhost:8080/user/name/李四,输出以下内容:
UserService.getByName(李四) 执行了
每次访问都会输出,因为 @CachePut 需要执行结果更新缓存
2、以 get 方式请求 localhost:8080/user/2,没有输出内容,因为通过 [1] 已经完成了 id 为 2 的缓存,直接从缓存中取出结果返回了
3、以 get 方式请求 localhost:8080/user/name/deleteAll,输出以下内容:
UserService.getByName(deleteAll) 执行了
每次访问都会输出,因为 @CachePut 需要执行结果更新缓存
4、以 get 方式请求 localhost:8080/user/2,输出以下内容:
UserService.getById(2) 执行了
后续访问不会输出,这次输出的原因是通过 [3] 清楚了所有缓存
*/
}
@CacheConfig
通常用来标注在类上,用来定义公共缓存配置。
属性
具有如下属性:cachenames
、keyGenerator
、cacheManager
、cacheResolver
,可参考 @Cacheable
注解属性。
示例
// com.springboot.service.UserService
import com.springboot.bean.User;
import com.springboot.mapper.UserMappper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.*;
import org.springframework.stereotype.Service;
/**
* 因为下面方法都是使用同一个 Cache 组件,所以可以在类上一次性指定所有方法使用的 Cache 组件名称
*/
@CacheConfig(cacheNames = {"users"} )
@Service
public class UserService {
@Autowired
private UserMappper userMappper;
@Cacheable(keyGenerator = "myKeyGenerator")
public User getById(Integer id) {
System.out.println("UserService.getById(" + id + ") 执行了");
return userMappper.getById(id);
}
@CachePut(key = "#user.id")
public User update(User user) {
System.out.println("UserService.update(" + user + ") 执行了");
userMappper.update(user);
return user;
}
@CacheEvict(key = "#id")
public void delete(Integer id) {
System.out.println("UserService.delete(" + id + ") 执行了");
}
@Caching(
cacheable = {
@Cacheable(key = "#name")
},
put = {
@CachePut(key = "#result.id", condition = "#result != null")
},
evict = {
@CacheEvict(condition = "#name=='deleteAll'", allEntries = true)
}
)
public User getByName(String name) {
System.out.println("UserService.getByName(" + name + ") 执行了");
return userMappper.getByName(name);
}
}
原理分析及小结
源码
依旧是从自动配置类入手以 @Cacheable
注解执行流程进行分析:
// org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration
package org.springframework.boot.autoconfigure.cache;
@Configuration
@ConditionalOnClass(CacheManager.class)
@ConditionalOnBean(CacheAspectSupport.class)
@ConditionalOnMissingBean(value = CacheManager.class, name = "cacheResolver")
@EnableConfigurationProperties(CacheProperties.class)
@AutoConfigureBefore(HibernateJpaAutoConfiguration.class)
@AutoConfigureAfter({ CouchbaseAutoConfiguration.class, HazelcastAutoConfiguration.class,
RedisAutoConfiguration.class })
// <1>
@Import(CacheConfigurationImportSelector.class)
public class CacheAutoConfiguration {
// 注册缓存管理器定制器
@Bean
@ConditionalOnMissingBean
public CacheManagerCustomizers cacheManagerCustomizers(
ObjectProvider<List<CacheManagerCustomizer<?>>> customizers) {
return new CacheManagerCustomizers(customizers.getIfAvailable());
}
@Bean
public CacheManagerValidator cacheAutoConfigurationValidator(
CacheProperties cacheProperties, ObjectProvider<CacheManager> cacheManager) {
return new CacheManagerValidator(cacheProperties, cacheManager);
}
@Configuration
@ConditionalOnClass(LocalContainerEntityManagerFactoryBean.class)
@ConditionalOnBean(AbstractEntityManagerFactoryBean.class)
protected static class CacheManagerJpaDependencyConfiguration
extends EntityManagerFactoryDependsOnPostProcessor {
public CacheManagerJpaDependencyConfiguration() {
super("cacheManager");
}
}
static class CacheManagerValidator implements InitializingBean {
private final CacheProperties cacheProperties;
private final ObjectProvider<CacheManager> cacheManager;
CacheManagerValidator(CacheProperties cacheProperties,
ObjectProvider<CacheManager> cacheManager) {
this.cacheProperties = cacheProperties;
this.cacheManager = cacheManager;
}
@Override
public void afterPropertiesSet() {
Assert.notNull(this.cacheManager.getIfAvailable(),
"No cache manager could "
+ "be auto-configured, check your configuration (caching "
+ "type is '" + this.cacheProperties.getType() + "')");
}
}
static class CacheConfigurationImportSelector implements ImportSelector {
// <2>
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
CacheType[] types = CacheType.values();
String[] imports = new String[types.length];
for (int i = 0; i < types.length; i++) {
imports[i] = CacheConfigurations.getConfigurationClass(types[i]);
}
return imports;
}
}
}
看到 <1>
处,该行是通过 @Import(CacheConfigurationImportSelector.class)
导入缓存配置类, CacheConfigurationImportSelector
为自动配置类中的内部类,对应的导入方法为 <2>
处的 selectImports
方法。通过调试会发现它实际导入了如下配置类:
org.springframework.boot.autoconfigure.cache.GenericCacheConfiguration
org.springframework.boot.autoconfigure.cache.JCacheCacheConfiguration
org.springframework.boot.autoconfigure.cache.EhCacheCacheConfiguration
org.springframework.boot.autoconfigure.cache.HazelcastCacheConfiguration
org.springframework.boot.autoconfigure.cache.InfinispanCacheConfiguration
org.springframework.boot.autoconfigure.cache.CouchbaseCacheConfiguration
org.springframework.boot.autoconfigure.cache.RedisCacheConfiguration
org.springframework.boot.autoconfigure.cache.CaffeineCacheConfiguration
org.springframework.boot.autoconfigure.cache.GuavaCacheConfiguration
org.springframework.boot.autoconfigure.cache.SimpleCacheConfiguration
org.springframework.boot.autoconfigure.cache.NoOpCacheConfiguration
那么实际生效的是哪个配置类呢?可以在配置文件中开启 debug 模式,接着在控制台中就可以看到:
SimpleCacheConfiguration matched:
- Cache org.springframework.boot.autoconfigure.cache.SimpleCacheConfiguration automatic cache type (CacheCondition)
- @ConditionalOnMissingBean (types: org.springframework.cache.CacheManager; SearchStrategy: all) did not find any beans (OnBeanCondition)
即生效的配置类为 org.springframework.boot.autoconfigure.cache.SimpleCacheConfiguration
。查看该配置类:
// org.springframework.boot.autoconfigure.cache.SimpleCacheConfiguration
package org.springframework.boot.autoconfigure.cache;
@Configuration
@ConditionalOnMissingBean(CacheManager.class)
@Conditional(CacheCondition.class)
class SimpleCacheConfiguration {
private final CacheProperties cacheProperties;
private final CacheManagerCustomizers customizerInvoker;
SimpleCacheConfiguration(CacheProperties cacheProperties,
CacheManagerCustomizers customizerInvoker) {
this.cacheProperties = cacheProperties;
this.customizerInvoker = customizerInvoker;
}
// <3>
@Bean
public ConcurrentMapCacheManager cacheManager() {
ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager();
List<String> cacheNames = this.cacheProperties.getCacheNames();
if (!cacheNames.isEmpty()) {
cacheManager.setCacheNames(cacheNames);
}
return this.customizerInvoker.customize(cacheManager);
}
}
在 <3>
处可以看到往 IoC 容器中注册了一个 org.springframework.cache.concurrent.ConcurrentMapCacheManager
,该类实现了 org.springframework.cache.CacheManager
接口,可以使用它的 getCache(String)
方法来获取缓存组件:
// org.springframework.cache.concurrent.ConcurrentMapCacheManager#getCache
public Cache getCache(String name) {
// this.cacheMap : private final ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap(16);
Cache cache = (Cache)this.cacheMap.get(name);
if (cache == null && this.dynamic) {
synchronized(this.cacheMap) {
// 从 ConcurrentMap 对象中获取 Cache 组件
cache = (Cache)this.cacheMap.get(name);
if (cache == null) { // 如果获取的 Cache 组件为空
// 新创建一个 ConcurrentMapCache 组件
cache = this.createConcurrentMapCache(name);
// 将其放入 ConcurrentMap 对象中
this.cacheMap.put(name, cache);
}
}
}
return cache;
}
可以看到该方法的作用实际上就是用来获取一个 ConcurrentMapCache
类型的 Cache
组件,而我们通过 Cache
组件获取数据时是通过 get
方法,最终是通过该类的 get
方法调用 lookup(key)
方法:
// org.springframework.cache.concurrent.ConcurrentMapCache#lookup
@Override
protected Object lookup(Object key) {
// this.store : private final ConcurrentMap<Object, Object> store;
return this.store.get(key);
}
而这个 key
默认是使用 org.springframework.cache.interceptor.SimpleKeyGenerator#generateKey
生成的:
// org.springframework.cache.interceptor.SimpleKeyGenerator
package org.springframework.cache.interceptor;
import java.lang.reflect.Method;
public class SimpleKeyGenerator implements KeyGenerator {
@Override
public Object generate(Object target, Method method, Object... params) {
return generateKey(params);
}
public static Object generateKey(Object... params) {
if (params.length == 0) {
//SimpleKey.EMPTY : public static final SimpleKey EMPTY = new SimpleKey();
return SimpleKey.EMPTY;
}
if (params.length == 1) {
Object param = params[0];
if (param != null && !param.getClass().isArray()) {
return param;
}
}
return new SimpleKey(params);
}
}
可以看到它的生成规则如下:
- 当目标方法的参数个数为空时,使用
new SimpleKey()
实例化一个SimpleKey
作为key
值。 - 当目标方法的参数个数为
1
且该参数不为空也不是数组类型时,直接使用该参数值作为key
值。 - 当目标方法的参数不止一个时,使用
new SimpleKey(params)
实例化一个SimpleKey
作为key
值。
即ConcurrentMapCache
组件也是将实际缓存数据存放在ConcurrentMap
对象中。
小结
ConcurrentMapCache
类实现了 Cache
接口,Cache
接口中定义了 get
方法用来获取数据、put
方法用来存放数据、evict
方法用来删除指定数据、clear
方法用来清空所有数据。
上述运行流程如下:
- 目标方法运行之前会使用
CacheManager
先根据cacheNames
获取Cache
组件,如果没有,则会创建一个新的Cache
组件返回。 - 通过返回的
Cache
组件使用生成的key
获取缓存内容。 - 如果获取到的缓存内容为空,就直接调用目标方法,然后将目标方法执行后的返回值通过
Cache
组件的put
方法放入缓存(ConcurrentMap
对象)中;如果获取到的缓存内容不为空,就直接返回获取到的缓存内容,不会执行目标方法。
默认情况下:
- 生效的自动配置类为
SimpleCacheConfiguration
。 CacheManager
的实现为ConcurrentMapCacheManager
。key
是使用KeyGenerator
生成的,实现为SimpleKeyGenerator
。
评论区