JavaSpringBootCache
说到Spring Boot缓存,那就不得不提JSR-107规范,它说明在Java中如何规范地使用缓存。
JSR是Java Specification Requests的简称,通常译为”Java 规范提案“。具体而言,是指向JCP(Java Community Process,Java标准制定组织)提出新增一个标准化技术规范的正式请求。任何人都可以提交JSR,通过一定的标准测试后,就可以向Java平台增添新的API和服务。JSR已成为Java界的一个重要标准。
JSR-107规范即JCache API,JCache规范定义了一种对Java对象临时在内存中进行缓存的方法,包括对象的创建、共享访问、假脱机(spooling)、失效、各JVM的一致性等,可被用于缓存JSP内最经常读取的数据,如产品目录和价格列表。利用JCache的缓存数据,可以加快大多数查询的反应时间(内部测试表明反应时间大约快15倍)。

一、JCache

在Spring Boot中使用缓存之前,有必要了解一下JCacheJCache定义了五个核心接口,分别是CachingProviderCacheManagerCacheEntryExpiry

  • 一个CachingProvider可以创建和管理多个**CacheManager**,并且一个CacheManager只能被一个CachingProvider所拥有,而一个应用可以在运行期间访问多个CachingProvider
  • 一个CacheManager可以创建和管理多个唯一命名的**Cache**,且一个Cache只能被一个CacheManager所拥有,这些Cache存在于CacheManager的上下文中。
  • Cache是一个类似Map的数据结构
  • Entry是一个存储在Cache中的key-value对
  • Expiry是指存储在Cache中的Entry的有效期,一旦超过这个时间,Entry将处于过期状态,即不可访问、更新和删除。缓存有效期可以通过ExpiryPolicy设置。

Spring Cache缓存原理与Redis实践 - 图1

二、Spring Cache原理

Spring 3.1开始,引入了Spring Cache,即Spring 缓存抽象。通过定义org.springframework.cache.Cacheorg.springframework.cache.CacheManager接口来统一不同的缓存技术,并支持使用JCache注解简化开发过程。
Cache接口为缓存的组件规范定义,包含缓存的各种操作集合。Spring中为Cache接口提供了各种xxxCache的实现:RedisCacheEhCacheCacheConcurrentMapCache等。
通过部分源码详细了解一下Cache接口和CacheManager接口。

Cache接口

  1. public interface Cache {
  2. //Cache名称
  3. String getName();
  4. //Cache负责缓存的对象
  5. Object getNativeCache();
  6. /**
  7. * 获取key对应的ValueWrapper
  8. * 没有对应的key,则返回null
  9. * key对应的value是null,则返回null对应的ValueWrapper
  10. */
  11. @Nullable
  12. Cache.ValueWrapper get(Object key);
  13. //返回key对应type类型的value
  14. @Nullable
  15. <T> T get(Object key, @Nullable Class<T> type);
  16. //返回key对应的value,没有则缓存Callable::call,并返回
  17. @Nullable
  18. <T> T get(Object key, Callable<T> valueLoader);
  19. //缓存目标key-value(替换旧值),不保证实时性
  20. void put(Object key, @Nullable Object value);
  21. //插入缓存,并返回该key对应的value;先调用get,不存在则用put实现
  22. @Nullable
  23. default Cache.ValueWrapper putIfAbsent(Object key, @Nullable Object value) {
  24. Cache.ValueWrapper existingValue = this.get(key);
  25. if (existingValue == null) {
  26. this.put(key, value);
  27. }
  28. return existingValue;
  29. }
  30. //删除缓存,不保证实时性
  31. void evict(Object key);
  32. //立即删除缓存:返回false表示剔除前不存在制定key活不确定是否存在;返回true,表示该key之前存在
  33. default boolean evictIfPresent(Object key) {
  34. this.evict(key);
  35. return false;
  36. }
  37. //清除所有缓存,不保证实时性
  38. void clear();
  39. //立即清楚所有缓存,返回false表示清除前没有缓存或不能确定是否有;返回true表示清除前有缓存
  40. default boolean invalidate() {
  41. this.clear();
  42. return false;
  43. }
  44. public static class ValueRetrievalException extends RuntimeException {
  45. @Nullable
  46. private final Object key;
  47. public ValueRetrievalException(@Nullable Object key, Callable<?> loader, Throwable ex) {
  48. super(String.format("Value for key '%s' could not be loaded using '%s'", key, loader), ex);
  49. this.key = key;
  50. }
  51. @Nullable
  52. public Object getKey() {
  53. return this.key;
  54. }
  55. }
  56. //缓存值的一个包装器接口,实现类为SimpleValueWrapper
  57. @FunctionalInterface
  58. public interface ValueWrapper {
  59. @Nullable
  60. Object get();
  61. }
  62. }

