大数据、Java EE 学习资料请关注 B 站:https://space.bilibili.com/204792350

SpringBoot 2.x 基于简单的内存缓存组件的实验以及简单阐述原理

简介

对于一些临时性或者存储周期较短的数据,若是还是用数据库还存储的话,那就给数据增加不必要的访问负载,而且这些数据存储在硬盘中,若是经常新增删,产生的磁盘碎片会越多。

所以,有没有一种可以存储临时数据的技术呢?

其中,有一种技术就是缓冲,将一些短期的数据临时的存储内存中,需要的时候就可以读取,不需要的时候,可以删除,而且读写速度非常快;

例如变量这样的就是存储在内存,有一种方案是变量缓冲,这样依赖线程的生命周期,线程销毁,当前线程的内存区块自然就销毁。

只要在这周期内,变量一直都保存在内存中。

具体怎么做到变量缓冲呢?

很简单,例如说从磁盘中读入一个文件,但是文件中的数据无法直接使用,需要经过一些处理才能使用,所以经过处理之后的数据赋值给一个变量。而且,需要使用这份数据的地方不止一处,这时候就不能将变量销毁了,而是需要将其缓冲到内存中,这样后面不管任何地方使用,都能第一时间获取到处理过后的数据。

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 设置。

Snipaste_2020-05-31_21-18-37

实战使用

创建项目环境:

  • Web
  • MySQL
  • Mybatis
  • Cache

为了快捷演示,这次采用注解版 Mybatis 整合;

必须要的步骤,配置数据源、Mybatis 所需配置,JavaBean、Mapper 接口;

对于缓存组件来将,每一个组件都有一个唯一的名称,若是注解方式的,可以通过 @Cacheable 的 cacheNames 定义;

@Cacheable 属性

名称描述
cacheNames指定缓存组件的名称
cacheValue指定缓存组件的名称,作用同上
key缓存数据使用的 key,默认是使用方法参数的,可以编写 SpEl 语法从请求参数中获取指定值,或者从返回结果获取;
keyGeneratorkey 的生成器,可以指定 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 会过滤掉不满足条件的类型,剩下的就是满足的;

至于重复存在的话,这个有优先级之分;

以下面的图片为例,优先级由高到低;

Snipaste_2020-06-01_19-34-01

当然,这里的导入并不是全部都注册到容器中,而是在这个里面,需要用到这个列表来做判断,推断出当前生效的缓存管理器的配置类是哪一个。

效率低一些,可以一个个的点开,查看配置类生效需要哪些条件。

通过在配置文件中添加:

debug=true

可以将注册和非注册的组件打印在 console 中,这是 SpringBoot 启动的时候必做的工作;

Snipaste_2020-06-01_20-01-39

发现 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 到缓存组件里;从这一方面来讲,使用了缓存的目标不在是普通的反射调用,而是交给了缓存方面来执行,之后就是将结果返回;

    Snipaste_2020-06-01_22-18-02

上面说到 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 垃圾回收吗?

扩展

SpringBoot 支持 10 种缓存类型

名称描述
GENERIC使用上下文中的“缓存”bean进行通用缓存。
JCACHEJSR-107 规范
EHCACHEEhCache
HAZELCASTHazelcast
INFINISPANInfinispan
COUCHBASECouchbase
REDISRedis
CAFFEINECaffeine
SIMPLE简单的内存缓存
NONE没有缓存

支持的 SpEl 语法

名称位置描述示例
methodNameroot Object当前被调用的方法名#root.methodName
methodroot Object当前被调用的方法#root.method.name
targetroot Object当前被调用的目标对象#root.target
targetClassroot Object当前被调用的目标对象类#root.targetClass
argsroot Object当前被调用的方法的参数列表#root.args[0]
cachedroot Object当前方法使用的缓存列表,如 @Cacheable(value={"cache1","cache2"}),则有两个 cache#root.caches[0].name
argument nameevaluation context方法参数的名称,可以直接 #参数名,也可以使用 #p0 或者 #a0 的形式,0 代表索引#id、#a0、#p0
resultevaluation context方法执行后的返回值,仅当方法执行之后的判断有效,如 unless,cache put ,cache evict,beforeInvocation=false#result

几个重要概念和注解

名称描述
Cache缓冲接口,定义缓冲操作;实现有:RedisCache、EhCacheCache、ConcurrentMapCache 等缓存;
keyGenerator缓存数据时 key 生成策略
serialize缓存数据时 value 序列化策略
@Cacheable主要针对方法配置,能够根据方法的请求的参数对其结果进行缓存
@CachePut保证方法被调,又希望结果被缓存
@EnableCaching开启居于注解的缓存
@CacheEvict清空缓存
@Primark多个缓存管理器,需要使用这个注解将某一个管理器作为默认;
# Java   SpringBoot  

评论

公众号:mumuser

企鹅群:932154986

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×