长大后想做什么?做回小孩!

0%

SpringBoot与缓存

为Redis整合做好准备

JSR107缓存规范

Java Caching定义了5个核心接口,分别是CachingProvider, CacheManager, Cache, EntryExpiry

  • CachingProvider定义了创建、配置、获取、管理和控制多个CacheManager。一个应用可以在运行期访问多个CachingProvider。
  • CacheManager定义了创建、配置、获取、管理和控制多个唯一命名的Cache,这些Cache存在于CacheManager的上下文中。一个CacheManager仅被一个CachingProvider所拥有。
  • Cache是一个类似Map的数据结构并临时存储以Key为索引的值。一个Cache仅被一个CacheManager所拥有。
  • Entry是一个存储在Cache中的key-value对。
  • Expiry 每一个存储在Cache中的条目有一个定义的有效期。一旦超过这个时间,条目为过期的状态。一旦过期,条目将不可访问、更新和删除。缓存有效期可以通过ExpiryPolicy设置。

uOY5zq.png

CacheManager管理多个Cache组件的,真正对缓存的CRUD操作在Cache组件中,每一个缓存组件有自己唯一一个名字。

Spring缓存抽象

Spring从3.1开始定义了org.springframework.cache.Cache和org.springframework.cache.CacheManager接口来统一不同的缓存技术,并支持使用JCache(JSR-107)注解简化我们开发。

  • Cache接口是缓存组件的规范定义,包含缓存的各种操作集合,Cache接口下Spring提供了各种xxxCache的实现;如RedisCache,EhCacheCache , ConcurrentMapCache等等。。。
  • 每次调用需要缓存功能的方法时,Spring会检查检查指定参数的指定的目标方法是否已经被调用过;如果有就直接从缓存中获取方法调用后的结果,如果没有就调用方法并缓存结果后返回给用户。下次调用直接从缓存中获取。

uOYoQ0.png

使用Spring缓存抽象时我们需要关注以下两点:

  1. 确定方法需要被缓存以及他们的缓存策略。
  2. 从缓存中读取之前缓存存储的数据。

SpringBoot缓存工作原理

浅读源码:

  1. 首先肯定是从自动配置入手,如果还不了解SpringBoot自动配置原理的同学可以参考之前的博文《SpringBoot自动配置》,按照自动配置的原理来说,应该有一个CacheAutoConfiguration的类:

    u7wshR.png

  2. 这个类上有一个注解@Import导入了一个CacheConfigurationImportSelector内部类,这个内部类有一个方法:

    1
    2
    3
    4
    5
    6
    7
    8
    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;
    }

    return前打上断点,debug一下发现:

    u7w074.png

    不少名字都很眼熟JCache(JSR-107)、EhCache、Redis。。。

  3. 这么多的配置类,要用哪个呢?例如GenericCacheConfiguration配置类上:

    u7wDAJ.png

    配置类上都注解了各种条件,条件满足时才会匹配该配置类。

    可以在配置文件中加入debug=true,启动时就会加载自动配置报告:

    u7wUXT.png

    这里就是默认情况:SimpleCacheConfiguration配置类会匹配并生效。(如果引入Redis依赖并使用了,这里应该匹配的就是RedisCacheConfiguration。)

  4. SimpleCacheConfiguration配置类会给容器注册一个CacheManager[ConcurrentMapCacheManager]:

    u7wrN9.png

  5. ConcurrentMapCacheManager可以获取和创建ConcurrentMapCache类型的缓存组件,缓存组件会将数据保存在ConcurrentMap中。

@Cacheable运行流程:


本篇仅以@Cacheable为例,简述其注解的运行流程:

  1. 方法运行之前,先查询Cache(缓存组件),按照cacheNames属性指定的名字获取。CacheManager先获取相应的缓存,第一次获取缓存如果没有,Cache组件会自动创建。
  2. 使用一个key去Cache中查找缓存的内容,这个key默认就是方法的参数,使用keyGenerator生成的。上例中,默认使用SimpleKeyGenerator生成key,SimpleKeyGenerator生成key的默认策略:
    • 如果没有参数:key=new SimpleKey();
    • 如果有一个参数:key=参数的值
    • 如果有多个参数:key=new SimpleKey(params);
  3. 没有查到缓存就调用目标方法。
  4. 将目标方法返回的结果,放进缓存中。

@Cacheable标注的方法执行之前,首先来检查缓存中有没有这个数据,默认按照参数的值作为key去查询缓存,如果没有数据就运行方法并将结果放入缓存,以后再来调用就可以直接使用缓存中的数据。

总结:

  1. 使用CacheManager[ConcurrentMapCacheManager]按照名字得到Cache[ConcurrentMapCache]组件。
  2. 用keyGenerator生成key,默认是SimpleKeyGenerator。

常用的注解:

