简介
对于一些临时性或者存储周期较短的数据,若是还是用数据库还存储的话,那就给数据增加不必要的访问负载,而且这些数据存储在硬盘中,若是经常新增删,产生的磁盘碎片会越多。
所以,有没有一种可以存储临时数据的技术呢?
其中,有一种技术就是缓冲,将一些短期的数据临时的存储内存中,需要的时候就可以读取,不需要的时候,可以删除,而且读写速度非常快;
例如变量这样的就是存储在内存,有一种方案是变量缓冲,这样依赖线程的生命周期,线程销毁,当前线程的内存区块自然就销毁。
只要在这周期内,变量一直都保存在内存中。
具体怎么做到变量缓冲呢?
很简单,例如说从磁盘中读入一个文件,但是文件中的数据无法直接使用,需要经过一些处理才能使用,所以经过处理之后的数据赋值给一个变量。而且,需要使用这份数据的地方不止一处,这时候就不能将变量销毁了,而是需要将其缓冲到内存中,这样后面不管任何地方使用,都能第一时间获取到处理过后的数据。
public class HandleData {
private List<?> list;
public List<?> handle() {
List<?> temp = new ArrayList<>();
// ... 经过处理之后,将数据赋值到属性上面
list = temp;
return temp;
}
public List<?> getList() {
if (list == null) {
list = handle();
}
return list;
}
}
简单的一段代码就实现了变量重复利用。
JSR 107
Java Caching 定义了 5 个核心接口,分别是 CachingProvider、CacheManager、Cache、Entry、Expiry;
- CachingProvider:定义了创建、配置、获取、管理和控制多个 CacheManager;一个应用可以在运行期间访问多个 CachingProvider;
- CacheManager:定义了创建、配置、获取、管理和控制多个唯一命名的 Cache,这些 Cache 存在于 CacheManager 的上下文中。一个 CacheManager 仅被一个 Caching 所拥有。
- Cache:是一个类似 Map 的数据结构并临时存储以 Key 为索引的值;一个 Cache 仅被一个 CacheManager 所拥有;
- Entry:一个存储在 Cache 中的 key-value 对;
- Expiry:每一个存储在 Cache 中的条目有一个定义的有效期;一旦超过这个有效期,条目为过期的状态;一旦过期,条目将不可访问、更新、删除;缓冲有效期可以通过 ExpiryPolicy 设置。
实战使用
创建项目环境:
- Web
- MySQL
- Mybatis
- Cache
为了快捷演示,这次采用注解版 Mybatis 整合;
必须要的步骤,配置数据源、Mybatis 所需配置,JavaBean、Mapper 接口;
对于缓存组件来将,每一个组件都有一个唯一的名称,若是注解方式的,可以通过 @Cacheable 的 cacheNames 定义;
@Cacheable 属性
名称 | 描述 |
---|---|
cacheNames | 指定缓存组件的名称 |
cacheValue | 指定缓存组件的名称,作用同上 |
key | 缓存数据使用的 key,默认是使用方法参数的,可以编写 SpEl 语法从请求参数中获取指定值,或者从返回结果获取; |
keyGenerator | key 的生成器,可以指定 key 的生成器的 bean id,与 key 属性二选一 |
cacheManager | 指定缓存管理器 |
cacheResolver | 指定缓存解析器,同上作用 |
condition | 只当符合条件的情况才缓存 |
unless | 否定缓存,当 unless 指定添加满足时 true 时,方法的返回值就不会被缓存,可以获取结果进行判断; unless = "#result == null" |
sync | 是否使用异步模式 |
步骤:
- 开启注解:@EnableCaching,这个注解标注在主程序类上面
- 标注缓存注解
示例:
@EnableCaching
@SpringBootApplication
public class CacheApplication {
public static void main(String[] args) {
SpringApplication.run(CacheApplication.class, args);
}
}
// ======================================================
@Cacheable(cacheNames = "department", key = "#id")
public Department getDeparment(Integer id) {
return departmentMapper.getDepartment(id);
}
想要测试有没有缓存,可以将日志输出出来
logging.level.com.huasio.mapper=true
正常情况,访问一次会往数据查询的 SQL,而加了注解之后,则第一次需要访问数据库读取数据,后面都不会在数据库查询,而是从缓存数据读取;
@CachePut
对缓存目标进行更新,简单来说就是,调用方法的同时更新缓存,这个更新与 @Cacheable 不同,该注解是先调用方法,然后写入缓存,更新的同时也是创建;
@Cacheable 是创建缓存,在调用缓存目标之前;
测试一下:
@CachePut(cacheNames = "department", key = "#result.id")
public Department updateDepartment(Department department) {
departmentMapper.updateDepartment(department);
return department;
}
一旦该方法被调用,则同步更新缓存数据。
但是,需要注意一点:缓存 key 的名称一定要相同,否则等同于新创建一个缓存;
而且,该方法的好处就是,再次获取的数据就是最新的,不需要从数据库中获取;
@CacheEvict
该注解可以指定的缓存数据清除,用法很简单,和上面的两种注解雷同:
@CacheEvict(cacheNames = "department", key = "#id")
唯一需要注意的就是 key 一定要与删除的目标一致;
@CacheConfig、@Caching
@CacheConfig:可以标注在类上面,代表着一个类的公共信息,可以对一个类一些基本信息统一指定;
@Caching:这个注解时前面三个:@Cacheable、@CachePut、@CacheEvict 的整合版,同时可以生效;
编码方式
除了使用注解之外,还有可以直接注入缓存管理器到类中,使用管理来手动缓存;
public class TestManual {
@Autowired
RedisCacheManager redisCache;
public void test() {
Cache cache = redisCacheManager.getCache("department");
cache.put("test", "这是手动缓存数据!");
}
}
这样方式就弥补了注解的缺点,无法缓存方法内某个变量的数据;
工作原理
按照常态,Cache 相关的功能必定有一个自动配置类来专门给容器中注册组件,加载各种配置信息;
CacheAutoConfiguration 这个自动配置类里面有一个方法,负责导入缓存管理器的配置类;
@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;
}
默认导入哪些缓存管理器的配置类呢?这个有根据的。
在 CacheType 枚举类里面定义了 10 类型,其实准确来说是 9 类,另外一类是不缓存;
实际上,CacheConfigurations 这个类里面将缓存类型与缓存管理器绑定到一起,这个 map 是不可更改的;所以,selectImport 会过滤掉不满足条件的类型,剩下的就是满足的;
至于重复存在的话,这个有优先级之分;
以下面的图片为例,优先级由高到低;
当然,这里的导入并不是全部都注册到容器中,而是在这个里面,需要用到这个列表来做判断,推断出当前生效的缓存管理器的配置类是哪一个。
效率低一些,可以一个个的点开,查看配置类生效需要哪些条件。
通过在配置文件中添加:
debug=true
可以将注册和非注册的组件打印在 console 中,这是 SpringBoot 启动的时候必做的工作;
发现 SpringBoot 默认加载的缓存配置类是: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);
}
}
所以,SpringBoot 默认的缓存管理器就是 ConcurrentMapCacheManager 这个,而这个管理器使用的容器是 map,一个 ConcurrentMap 安全线程的接口实现类作为容器;
private final ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap<>(16);
缓存的生效的步骤
-
首次访问缓存目标,根据缓存名称从缓存管理器获取缓存组件,看看是否已经加载过了,若是没有的话则根据缓存名称,创建缓存组件,默认组件:ConcurrentMapCache
-
创建缓存目标的缓存组件之后,接下来会尝试从所有缓存组件中(可以指定多个缓存组件)查找缓存 key 的值,若是没有设置 key 属性,则默认根据请求参数作为缓存组件名称作为 key,默认 key 是使用 SimpleKey 生成出来的;生成策略看下方;
-
若是没有找到缓存,则会使用动态代理的方式,执行目标方法,以获取返回值将其 put 到缓存组件里;从这一方面来讲,使用了缓存的目标不在是普通的反射调用,而是交给了缓存方面来执行,之后就是将结果返回;
上面说到 key 是生成出来的,那么是如何生成的呢?按照什么策略?
有两种判断:
- 设置 key 属性,根据方法和注解上面的 key 属性表达式创建一个 SpEl 表达式对象,然后解析出表达式的值做 key
- 非设置 key 属性,默认的生成方案:
- 若是没有请求参数,则 SimpleKey 空对象作为 key
- 若是有一个请求参数,则保证不是数组的情况下,作为 key
- 若是以上条件都不满足,则将参数作为 SimpleKey 对象的参数,实
定义 KeyGenerator 生成器
@Cacheable 里面有个 KeyGenerator 属性,可以指定自定义生成器的 Bean id;
接下来尝试自定义一个生成器,来生成 key;
实现 KeyGenerator 接口即可,只有一个方法,该方法返回的就是 key;
需要注意, KeyGenerator 这个接口不要导错;是这个包:org.springframework.cache.interceptor.KeyGenerator
@SpringBootConfiguration
public class CustomGenerator {
@Bean("CustomGenerator")
public KeyGenerator keyGenerator() {
return (target, method, params) -> {
System.out.println(target.toString());
System.out.println(method.getName());
System.out.println(Arrays.toString(params));
return method.getName() + "[" + Arrays.toString(params) + "]";
};
}
}
使用的话,就在 keyGenerator 属性直接填入 bean id 即可;
问题
- 缓存如何生效的呢?为何加了注解之后,就能使用缓存。
- 使用缓存之后,如何保证目标方法只调用一次,不会调用两次,那么是怎么做到的?由缓存组件调用还是 SpringBoot 的反射执行?
- 如何使用缓存?
- 如何自定义缓存管理器?
- 为何缓存能一直保留着,是怎么做到的?它是如何保证缓存的数据不会随着一次请求线程的销毁而清空缓存,它处于程序的什么位置?
- 缓存的数据会被 Java 虚拟机的 GC 垃圾回收吗?
扩展
- JSR107 Caching:https://jcp.org/en/jsr/detail?id=107
SpringBoot 支持 10 种缓存类型
名称 | 描述 |
---|---|
GENERIC | 使用上下文中的“缓存”bean进行通用缓存。 |
JCACHE | JSR-107 规范 |
EHCACHE | EhCache |
HAZELCAST | Hazelcast |
INFINISPAN | Infinispan |
COUCHBASE | Couchbase |
REDIS | Redis |
CAFFEINE | Caffeine |
SIMPLE | 简单的内存缓存 |
NONE | 没有缓存 |
支持的 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] |
cached | root Object | 当前方法使用的缓存列表,如 @Cacheable(value={"cache1","cache2"}),则有两个 cache | #root.caches[0].name |
argument name | evaluation context | 方法参数的名称,可以直接 #参数名,也可以使用 #p0 或者 #a0 的形式,0 代表索引 | #id、#a0、#p0 |
result | evaluation context | 方法执行后的返回值,仅当方法执行之后的判断有效,如 unless,cache put ,cache evict,beforeInvocation=false | #result |
几个重要概念和注解
名称 | 描述 |
---|---|
Cache | 缓冲接口,定义缓冲操作;实现有:RedisCache、EhCacheCache、ConcurrentMapCache 等缓存; |
keyGenerator | 缓存数据时 key 生成策略 |
serialize | 缓存数据时 value 序列化策略 |
@Cacheable | 主要针对方法配置,能够根据方法的请求的参数对其结果进行缓存 |
@CachePut | 保证方法被调,又希望结果被缓存 |
@EnableCaching | 开启居于注解的缓存 |
@CacheEvict | 清空缓存 |
@Primark | 多个缓存管理器,需要使用这个注解将某一个管理器作为默认; |
Q.E.D.
Comments | 0 条评论