文章目录
JSR-107与SpringBoot缓存
主要介绍JSR-107、Spring缓存抽象和基于Redis的缓存实现。
JSR-107
JSR是Java Specification Requests(Java规范请求)的缩写,是提交Java规范。JSR-107是关于如何使用缓存的规范,是Java提供的一个接口规范,类似JDBC规范,没有具体的实现。
JSR-107核心接口
Java Caching(JSR-107)定义了5个核心接口,分别是CachingProvider,CacheManager,Cache,Entry和Expiry。
- CachingProvider(缓存提供者):创建、配置、获取、管理和控制多个CacheManager
- CacheManager(缓存管理器):创建、配置、获取、管理和控制多个唯一命名的Cache,Cache存在于CacheManager的上下文中。一个CacheManager仅对应一个CachingProvider
- Cache(缓存):是由CacheManager管理的,CacheManager管理Cache的生命周期,Cache存在于CacheManager的上下文中,是一个类似map的数据结构,并临时存储以key为索引的值。一个Cache仅被一个CacheManager所拥有
- Entry(缓存键值对):是一个存储在Cache中的key-value对
- Expiry(缓存时效):每一个存储在Cache中的条目都有一个定义的有效期。一旦超过这个时间,条目就自动过期,过期后,条目将不可以访问、更新和删除操作。缓存有效期可以通过ExpiryPolicy设置
JSR-107图示
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Zcmqv0fd-1625653766740)(SpringBoot缓存.assets\image-20210707091604842.png)]
一个应用可以有多个缓存提供者(CachingProvider),一个缓存提供者可以获得多个缓存管理者(CacheManager),一个缓存管理器管理着多个不同的缓存(Cache),缓存中是一个个的缓存键值对(Entry),每个entry有一个有限期(Expiry)。
使用JSR-107需要引入依赖
<dependency>
<groupId>javax.cache</groupId>
<artifactId>cache-api</artifactId>
</dependency>
Spring的缓存抽象
缓存抽象定义
Spring从3.1开始定义了org.springframework.cache.Cache和org.springframework.cache.CacheManager接口来统一不同的缓存技术;并支持使用Java Caching (JSR-107)注解简化缓存开发。
Spring Cache只负责维护抽象层,具体实现有自己的选型决定,将缓存处理和缓存技术解耦。
每次调用需要缓存功能的方法时,Spring会检查指定参数的指定的目标方法是否已经被调用过,如果有就直接从缓存中获取方法调用后的结果,如果没有就调用方法并缓存结果后返回给用户。下次调用直接从缓存中获取。
使用Spring缓存抽象时我们需要关注以下两点:
①确定方法需要被缓存以及他们的缓存策略
②从缓存中读取之前缓存存储的数据
重要接口
- Cache:缓存抽象的规范接口,缓存实现有:RedisCache,EhCache,ConcurrentMapCache等
- CacheManager:缓存管理器,管理Cache的生命周期
Spring缓存使用
重要概念&缓存注解
概念/注解 | 作用 |
---|---|
Cache | 缓存接口,定义缓存操作。实现有RedisCache,EhCache,ConcurrentMapCache等 |
CacheManager | 缓存管理器,管理各种缓存(Cache)组件 |
@Cacheable | 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存 |
@CacheEvict | 清空缓存 |
@CachePut | 保证方法被调用,又希望结果被缓存 |
@EnableCaching | 开启基于注解的缓存 |
KeyGenerator | 缓存数据时key生成策略 |
serialize | 缓存数据时value序列化策略 |
注意:
- @Cacheable标注在方法上,表示方法的结果需要被缓存起来,缓存的键由keyGenerator的策略决定,缓存的值的形式则由serialize序列化策略决定(序列化还是json格式);标注上该注解之后,在缓存时效内再次调用该方法时不会调用方法本身而是直接从缓存获取结果
- @CachePut也标注在方法上,和@Cacheable相似也会将方法的返回值缓存起来,不同的是标注@CachePut的方法每次都会被调用,而且每次都会将结果缓存起来,适用于对象的更新
缓存初体验
-
新建项目,引入依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency>
Spring对缓存的支持功能在
spring-context
模块中,spring-boot-starter-web
模块已经依赖了spring-context
,所以我们不用再次引用。 -
数据准备和实体对象
SET FOREIGN_KEY_CHECKS=0; DROP TABLE IF EXISTS `department`; CREATE TABLE `department` ( `id` int(11) NOT NULL AUTO_INCREMENT, `departmentName` varchar(255) DEFAULT not NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; DROP TABLE IF EXISTS `employee`; CREATE TABLE `employee` ( `id` int(11) NOT NULL AUTO_INCREMENT, `lastName` varchar(255) DEFAULT NULL, `email` varchar(255) DEFAULT NULL, `gender` int(2) DEFAULT NULL, `d_id` int(11) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; SET FOREIGN_KEY_CHECKS=1;
@Data public class Employee { private Integer id; private String lastName; private String email; private Integer gender; //性别 1男 0女 private Integer dId; } @Data public class Department { private Integer id; private String departmentName; }
-
yml配置文件
server: port: 8080 spring: datasource: url: jdbc:mysql:///self_demo?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true&serverTimezone=Asia/Shanghai username: root password: root mybatis: configuration: map-underscore-to-camel-case: true # 开启驼峰命名 logging: level: com.szile.demo.cache.mapper: debug # 调高日志级别,打印SQL
-
创建对应的mapper接口
public interface EmployeeMapper { @Select("SELECT * FROM employee WHERE id = #{id}") public Employee getEmpById(Integer id); @Insert("INSERT INTO employee(lastName,email,gender,d_id) VALUES(#{lastName},#{email},#{gender},#{dId})") public void insertEmp(Employee employee); @Update("UPDATE employee SET lastName = #{lastName},email = #{email},gender = #{gender},d_id = #{dId} WHERE id " + "= #{id}") public void updateEmp(Employee employee); @Delete("DELETE FROM employee WHERE id = #{id}") public void deleteEmpById(Integer id); }
-
编写service
@Service public class EmployeeService { @Autowired private EmployeeMapper employeeMapper; @Cacheable(value = "emp") // 将方法运行的结果进行缓存,以后再获取相同的数据时,直接从缓存中获取,不再调用方法 public Employee getEmployeeById(Integer id) { return employeeMapper.getEmpById(id); } }
-
编写controller
@RestController public class EmployeeController { @Autowired private EmployeeService employeeService; @GetMapping("emp/{id}") public Employee getEmpById(@PathVariable Integer id) { return employeeService.getEmployeeById(id); } }
-
启动类
@EnableCaching // 开启基于注解的缓存支持 @MapperScan(value = {"com.szile.demo.cache.mapper"}) @SpringBootApplication public class CacheApplication { public static void main(String[] args) { SpringApplication.run(CacheApplication.class, args); } }
-
测试
浏览器发送两次请求http://localhost:8080/emp/1,发现控制台只在第一次请求时打印的SQL,第二次请求时没有SQL打印。
结论:缓存配置成功,使用了缓存。
@Cacheable注解的属性
@Cacheable
是将方法运行的结果进行缓存,以后在获取相同的数据时,直接从缓存中获取,不再调用方法。
@Cacheable注解的属性
属性名 | 描述 |
---|---|
cacheName/value | 指定缓存的名称,缓存使用CacheManage管理多个缓存组件Cache,Cache组件就是根据该名称进行区分的。对缓存的真 正CRUD操作在Cache中定义,每个缓存组件Cache都有自己唯一的 名字,通过cacheNames或者value属性指定,相当于是将缓存的键 值对进行分组,缓存的名字是一个数组,也就是说可以将一个缓存 键值对分到多个组里面。 |
key | 缓存数据时key的值,默认是使用方法参数的值,可以使用SpELl表达式计算key的值 |
keyGenerator | 缓存key的生成策略,和key二选一 |
cacheManager | 指定缓存管理器(如ConcurrentHashMap,Redis等) |
cacheResolver | 和cacheManager功能一样,二选一 |
condition | 指定缓存的条件(满足什么条件才缓存),可用SpELl表达式(如#id>0,表示当入参id大于0时才缓存) |
unless | 否定缓存,即满足unless指定的条件时,方法的结果不进行缓存, 使用unless时可以在调用的方法获取到结果之后再进行判断(如 #result==null,表示如果结果为null时不缓存) |
sync | 是否使用异步模式进行缓存 |
注:
- 即满足condition又满足unless条件的不进行缓存
- 使用异步模式进行缓存时(sync=true):unless条件将不被支持
SpEL表达式
名称 | 位置 | 描述 | 示例 |
---|---|---|---|
methodName | root object | 当前被调用的方法名 | #root.methodName |
method | root object | 当前被调用的方法 | #root.mehod.name |
target | root object | 当前被调用的目标对象 | #root.target |
targetClass | root object | 当前被调用的目标对象类 | #root.targetClass |
args | root object | 当前被调用的方法的参数列表 | #root.args[0] |
caches | root object | 当前方法调用使用的缓存列表(如@Cacheable(value={“cache1”,“cache2”})),则有两个cache | #root.caches[0].name |
argument name | evaluation context | 方法参数名,可以直接#参数名,也可以使用#p0或#a0的形式,0代表参数的索引 | #iban、#a0、#p0 |
result | evaluation context | 方法执行后的返回值(仅当方法 执行之后的判断有效, 如"unless","cache put"的表 达式,"cache evict"的表达式 beforeInvocation=false) | #result |
@Cacheable的运行流程
- 方法运行之前,先去查询Cache(缓存组件),按照cacheNames指定的名字获取(CacheManager先获取相应的缓存,第一次获取缓存如果没有Cache组件会自动创建)
- 去Cache中查找缓存的内容,使用的key默认就是方法的参数,key默认是使用keyGenerator生成的,默认使用的是SimpleKeyGenerator。
SimpleKeyGenerator生成key的默认策略:- 如果没有参数:key = new SimpleKey();
- 如果有一个参数:key = 参数的值
- 如果有多个参数:key = new SimpleKey(params);
- 没有查到缓存就调用目标方法
- 将目标方法返回的结果放进缓存中
总结:@Cacheable标注的方法在执行之前会先检查缓存中有没有这个数据,默认按照参数的值为key查询缓存,如果没有就运行方法并将结果放入缓存,以后再来调用时直接使用缓存中的数据。
核心:
- 使用CacheManager(ConcurrentMapCacheManager)按照名字得到
Cache(ConcurrentMapCache)组件 - key使用keyGenerator生成,默认使用SimpleKeyGenerator
@CachePut&@CacheEvict&@CacheConfig
@CachePut
-
说明:既调用方法又更新缓存数据。一般用于更新操作,在更新缓存时一定要和想要更新的缓存有相同的缓存名和相同的key。
-
运行时间:
- 先调用目标方法
- 将目标方法的结果缓存起来
-
示例
@CachePut(value = "emp", key = "#employee.id") public Employee updateEmp(Employee employee) { employeeMapper.updateEmp(employee); return employee; }
总结:
@CachePut
标注的方法总会被调用,且调用之后才将结果放入缓存,因此可以使用#result获取到方法的返回值。
@CacheEvict
-
说明:缓存清除,清除缓存时要指明缓存的名字和key,相当于告诉数据库要删除哪个表的哪条数据,key默认为参数的值
-
属性
- value/cacheNames:缓存的名字
- key:缓存的键
- allEntries:是否清除指定缓存中的所有键值对,默认为false,设置为true时会清除缓存中的所有键值对,与key属性二选一使用
- beforeInvocation:在@CacheEvict注解的方法调用之前清除指定缓存,默认为false,即在方法调用之后清除缓存,设置为true时则会在方法调用之前清除缓存(在方法调用之前还是之后清除缓存的区别在于方法调用时是否会出现异常,若不出现异常,这两种设置没有区别,若出现异常,设置为在方法调用之后清除缓存将不起作用,因为方法调用失败了)
-
示例
@CacheEvict(value = "emp", key = "#id", beforeInvocation = true) public void deleteById(Integer id) { employeeMapper.deleteEmpById(id); }
@CacheConfig
-
作用:标注在类上,抽取缓存相关注解的公共配置,可抽取的公共配置有缓存名,主键生成器等。
@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface CacheConfig { String[] cacheNames() default {}; String keyGenerator() default ""; String cacheManager() default ""; String cacheResolver() default ""; }
-
通过@CacheConfig的cacheNames属性指定缓存的名称之后,该类中的其他缓存注解就不必再写value或者cacheName了,会使用该名称作为value或者cacheName的值,当然也遵循就近原则
缓存自动配置源码剖析
在spring boot中所有的自动配置都是xxxAutoConfiguration,配置在META-INF/spring.factories文件的org.springframework.boot.autoconfigure.EnableAutoConfiguration配置下,在该配置下我们找到了CacheAutoConfiguration
。CacheAutoConfiguration
就是SpringBoot的缓存自动配置类。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zBDJCWrO-1625653766743)(SpringBoot缓存.assets\image-20210707164211015.png)]
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(CacheManager.class)
@ConditionalOnBean(CacheAspectSupport.class)
@ConditionalOnMissingBean(value = CacheManager.class, name = "cacheResolver")
@EnableConfigurationProperties(CacheProperties.class)
@AutoConfigureAfter({ CouchbaseAutoConfiguration.class, HazelcastAutoConfiguration.class,
HibernateJpaAutoConfiguration.class, RedisAutoConfiguration.class })
@Import({ CacheConfigurationImportSelector.class, CacheManagerEntityManagerFactoryDependsOnPostProcessor.class })
public class CacheAutoConfiguration {
// ... 省略
/**
* {@link ImportSelector} to add {@link CacheType} configuration classes.
*/
static class CacheConfigurationImportSelector implements ImportSelector {
@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;
}
}
// ... 省略
}
CacheAutoConfiguration
类中有一个静态内部类CacheConfigurationImportSelector
,它的selectImports
方法就是用来给容器添加缓存用到的组件。
配置的缓存组件
-
org.springframework.boot.autoconfigure.cache.GenericCacheConfiguration
-
org.springframework.boot.autoconfigure.cache.EhCacheCacheConfiguration
-
org.springframework.boot.autoconfigure.cache.HazelcastCacheConfiguration
-
org.springframework.boot.autoconfigure.cache.InfinispanCacheConfiguration
-
org.springframework.boot.autoconfigure.cache.JCacheCacheConfiguration
-
org.springframework.boot.autoconfigure.cache.CouchbaseCacheConfiguration
-
org.springframework.boot.autoconfigure.cache.RedisCacheConfiguration
-
org.springframework.boot.autoconfigure.cache.CaffeineCacheConfiguration
-
org.springframework.boot.autoconfigure.cache.SimpleCacheConfiguration // 默认使用
-
org.springframework.boot.autoconfigure.cache.NoOpCacheConfiguration
SimpleCacheConfiguration
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(CacheManager.class)
@Conditional(CacheCondition.class)
class SimpleCacheConfiguration {
@Bean
ConcurrentMapCacheManager cacheManager(CacheProperties cacheProperties,
CacheManagerCustomizers cacheManagerCustomizers) {
ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager();
List<String> cacheNames = cacheProperties.getCacheNames();
if (!cacheNames.isEmpty()) {
cacheManager.setCacheNames(cacheNames);
}
return cacheManagerCustomizers.customize(cacheManager);
}
}
SimpleCacheConfiguration
中创建一个ConcurrentMapCacheManager
的bean,是一个CacheManager。ConcurrentMapCacheManager
实现了CacheManager
接口。
看一下ConcurrentMapCacheManager
的getCache()方法
@Nullable
public Cache getCache(String name) {
Cache cache = (Cache)this.cacheMap.get(name);
if (cache == null && this.dynamic) {
synchronized(this.cacheMap) {
cache = (Cache)this.cacheMap.get(name);
if (cache == null) {
cache = this.createConcurrentMapCache(name);
this.cacheMap.put(name, cache);
}
}
}
return cache;
}
getCache()
方法使用了双重锁校验.
如果从cacheMap
没有获取到cache就会调用this.createConcurrentMapCache(name)
protected Cache createConcurrentMapCache(String name) {
SerializationDelegate actualSerialization = this.isStoreByValue() ? this.serialization : null;
return new ConcurrentMapCache(name, new ConcurrentHashMap(256), this.isAllowNullValues(), actualSerialization);
}
createConcurrentMapCache
方法中会创建一个ConcurrentMapCache
对象,这就是我们说的Cache
.
ConcurrentMapCache
类里有三个属性。
public class ConcurrentMapCache extends AbstractValueAdaptingCache {
private final String name;
private final ConcurrentMap<Object, Object> store;
@Nullable
private final SerializationDelegate serialization;
}
private final ConcurrentMap<Object, Object> store;
就是Entry 用来存放键值对;
基于Redis缓存实现
在缓存初体验的基础上进行修改。
-
引入Redis的starter
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
引入redis的starter之后,容器会加入redis相关的bean。
-
添加redis配置
spring: redis: host: localhost database: 1
-
修改实体类,实现
java.io.Serializable
接口使用redis存储的对象必须可实例化(实现Serializable接口),否则会报错。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-x8qt8i1V-1625653766750)(SpringBoot缓存.assets\image-20210707181000462.png)]
SpringBoot默认采用的是JDK的对象序列化方式,我们可以切换为使用JSON格式进行对象的序列化操作,这时需要我们自定义序列化规则(当然我们也可以使用Json工具先将对象转化为Json格式之后再保存至redis,这样就无需自定义序列化)
自定义RedisCacheManager
Redis默认序列化机制
Redis的自动配置类:RedisCacheConfiguration
类
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisConnectionFactory.class)
@AutoConfigureAfter(RedisAutoConfiguration.class)
@ConditionalOnBean(RedisConnectionFactory.class)
@ConditionalOnMissingBean(CacheManager.class)
@Conditional(CacheCondition.class)
class RedisCacheConfiguration {
@Bean
RedisCacheManager cacheManager(CacheProperties cacheProperties, CacheManagerCustomizers cacheManagerCustomizers,
ObjectProvider<org.springframework.data.redis.cache.RedisCacheConfiguration> redisCacheConfiguration,
ObjectProvider<RedisCacheManagerBuilderCustomizer> redisCacheManagerBuilderCustomizers,
RedisConnectionFactory redisConnectionFactory, ResourceLoader resourceLoader) {
RedisCacheManagerBuilder builder = RedisCacheManager.builder(redisConnectionFactory).cacheDefaults(
determineConfiguration(cacheProperties, redisCacheConfiguration, resourceLoader.getClassLoader()));
List<String> cacheNames = cacheProperties.getCacheNames();
if (!cacheNames.isEmpty()) {
builder.initialCacheNames(new LinkedHashSet<>(cacheNames));
}
redisCacheManagerBuilderCustomizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
return cacheManagerCustomizers.customize(builder.build());
}
private org.springframework.data.redis.cache.RedisCacheConfiguration determineConfiguration(
CacheProperties cacheProperties,
ObjectProvider<org.springframework.data.redis.cache.RedisCacheConfiguration> redisCacheConfiguration,
ClassLoader classLoader) {
return redisCacheConfiguration.getIfAvailable(() -> createConfiguration(cacheProperties, classLoader));
}
private org.springframework.data.redis.cache.RedisCacheConfiguration createConfiguration(
CacheProperties cacheProperties, ClassLoader classLoader) {
Redis redisProperties = cacheProperties.getRedis();
org.springframework.data.redis.cache.RedisCacheConfiguration config = org.springframework.data.redis.cache.RedisCacheConfiguration
.defaultCacheConfig();
config = config.serializeValuesWith(
SerializationPair.fromSerializer(new JdkSerializationRedisSerializer(classLoader)));
if (redisProperties.getTimeToLive() != null) {
config = config.entryTtl(redisProperties.getTimeToLive());
}
if (redisProperties.getKeyPrefix() != null) {
config = config.prefixKeysWith(redisProperties.getKeyPrefix());
}
if (!redisProperties.isCacheNullValues()) {
config = config.disableCachingNullValues();
}
if (!redisProperties.isUseKeyPrefix()) {
config = config.disableKeyPrefix();
}
return config;
}
}
从上述核心源码中可以看出,RedisCacheConfiguration内部同样通过Redis连接工厂RedisConnectionFactory定义了一个缓存管理器RedisCacheManager;同时定制RedisCacheManager时,也默认使用了JdkSerializationRedisSerializer序列化方式。如果想要使用自定义序列化方式的RedisCacheManager进行数据缓存操作,可以参考上述核心代码创建一个名为cacheManager的Bean组件,并在该组件中设置对应的序列化方式即可 。
自定义RedisCacheManager
在项目中创建Redis配置类
@Configuration
public class RedisConfig {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
// 分别创建String和JSON格式序列化对象,对缓存数据key和value进行转换
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer<Object> jsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
// 解决查询缓存转换异常问题
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jsonRedisSerializer.setObjectMapper(objectMapper);
RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofDays(1))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(stringRedisSerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jsonRedisSerializer))
.disableCachingNullValues();
RedisCacheManager cacheManager = RedisCacheManager.builder(redisConnectionFactory).cacheDefaults(configuration).build();
return cacheManager;
}
}
上述代码中,在RedisConfig配置类中使用@Bean注解注入了一个默认名称为方法名的
cacheManager组件。在定义的Bean组件中,通过RedisCacheConfiguration对缓存数据的key和value分别进行了序列化方式的定制,其中缓存数据的key定制为StringRedisSerializer(即String格式),而value定制为了Jackson2JsonRedisSerializer(即JSON格式),同时还使用entryTtl(Duration.ofDays(1))方法将缓存数据有效期设置为1天 。
完成基于注解的Redis缓存管理器RedisCacheManager定制后,可以对该缓存管理器的效果进行测试(使用自定义序列化机制的RedisCacheManager测试时,实体类可以不用实现序列化接口)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YRgEr8HM-1625653766757)(SpringBoot缓存.assets\image-20210707182741764.png)]