可以看出,Cache接口抽象了缓存的 get put evict 等相关操作。

AbstractValueAdaptingCache

  1. public abstract class AbstractValueAdaptingCache implements Cache {
  2. //是否允许Null值
  3. private final boolean allowNullValues;
  4. protected AbstractValueAdaptingCache(boolean allowNullValues) {
  5. this.allowNullValues = allowNullValues;
  6. }
  7. public final boolean isAllowNullValues() {
  8. return this.allowNullValues;
  9. }
  10. @Nullable
  11. public ValueWrapper get(Object key) {
  12. return this.toValueWrapper(this.lookup(key));
  13. }
  14. @Nullable
  15. public <T> T get(Object key, @Nullable Class<T> type) {
  16. //查询到的缓存值做fromStoreValue转换
  17. Object value = this.fromStoreValue(this.lookup(key));
  18. //转换后非null值且无法转换为type类型则抛出异常
  19. if (value != null && type != null && !type.isInstance(value)) {
  20. throw new IllegalStateException("Cached value is not of required type [" + type.getName() + "]: " + value);
  21. } else {
  22. return value;
  23. }
  24. }
  25. //从缓存中获取key对应的value,子类实现
  26. @Nullable
  27. protected abstract Object lookup(Object key);
  28. //对于从缓存中获取的值,允许为空且值为NullValue时,处理为null
  29. @Nullable
  30. protected Object fromStoreValue(@Nullable Object storeValue) {
  31. return this.allowNullValues && storeValue == NullValue.INSTANCE ? null : storeValue;
  32. }
  33. //对于要插入缓存的null值,在允许null值的情况下处理为NullValue;否则抛出异常IllegalArgumentException
  34. protected Object toStoreValue(@Nullable Object userValue) {
  35. if (userValue == null) {
  36. if (this.allowNullValues) {
  37. return NullValue.INSTANCE;
  38. } else {
  39. throw new IllegalArgumentException("Cache '" + this.getName() + "' is configured to not allow null values but null was provided");
  40. }
  41. } else {
  42. return userValue;
  43. }
  44. }
  45. //get操作依据,查询到缓存值非null,则fromStoreValue转换后包装成SimpleValueWrapper返回
  46. @Nullable
  47. protected ValueWrapper toValueWrapper(@Nullable Object storeValue) {
  48. return storeValue != null ? new SimpleValueWrapper(this.fromStoreValue(storeValue)) : null;
  49. }
  50. }

