引言
应用程序的数据除了可以放在配置文件中、数据库中以外,还会有相当一部分存储在计算机的内存中,这部分数据访问速度要快于数据库的访问,因此通常在做提升数据访问速度时,会将需要提升访问速度的数据放入到内存中,我们称之为缓存。
最常用的缓存方式是使用并发容器,因为具有比较高的并发性能,因此Spring的默认缓存策略就是使用ConcurrentHashMap作为缓存容器。下面将会逐步展开缓存的概念与Spring中的使用规则。
一、JSR-107缓存API
为了统一缓存的开发规范,以及提升系统的扩展性,J2EE发布了JSR-107缓存规范。主要定义了五大核心接口:
CachingProvider、CacheManager、Cache、Entry、Expiry
而实际开发中,我们通常会使用Spring缓存抽象来完成对缓存的操作,它是Spring为开发者定义的一套用于管理缓存的接口及相关实现。而Spring缓存抽象底层的概念与这五大接口的描述都是通用的,因此了解JSR-107定义的相关概念以及API接口描述,将有助于我们学习Spring的缓存抽象。
1.1 接口定义
1、CachingProvider:定义了创建、配置、获取、管理和控制多个CacheManager。一个应用可以在运行期访问多个CachingProvider。
2、CacheManager定义了创建、配置、获取、管理和控制多个唯一命名的Cache,这些Cache存在于CacheManager的上下文中。一个CacheManager仅被一个CachingProvider所拥有。
3、Cache:这是一个类似Map的数据结构并临时存储以Key为索引的值。一个Cache仅被一个CacheManager所拥有。
4、Entry:它是一个存储在Cache中的Key-Value对。
5、Expiry:每一个存储在Cache中的条目都有一个定义的有效期。一旦超过这个时间,条目为过期的状态。一旦过期,条目将不可访问、更新、删除。缓存有效期可以通过ExpiryPolicy设置。
1.2 接口关系图谱
二、Spring缓存抽象(以下重点)
2.1 Spring缓存接口
Spring从3.1开始定义了:org.springframework.cache.Cache 和 org.springframework.cache.CacheManager 接口来统一不同的缓存技术,并支持使用JCache(JSR-107)注解简化我们的开发。那么按照JSR-107的缓存思想,CacheManager就是用于管理Cache的,而Cache则是真正对缓存进行操作的抽象。
1、Cache接口是具体的缓存组件的规范定义,包含对缓存数据的各种操作;
2、Cache接口下Spring提供了各种xxxCache组件(实现类),如RedisCache,EhCacheCache,ConcurrentMapCache等。
每次调用需要缓存功能的方法时,Spring都会检查指定参数的指定目标方法是否已经被调用过。如果有就直接从缓存中获取方法调用后的结果;如果没有就调用方法并缓存结果后返回给用户,下次调用直接从缓存中获取。
使用Spring缓存抽象时我们需要注意以下两点:
1、确定哪些方法需要被缓存以及它们的缓存策略。
2、从缓存中读取之前缓存存储的数据。
2.2 Spring 缓存注解
最常用的缓存注解有如下:
@Cacheable主要针对方法配置,能够根据方法的请求参数对其结果进行缓存。
@CacheEvict清空缓存。用于标注在一些删除方法上。
@CachePut保证方法被调用,又希望结果被缓存。用于标注在一些更新方法上,更新缓存。
@EnableCaching开启基于注解的缓存。
2.3 缓存策略
keyGengerator缓存数据时key的生成策略。
serialize缓存数据时value的序列化策略。
三、Spring缓存快速入门
自进入Spring Boot时代,很多功能都已经不再需要繁杂的Java代码来实现,而是使用注解来完成相同的功能,下面将介绍如何使用注解的方式来完成一整套关于Spring默认缓存数据的操作。
3.1 开启基于注解的缓存功能
首先,如果希望使用注解的方式使用缓存,那么就需要开启基于注解的缓存功能。
具体方法是在Spring Boot的主程序上加上@EnableCaching注解:
@EnableJpaRepositories
@SpringBootApplication
@EnableCaching
public class CourseSystemApplication {public static void main(String[] args) {SpringApplication.run(CourseSystemApplication.class, args);}
}
3.2 自定义缓存组件
CacheManager管理多个Cache组件,对缓存的真正CRUD操作在Cache组件中,每一个缓存组件有自己唯一一个名字。因此,在使用注解定义缓存组件的时候需要指定一些属性:
cacheNames / value:指定缓存组件的名称,两个属性都是指定缓存名称,二选一。
key:缓存数据使用的key。默认是使用方法参数的值。而缓存的数据就是方法返回值。另外key也可以使用SpEL表达式来表示。
keyGenerator:key的生成器,与key属性二选一。
cacheManager:指定缓存管理器,或者cacheResolver指定缓存解析器。
condition:指定符合条件的情况下才缓存。
unless:否定缓存。当unless指定的条件为true,方法返回值就不会缓存。也可以获取到结果判断是否需要缓存,如: unless = “#result == null”,就代表如果结果是null就不缓存。
sync:是否使用异步模式进行缓存。注意!!sync属性默认为false,如果为true,则unless属性将不再支持。
附 SpEL表达式指定key的规则 :
3.3 代码示例
在查询全部课程的serviceImpl方法上使用缓存注解@Cacheable。
@Cacheable(cacheNames = "courses")
public List<Course> findAllCourses() { List<Course> allCourses = couseRep.findAll(); logger.info("查询全部课程 : " + allCourses); return allCourses;
}
查询效果 :
第一次查询,有SQL日志打印;第二次以后都没有SQL打印,说明缓存生效了。
问题描述 :
1、第一次在Service接口中标记了缓存注解,没有生效。原因很可能是因为当service组件自动注入的时候实则是实现类在真正执行操作,因此,只有在真正使用的组件上使用缓存才能够起作用。因此,以interface--Impl的形式开发的时候,要将缓存注解标记在具体实现类上,否则会失效。
2、给key属性赋值一个普通的字符串,报:SpelEvaluationException异常。因此这个key只能使用SpEl表达式来描述。像上面这种没有参数的情况,可以不必指定key属性,spring会默认为缓存数据生成一个key。
四、@Cacheable缓存工作原理
4.1 缓存组件的自动配置
缓存的相关配置来自于CacheAutoConfiguration类。
这个类使用@Import注解向容器中导入一个CacheConfigurationImportSelector的静态内部类,和其他自动配置时的导入选择器类似,它也是ImportSelector的实现类,这些实现类只有一个方法:String[] selectImports(...),专门用来导入具体的JavaConfig配置类。
而CacheConfigurationImportSelector,负责导入各种缓存组件的配置类。通过在selectImports内部打上断点debug启动项目的方式,我们可以一览返回值String[]中的内容:
[ 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
]
这些缓存配置,就是Spring缓存抽象的具体底层实现所需要用到的JavaConfig。
这些配置类内部都会有一些规则判断,@ConditionalXxx来判断是否生效。以第一个GenericCacheConfiguration为例:
@Configuration
@ConditionalOnBean(Cache.class)
@ConditionalOnMissingBean(CacheManager.class)
@Conditional(CacheCondition.class)
class GenericCacheConfiguration {private final CacheManagerCustomizers customizers;GenericCacheConfiguration(CacheManagerCustomizers customizers) {this.customizers = customizers;}@Beanpublic SimpleCacheManager cacheManager(Collection<Cache> caches) {SimpleCacheManager cacheManager = new SimpleCacheManager();cacheManager.setCaches(caches);return this.customizers.customize(cacheManager);}
}
可以看到类头上,相关的@Conditional注解来表明这个配置类在哪种情况下生效。但是我们也可以使用spring boot的自动配置报告来查看究竟是哪个缓存配置类生效。
4.2 SimpleCacheConfiguration
通过在全局配置文件中设置debug=true,使用spring boot的自动配置报告功能,打印匹配的JavaConfig配置类,我们可以看到默认启用的缓存配置是SimpleCacheConfiguration:
打开这个配置类:
/*** Simplest cache configuration, usually used as a fallback.** @author Stephane Nicoll* @since 1.3.0*/
@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;}@Beanpublic ConcurrentMapCacheManager cacheManager() {ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager();List<String> cacheNames = this.cacheProperties.getCacheNames();if (!cacheNames.isEmpty()) {cacheManager.setCacheNames(cacheNames);}return this.customizerInvoker.customize(cacheManager);}
}
这个配置类通过@Bean向容器中注册了一个ConcurrentMapCacheManager对象,这个CacheManager可以获取和创建ConcurrentMapCache类型的缓存组件。
4.3 缓存执行流程(重点)
使用@Cacheable时:
1、在方法service方法第一次执行前,先去查询Cache(缓存组件)。它是按照cacheNames指定的名字调用CacheManager中的getCache(String name)方法获取的。
@Override
public Cache getCache(String name) {Cache cache = this.cacheMap.get(name);if (cache == null && this.dynamic) {synchronized (this.cacheMap) {cache = this.cacheMap.get(name);if (cache == null) {cache = createConcurrentMapCache(name);this.cacheMap.put(name, cache);}}}return cache;
}
当第一次执行时未找到指定缓存,那么就会去调用createConcurrentMapCache(name)创建这个缓存,并将key-value放入到缓存中。
2、去Cache中查找缓存,使用一个key,这个key默认是使用SimpleKeyGenerator生成key。
SimpleKeyGengerator生成key的默认策略:
/*** Generate a key based on the specified parameters.*/
public static Object generateKey(Object... params) {if (params.length == 0) {return SimpleKey.EMPTY;}if (params.length == 1) {Object param = params[0];if (param != null && !param.getClass().isArray()) {return param;}}return new SimpleKey(params);
}
如果没有参数,那么key就是用一个SimpleKey对象;如果有一个参数,则key = 参数的值;
如果有多个参数,key = new SimpleKey(params);
3、没有查找到缓存就调用目标方法。
4、将目标方法返回的结果,放入缓存中。
总结 :
由@Cacheable标注的方法执行之前先来检查缓存中有没有这个数据,默认按照参数的值作为key去查询,如果没有就运行方法,并将结果放入缓存,以后再调用就可以直接是用缓存中的数据。
核心 :
1、是用CacheManager(默认ConcurrentMapCacheManager)按照名字得到Cache(默认ConcurrentMapCache)组件。
2、key使用keyGenerator生成的,默认的是SimpleKeyGenerator。
五、自定义keyGenerator
自定义的key有两种实现手段,一种是通过@Cacheable的key属性,另一个是通过keyGenerator属性。
key属性使用的是SpEL表达式来指定key的格式规则,如:key = “#root.methodName+’[’+#id+’]’”,但是一种比较零散的自定义策略。
而keyGenerator可以指定一个通用的缓存key的生成策略。在这里简单说一下代码实现。
@Configuration
public class SysCacheKeyGenerator {/*** 课程缓存key生成策略*/@Bean("courseDefaultKeyGenerator")public KeyGenerator courseCacheKeyGenerator() {return new KeyGenerator() {@Overridepublic Object generate(Object target, Method method, Object... params) {String key = params.length == 0 ? "courseList" : params.toString();return method.getName() + "." + key;}};}
}
使用这段代码来定义一个自定义的keyGenerator,并注册到容器中,然后配置@Cacheable的keyGenerator属性:
@Override
@Cacheable(cacheNames = "courses", keyGenerator = "courseDefaultKeyGenerator")
public List<Course> findAllCourses() {List<Course> allCourses = couseRep.findAll();logger.info("查询全部课程 : " + allCourses);return allCourses;
}
那么实际缓存时就会使用我们自定义的key:
六、设置缓存条件
@Cacheable注解,
condition属性可以动态的判断数据是否满足缓存条件。
如:condition = “#id > 1”,意思是判断参数id的值大于1的情况下才对结果缓存。
那么它的用法主要依赖于SpEl表达式的逻辑判断。
unless属性的意思是“不缓存”,同样是通过SpEl表达式来判断true或false,如果unless的条件成立,那么将不会对数据进行缓存。
如:unless = “#id == 2” ,意思是如果id的值为2,那么就不缓存结果了。
七、@CachePut
@CachePut注解意为更新缓存。
一般标注在涉及到数据库更新操作的方法上,在更新数据库中记录的同时也会更新缓存中对应的数据。
运行实际 :
1、先调用目标方法
2、将目标方法的结果缓存起来。
注意 !!!
在使用@CachePut的时候要注意缓存数据的key要与查询该缓存数据时所用的key保持一致,否则,两个key如果不一致,就会出现查询方法依然是缓存更新之前的数据。
这里key可以使用#result来取得返回结果,及其内部属性如#result.id,但是@Cacheable不能使用#result来取得结果值,想想这是为什么?(提示,执行时机)
八、@CacheEvict
@CacheEvict :清除缓存。
key:指定要清除的缓存。
allEntries = true 代表清除这个缓存中的所有数据。
beforeInvocation = false 代表清除缓存的操作是在方法执行后,这也是默认值。如果出现异常,那么就不会清除缓存。该属性如果为true,那么就代表在方法执行前清除缓存,无论方法是否出现异常。
九、@Caching与@CacheConfig
@Caching是一个复杂缓存的注解,可以指定多种缓存规则;@CacheConfig放在类头上,用于抽取一些公共的缓存属性配置,如可以指定公共的缓存名称。
总结
关于Spring默认缓存组件的使用,基本就是@Cacheable、@CachePut、@CacheEvict这三个注解,另外注意在使用它们之前,记得在Spring Boot的程序主类上开启@EnableCaching。
另外,一定要注意@Cacheable和@CachePut的执行时机,以及它们真正操作的key值,因为这个缓存数据是存储在内存中的,因此不像数据库中的数据那样直观明了,操作缓存中的数据要求开发者有比较好的数据管理规则,否则,很容易出现缓存数据失效的问题。
综上,就是关于Spring Boot使用默认缓存管理器的缓存数据的基本操作方法和基本工作原理,欢迎文末留言。