u7wdnU.png

@Cacheable:

使用注解时,至少要指定一个cacheNames属性,不然抛异常。

@Cacheable注解是在目标方法执行之前生效。

  • cacheNames/value:指定缓存组件的名字;将方法的返回值放在哪个缓存中,是数组的方式,可以指定多个缓存。
  • key:缓存数据使用的key;可以用它来指定。默认是使用方法参数的值。这里可以使用SpEL。
  • keyGenerator: key的生 成器;可以自己指定key的生成器的组件id,但是key/keyGenerator:二选一使用。
  • cacheManager:指定缓存管理器:或者cacheResolver指定获取解析器
  • condition:指定符合条件的情况下才缓存:
  • unless:否定缓存;当unless指定的条件为true,方法的返回值就不会被缓存,可以获取到结果进行判断unless=”#result == null”。
  • sync:是否使用异步模式

@CachePut

既调用方法,又更新缓存数据,同步更新缓存。@CachePut注解是在目标方法执行之后起作用的。

看一个例子并思考:

controller、dao层代码省略,主要是看service代码:

1
2
3
4
5
6
7
8
9
10
11
12
@Override
@Cacheable(value = "user")
public User selectUserById(Integer id) {
return userDao.selectUserById(id);
}

@Override
@CachePut(value = "user")
public User updateUser(User user) {
userDao.updateUser(user);
return user;
}
  1. 首先根据id=133查询请求,因为是第一次查询该数据,所以请求数据库获取结果,并将结果存入缓存,再次发起同一个请求,控制台显示没有访问数据库,而是直接从缓存中取出数据。
  2. 发起根据id=133更新请求,查看数据库数据更新成功,因为有@CachePut注解,所以更新后的数据,应该也同步更新到了缓存中。
  3. 为了验证缓存中数据是否同步更新了,再次发起根据id=133查询请求,从控制台看出依然没有访问数据库而是从缓存中获取了数据,但是,数据仍然是没更新前的!!!

难道@CachePut注解没有同步更新缓存么?

原因:查询方法和更新方法的缓存操作注解中都没有指定key,所以是使用默认的key生成策略(方法参数列表作为key),因此查询操作时缓存中存入的是:key是id(133),value是方法返回的User对象。更新操作时缓存了:key是参数user对象,value是方法返回的User对象。这就是为什么查询id=133的数据时发现“缓存没有同步更新”的假象。

解决方法:

可以在注解中用SpEl指定key,使两次缓存的数据的key保持一致。

1
2
3
4
5
6
7
8
9
@Cacheable(value = "user",key = "#id")
public User selectUserById(Integer id) {
return userDao.selectUserById(id);
}
@CachePut(value = "user",key = "#user.id")
public User updateUser(User user) {
userDao.updateUser(user);
return user;
}

重新进行上述中的验证步骤,发现缓存中同步更新了。

@CacheEvict:

清除缓存,依然可以需要用value指定缓存组件名和key指定要清除的数据。

allEntries:该属性默认是false,如果该属性设置为true,会清除指定缓存组件中的所有数据,就不需要指定key了。

beforeInvocation:该属性默认是false,代表缓存的清除工作在方法执行之后进行,如果方法出现异常,则缓存就不会清除。如果将该属性设置为true,缓存的清除工作在方法执行之前进行,即使方法出现异常,缓存依然会清除。

@Caching

1
2
3
4
5
6
7
8
9
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Caching {
Cacheable[] cacheable() default {};
CachePut[] put() default {};
CacheEvict[] evict() default {};
}

该注解是@Cacheable、@CachePut和@CacheEvict三个注解的合体,当一些缓存的规则比较复杂时可以使用此注解。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
@Caching(
cacheable = {
@Cacheable(value = "user",key = "#username")
},
put = {
@CachePut(value = "user",key = "#result.id"),
@CachePut(value = "user",key = "#result.address")
}
)
public User selectUserByUsername(String username) {
return userDao.selectUserByUsername(username);
}

当该方法执行之后,方法返回值会分别以id、address、username作为key进行缓存,当其他带有缓存操作的方法以id或者addresss再次查询该条数据时,就不需要再访问数据库了。但是,如果再次调用该方法以username进行查询时,依然会访问数据库,因为存在@CachePut注解,无论缓存中有无数据都会执行方法。

@CacheConfig

1
2
3
4
5
6
7
8
9
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CacheConfig {
String[] cacheNames() default {};
String keyGenerator() default "";
String cacheManager() default "";
String cacheResolver() default "";
}

之前使用缓存注解时每次都要分别指定value、keyGenerator、cacheManager、cacheResolver,这样是很麻烦而且不科学的。于是@CacheConfig就可以排上用场了。

在类上加@CacheConfig并设置上述四个属性,则方法中就不需要再进行指定了。