抽象类AbstractValueAdaptingCache实现了Cache接口,主要抽象了对NULL值的处理逻辑。

  • **allowNullValues**属性表示是否允许处理NULL值的缓存
  • **fromStoreValue**方法处理NULL值的get操作,在属性allowNullValues为true的情况下,将NullValue处理为NULL
  • **toStoreValue**方法处理NULL值得put操作,在属性allowNullValues为true的情况下,将NULL处理为NullValue,否则抛出异常
  • **toValueWrapper**方法提供Cache接口中get方法的默认实现,从缓存中读取值,再通过fromStoreValue转化,最后包装为SimpleValueWrapper返回
  • **ValueWrapper get(Object key)****T get(Object key, @Nullable Classtype)**方法基于上述方法实现
  • **ValueWrapper get(Object key)****@Nullable Classtype)**方法基于上述方法实现
  • **lookup**抽象方法用于给子类获取真正的缓存值

    ConcurrentMapCache

    1. public class ConcurrentMapCache extends AbstractValueAdaptingCache {
    2. private final String name;
    3. //定义ConcurrentMap缓存
    4. private final ConcurrentMap<Object, Object> store;
    5. //如果要缓存的是值对象的copy,则由此序列化代理类处理
    6. @Nullable
    7. private final SerializationDelegate serialization;
    8. public ConcurrentMapCache(String name) {
    9. this(name, new ConcurrentHashMap(256), true);
    10. }
    11. //默认允许处理null
    12. public ConcurrentMapCache(String name, boolean allowNullValues) {
    13. this(name, new ConcurrentHashMap(256), allowNullValues);
    14. }
    15. //默认serialization = null
    16. public ConcurrentMapCache(String name, ConcurrentMap<Object, Object> store, boolean allowNullValues) {
    17. this(name, store, allowNullValues, (SerializationDelegate)null);
    18. }
    19. protected ConcurrentMapCache(String name, ConcurrentMap<Object, Object> store, boolean allowNullValues, @Nullable SerializationDelegate serialization) {
    20. super(allowNullValues);
    21. Assert.notNull(name, "Name must not be null");
    22. Assert.notNull(store, "Store must not be null");
    23. this.name = name;
    24. this.store = store;
    25. this.serialization = serialization;
    26. }
    27. //serialization不为空,缓存值对象的copy
    28. public final boolean isStoreByValue() {
    29. return this.serialization != null;
    30. }
    31. public final String getName() {
    32. return this.name;
    33. }
    34. public final ConcurrentMap<Object, Object> getNativeCache() {
    35. return this.store;
    36. }
    37. //实现lookup:store#get
    38. @Nullable
    39. protected Object lookup(Object key) {
    40. return this.store.get(key);
    41. }
    42. //基于ConcurrentMap::computeIfAbsent方法实现;get和put的值由fromStoreValue和toStoreValue处理Null
    43. @Nullable
    44. public <T> T get(Object key, Callable<T> valueLoader) {
    45. return this.fromStoreValue(this.store.computeIfAbsent(key, (k) -> {
    46. try {
    47. return this.toStoreValue(valueLoader.call());
    48. } catch (Throwable var5) {
    49. throw new ValueRetrievalException(key, valueLoader, var5);
    50. }
    51. }));
    52. }
    53. public void put(Object key, @Nullable Object value) {
    54. this.store.put(key, this.toStoreValue(value));
    55. }
    56. @Nullable
    57. public ValueWrapper putIfAbsent(Object key, @Nullable Object value) {
    58. Object existing = this.store.putIfAbsent(key, this.toStoreValue(value));
    59. return this.toValueWrapper(existing);
    60. }
    61. public void evict(Object key) {
    62. this.store.remove(key);
    63. }
    64. public boolean evictIfPresent(Object key) {
    65. return this.store.remove(key) != null;
    66. }
    67. public void clear() {
    68. this.store.clear();
    69. }
    70. public boolean invalidate() {
    71. boolean notEmpty = !this.store.isEmpty();
    72. this.store.clear();
    73. return notEmpty;
    74. }
    75. protected Object toStoreValue(@Nullable Object userValue) {
    76. Object storeValue = super.toStoreValue(userValue);
    77. if (this.serialization != null) {
    78. try {
    79. return this.serialization.serializeToByteArray(storeValue);
    80. } catch (Throwable var4) {
    81. throw new IllegalArgumentException("Failed to serialize cache value '" + userValue + "'. Does it implement Serializable?", var4);
    82. }
    83. } else {
    84. return storeValue;
    85. }
    86. }
    87. protected Object fromStoreValue(@Nullable Object storeValue) {
    88. if (storeValue != null && this.serialization != null) {
    89. try {
    90. return super.fromStoreValue(this.serialization.deserializeFromByteArray((byte[])((byte[])storeValue)));
    91. } catch (Throwable var3) {
    92. throw new IllegalArgumentException("Failed to deserialize cache value '" + storeValue + "'", var3);
    93. }
    94. } else {
    95. return super.fromStoreValue(storeValue);
    96. }
    97. }
    98. }

    ConcurrentMapCache继承了抽象类AbstractValueAdaptingCache,是Spring的默认缓存实现。它支持对缓存对象copy的缓存,由SerializationDelegate serialization 处理序列化,默认为 null 即基于引用的缓存。缓存相关操作基于基类 AbstractValueAdaptingCache 的 null 值处理,默认允许为 null。

    CacheManager

    1. public interface CacheManager {
    2. @Nullable
    3. //获取指定name的Cache,可能延迟创建
    4. Cache getCache(String name);
    5. //获取当前CacheManager下的Cache name集合
    6. Collection<String> getCacheNames();
    7. }

    CacheManager 基于 name 管理一组 Cache。当然,CacheManager也有很多实现类,如ConcurrentMapCacheManagerAbstractCacheManagerSimpleCacheManager,这些xxxCacheManager类都是为了制定Cache的管理规则,这里就不再深入探讨了。

    三、Spring Cache实践

    除了第二章中提到的Cache接口和CacheManager接口,在使用Spring 缓存抽象时,还会用到一些JCache注解。

Cache 缓存接口,定义缓存操作。实现有RedisCacheEhCacheCacheConcurrentMapCache
CacheManager 缓存管理,管理各种缓存(Cache)组件
@Cacheable 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存
@CacheEvict 清空缓存
@CachePut 保证方法被调用,又希望结果被缓存。
@EnableCaching 开启基于注解的缓存
keyGenerator 缓存数据时key生成策略
serialize 缓存数据时Value序列化策略

@Cacheable@CacheEvict@CachePut三个注解都是对方法进行配置,主要参数如下图所示:

**@Cacheable**/**@CachePut**/**@CacheEvict** 主要的参数
value 缓存的名称,在Spring配置文件中定义,必须指定至少一个 例如:
@Cacheable(value = **"mycache"**)或者@Cacheable(value = {**"cache1"**, **"cache2"**})
key 缓存的key,可以为空,如果指定要按照SpEL表达式编写,如果不指定,则缺省按照方法的所有参数进行组合 例如:
@Cacheable(value = **"testcache", key="#userName"**)
condition 缓存的条件,可以为空,使用SpEL编写,返回true或者false,只有为true才进行缓存/清除缓存,在调用方法之前之后都能判断 例如:
@Cacheable(value = **"testcache", condition="#userName.length()>2"**)
allEntries
**@CacheEvict**
是否清空所有缓存内容,缺省为false,如果指定为true,则方法调用后将立即清空所有缓存 例如:
@Cacheable(value = **"testcache", **allEntries=true)
beforeInvocation**@CacheEvict** 是否在方法执行前就全清空,缺省为false,如果指定为true,则在方法还没有执行的时候就清空缓存,缺省情况下,如果方法执行抛出,则不会清空缓存 例如:
@CacheEvict(value = **"testcache"**, beforeInvocation = **true**)
unless
**@CachePut**
**@Cacheable**
用于否决缓存的,不像condition,该表达式只在方法执行之后判断,此时可以拿到返回值result进行判断,条件为true不会缓存,false才缓存 例如:
@Cacheable(value = **"testcache"**, unless = **"#result==null"**)

Cache中可用的SpEL表达式如下图所示:

Cache SpEL available metadata
名字 位置 描述 示例
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]
caches root object 当前方法调用使用的缓存列表(如@Cacheable(value = {**"cache1"**, **"cache2"**})),则有两个Cache #root.caches[0]name
argument name evaluation context 方法参数的名字,可以直接 #参数名,也可以使用 #p0#a0 的形式,0代表参数的索引; #ban#a0#p0
result evaluation context 方法执行后的返回值(仅当方法执行之后的判断有效,如”unless”,”cache put”的表达式”cache evict”的表达式beforeInvocation = **true** #result

@EnableCaching

这个注解表示开启基于注解的缓存,一般放在主程序类前面,如下所示:

  1. @EnableCaching
  2. public class SpringBootCacheApplication {
  3. public static void main(String[] args) {
  4. SpringApplication.run(SpringBootCacheApplication.class, args);
  5. }
  6. }

@Cacheable

这个注解放在方法前,可以将方法的运行结果进行缓存,之后就不用调用方法了,直接从缓存中取值即可。常用属性可以见上图。以下是常见用法:

  1. @Cacheable(cacheNames = {"emp"}, key = "#id", conditon = "mid>0", unless = "#result == null")
  2. public Employee getEmp(Integer id) {
  3. Employee emp = employeeMapper.getEmpById(id);
  4. return emp;
  5. }

@Cacheable注解中常用的参数有cacheNames/value(指定缓存组件的名字,可以指定多个)、key(缓存数据时使用的key,默认使用方法参数的值,也可以自定义)、keyGenerator(key的生成器,可以自定义,key与keyGenerator二选一)、cacheManager(指定缓存管理器,或者使用cacheResolver指定获取解析器)、condition(符合条件才缓存)、unless(符合条件则不缓存,可以获取方法运行结果进行判断)、sync(是否使用异步模式,不可与unless一起使用)。@Cacheable的运行原理:

  1. 方法运行前,程序会使用cacheManager根据cacheNames获取Cache,如果没有对应名称的Cache,则自动创建一个Cache。
  2. 使用key去Cache中查找对应的缓存内容。key默认使用SimpleKeyGenerator生成,其生成策略如下:
  • 如果没有参数,key=new SimpleKey()
  • 如果只有一个参数,key=参数值
  • 如果有多个参数,key=new SimpleKey(params)
  1. 没有查到缓存值,则调用目标方法
  2. 以步骤二中返回的key,目标方法返回的结果为value,存入缓存

    @CachePut

    @CachePut注解先调用目标方法,然后再缓存目标方法的结果。

    1. @CachePut(value = "emp", key = "#result.id")
    2. public Employee updateEmp(Employee employee) {
    3. employeeMapper.updateEmp(employee);
    4. return employee;
    5. }

    @CacheEvict

    @CacheEvict用于清除缓存,可以通过key指定需要清除的缓存,allEntries置为true表示删除所有缓存。

    1. @CacheEvict(value = "emp", key = "#id", allEntries = true)
    2. public void deleteEmp(Integer id) {
    3. employeeMapper.deleteEmpById(id);
    4. }

    默认删除缓存行为在方法执行之后(beforeInvocation=false),如果方法运行异常,则该缓存不会被删除。但可以通过设置beforeInvocation = true,将删除缓存行为在方法执行之前。

    @Caching&@CacheConfig

    @Caching注解中包含了@Cacheable@CachePut@CacheEvict注解,可以同时指定多个缓存规则。示例如下所示:

    1. @Caching(
    2. cacheable = {
    3. @Cacheable(value = "emp", key = "#lastName")
    4. },
    5. put = {
    6. @CachePut(value = "emp", key = "#result.id")
    7. @CachePut(value = "emp", key = "#result.email")
    8. }
    9. )

    @CacheConfig注解放在类上,抽取缓存的公共配置,如cacheNamescacheManager等,这样就不用在每个缓存注解中重复配置了。

    四、Redis测试

    Spring Boot里面默认使用的CacheCacheManager分别是ConcurrentMapCacheConcurrentMapCacheManager,将数据存储在ConcurrentMap中。
    然而,在实际开发过程中,一般会使用一些缓存中间件,如Redis、Memcached和Encache等。接下来,演示一下Redis环境搭建与测试。

    Redis环境搭建

    Redis是一个开源的、内存中的数据结构存储系统,可以作为数据库、缓存和消息中间件。这里选择用Docker搭建Redis环境。首先需要下载镜像,然后启动,具体命令如下:

    1. // 默认拉取最新的Redis镜像
    2. docker pull redis
    3. // 启动Redis容器
    4. docker run -d -p 6379:6379 --name myredis redis

    接下来,使用Redis Desktop Manager软件连接Redis,Redis的端口号默认为6379。
    RedisAutoConfiguration.java文件里面定义了StringRedisTemplate(操作字符串)和RedisTemplate组件,将组件自动注入代码中,即可使用。两个组件都有针对Redis不同数据类型的处理方法。Redis常见的五大数据类型:
    String(字符串)、List(列表)、Set(集合)、Hash(散列)、ZSet(有序集合)

    1. stringRedisTemplate.opsForValue()[字符串]
    2. stringRedisTemplate.opsForList()[列表]
    3. stringRedisTemplate.opsForSet()[集合]
    4. stringRedisTemplate.opsForHash()[散列]
    5. stringRedisTemplate.opsForZSet()[有序集合]

    下面是使用示例:

    1. public class RedisTest {
    2. @Autowired
    3. StringRedisTemplate stringRedisTemplate;
    4. @Autowired
    5. RedisTemplate redisTemplate;
    6. // 测试保存数据
    7. public void test01 {
    8. stringRedisTemplate.opsForValue().append("msg","hello");//存入数据
    9. String s = stringRedisTemplate.opsForValue().get("msg");//获取数据
    10. stringRedisTemplate.opsForList().leftPush("mylist","1");
    11. }
    12. //测试保存对象
    13. public void test02 {
    14. Employee emp = new Employee();
    15. //默认使用jdk序列化机制,将对象序列化后的数据保存至Redis中
    16. redisTemplate.opsForValue().set("emp-01", empById);
    17. }
    18. }

    如果想将对象以json的方式保存,可将对象转换为json或改变RedisTemplate中的默认序列化规则。后者的参考代码如下,首先找到Redis的自动加载类RedisAutoConfiguration,自定义一个RedisTemplate,然后放入容器中。

    1. public class MyRedisConfig {
    2. @Bean
    3. public RedisTemplate<Object, Employee> empRedisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
    4. RedisTemplate<Object, Employee> template = new RedisTemplate<Object, Employee>();
    5. template.setConnectionFactory(redisConnectionFactory);
    6. Jackson2JsonRedisSerializer<Employee> ser = new Jackson2JsonRedisSerializer<Employee>(Employee.class);
    7. template.setDefaultSerizlizer(ser);
    8. return template;
    9. }
    10. }

    自定义CacheManager

    当Spring Boot项目中引入Redis的starter依赖时,会将RedisCacheManager作为默认的CacheManagerRedisCacheManager管理RedisCache,后者使用RedisTemplate操作Redis,默认序列化机制是jdk。如果需要改变Redis序列化机制,可以自定义CacheManager。参考代码如下:

    1. @Bean
    2. public RedisCacheManager employeeCacheManager(RedisTemplate<Object,Object> employeeRedisTemplate) {
    3. RedisCacheManager cacheManager = new RedisCacheManager(employeeRedisTemplate);
    4. // 将cacheNames作为key的前缀
    5. cacheManager.setUserPrefix(true);
    6. return cacheManager;
    7. }

    自定义RedisCacheCacheManager都可以更改缓存方式,两者区别在于:前者用于自定义缓存层,后者将缓存交给spring管理(spring cache中不止Redis)。