为监控而生的多级缓存框架 layering-cache

2020-01-16 09:05栏目:龙竞技官网
TAG:

layering-cache是在Spring Cache基础上扩展而来的一个缓存框架,主要目的是在使用注解的时候支持配置过期时间。layering-cache其实是一个两级缓存,一级缓存使用Caffeine作为本地缓存,二级缓存使用redis作为集中式缓存。并且基于redis的Pub/Sub做缓存的删除,所以它是一个适用于分布式环境下的一个缓存系统。

在前文我们介绍了如何使用Redis或者Caffeine来做缓存。

spring boot+spring cache实现两级缓存(redis+caffeine),rediscaffeine

spring boot中集成了spring cache,并有多种缓存方式的实现,如:Redis、Caffeine、JCache、EhCache等等。但如果只用一种缓存,要么会有较大的网络消耗(如Redis),要么就是内存占用太大(如Caffeine这种应用内存缓存)。在很多场景下,可以结合起来实现一、二级缓存的方式,能够很大程度提高应用的处理效率。

内容说明:

  1. 缓存、两级缓存
  2. spring cache:主要包含spring cache定义的接口方法说明和注解中的属性说明
  3. spring boot + spring cache:RedisCache实现中的缺陷
  4. caffeine简介
  5. spring boot + spring cache 实现两级缓存(redis + caffeine)
  • 支持缓存监控统计
  • 支持缓存过期时间在注解上直接配置
  • 支持二级缓存的自动刷新(当缓存命中并发现缓存将要过期时会开启一个异步线程刷新缓存)
  • 刷新缓存分为强刷新和软刷新,强刷新直接调用缓存方法,软刷新直接改缓存的时间
  • 缓存Key支持SpEL表达式
  • 新增FastJsonRedisSerializer,KryoRedisSerializer序列化,重写String序列化。
  • 输出INFO级别的监控统计日志
  • 二级缓存是否允许缓存NULL值支持配置
  • 二级缓存空值允许配置时间倍率
  • Spring Boot缓存实战 Redis 设置有效时间和自动刷新缓存-2
  • Spring Boot缓存实战 Caffeine

缓存、两级缓存

简单的理解,缓存就是将数据从读取较慢的介质上读取出来放到读取较快的介质上,如磁盘-->内存。平时我们会将数据存储到磁盘上,如:数据库。如果每次都从数据库里去读取,会因为磁盘本身的IO影响读取速度,所以就有了像redis这种的内存缓存。可以将数据读取出来放到内存里,这样当需要获取数据时,就能够直接从内存中拿到数据返回,能够很大程度的提高速度。但是一般redis是单独部署成集群,所以会有网络IO上的消耗,虽然与redis集群的链接已经有连接池这种工具,但是数据传输上也还是会有一定消耗。所以就有了应用内缓存,如:caffeine。当应用内缓存有符合条件的数据时,就可以直接使用,而不用通过网络到redis中去获取,这样就形成了两级缓存。应用内缓存叫做一级缓存,远程缓存(如redis)叫做二级缓存

spring cache

当使用缓存的时候,一般是如下的流程:

图片 1

从流程图中可以看出,为了使用缓存,在原有业务处理的基础上,增加了很多对于缓存的操作,如果将这些耦合到业务代码当中,开发起来就有很多重复性的工作,并且不太利于根据代码去理解业务。

spring cache是spring-context包中提供的基于注解方式使用的缓存组件,定义了一些标准接口,通过实现这些接口,就可以通过在方法上增加注解来实现缓存。这样就能够避免缓存代码与业务处理耦合在一起的问题。spring cache的实现是使用spring aop中对方法切面(MethodInterceptor)封装的扩展,当然spring aop也是基于Aspect来实现的。

spring cache核心的接口就两个:Cache和CacheManager

图片 2

Cache接口

提供缓存的具体操作,比如缓存的放入、读取、清理,spring框架中默认提供的实现有:

图片 3

除了RedisCache是在spring-data-redis包中,其他的基本都是在spring-context-support包中

图片 4

#Cache.java

package org.springframework.cache;

import java.util.concurrent.Callable;

public interface Cache {

 // cacheName,缓存的名字,默认实现中一般是CacheManager创建Cache的bean时传入cacheName
 String getName();

 // 获取实际使用的缓存,如:RedisTemplate、com.github.benmanes.caffeine.cache.Cache<Object, Object>。暂时没发现实际用处,可能只是提供获取原生缓存的bean,以便需要扩展一些缓存操作或统计之类的东西
 Object getNativeCache();

 // 通过key获取缓存值,注意返回的是ValueWrapper,为了兼容存储空值的情况,将返回值包装了一层,通过get方法获取实际值
 ValueWrapper get(Object key);

 // 通过key获取缓存值,返回的是实际值,即方法的返回值类型
 <T> T get(Object key, Class<T> type);

 // 通过key获取缓存值,可以使用valueLoader.call()来调使用@Cacheable注解的方法。当@Cacheable注解的sync属性配置为true时使用此方法。因此方法内需要保证回源到数据库的同步性。避免在缓存失效时大量请求回源到数据库
 <T> T get(Object key, Callable<T> valueLoader);

 // 将@Cacheable注解方法返回的数据放入缓存中
 void put(Object key, Object value);

 // 当缓存中不存在key时才放入缓存。返回值是当key存在时原有的数据
 ValueWrapper putIfAbsent(Object key, Object value);

 // 删除缓存
 void evict(Object key);

 // 删除缓存中的所有数据。需要注意的是,具体实现中只删除使用@Cacheable注解缓存的所有数据,不要影响应用内的其他缓存
 void clear();

 // 缓存返回值的包装
 interface ValueWrapper {

 // 返回实际缓存的对象
 Object get();
 }

 // 当{@link #get(Object, Callable)}抛出异常时,会包装成此异常抛出
 @SuppressWarnings("serial")
 class ValueRetrievalException extends RuntimeException {

 private final Object key;

 public ValueRetrievalException(Object key, Callable<?> loader, Throwable ex) {
  super(String.format("Value for key '%s' could not be loaded using '%s'", key, loader), ex);
  this.key = key;
 }

 public Object getKey() {
  return this.key;
 }
 }
}

CacheManager接口

主要提供Cache实现bean的创建,每个应用里可以通过cacheName来对Cache进行隔离,每个cacheName对应一个Cache实现。spring框架中默认提供的实现与Cache的实现都是成对出现,包结构也在上图中

#CacheManager.java

package org.springframework.cache;

import java.util.Collection;

public interface CacheManager {

 // 通过cacheName创建Cache的实现bean,具体实现中需要存储已创建的Cache实现bean,避免重复创建,也避免内存缓存对象(如Caffeine)重新创建后原来缓存内容丢失的情况
 Cache getCache(String name);

 // 返回所有的cacheName
 Collection<String> getCacheNames();
}

常用注解说明

@Cacheable:主要应用到查询数据的方法上

package org.springframework.cache.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.Callable;
import org.springframework.core.annotation.AliasFor;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Cacheable {
    // cacheNames,CacheManager就是通过这个名称创建对应的Cache实现bean
 @AliasFor("cacheNames")
 String[] value() default {};

 @AliasFor("value")
 String[] cacheNames() default {};

    // 缓存的key,支持SpEL表达式。默认是使用所有参数及其计算的hashCode包装后的对象(SimpleKey)
 String key() default "";

 // 缓存key生成器,默认实现是SimpleKeyGenerator
 String keyGenerator() default "";

 // 指定使用哪个CacheManager
 String cacheManager() default "";

 // 缓存解析器
 String cacheResolver() default "";

 // 缓存的条件,支持SpEL表达式,当达到满足的条件时才缓存数据。在调用方法前后都会判断
 String condition() default "";

    // 满足条件时不更新缓存,支持SpEL表达式,只在调用方法后判断
 String unless() default "";

 // 回源到实际方法获取数据时,是否要保持同步,如果为false,调用的是Cache.get(key)方法;如果为true,调用的是Cache.get(key, Callable)方法
 boolean sync() default false;
}

@CacheEvict:清除缓存,主要应用到删除数据的方法上。相比Cacheable多了两个属性

package org.springframework.cache.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface CacheEvict {
    // ...相同属性说明请参考@Cacheable中的说明

 // 是否要清除所有缓存的数据,为false时调用的是Cache.evict(key)方法;为true时调用的是Cache.clear()方法
 boolean allEntries() default false;

 // 调用方法之前或之后清除缓存
 boolean beforeInvocation() default false;
}
  1. @CachePut:放入缓存,主要用到对数据有更新的方法上。属性说明参考@Cacheable
  2. @Caching:用于在一个方法上配置多种注解
  3. @EnableCaching:启用spring cache缓存,作为总的开关,在spring boot的启动类或配置类上需要加上此注解才会生效
  1. 引入layering-cache

问题描述:

通过使用redis和Caffeine来做缓存,我们会发现一些问题。

  • 如果只使用redis来做缓存我们会有大量的请求到redis,但是每次请求的数据都是一样的,假如这一部分数据就放在应用服务器本地,那么就省去了请求redis的网络开销,请求速度就会快很多。但是使用redis横向扩展很方便。
  • 如果只使用Caffeine来做本地缓存,我们的应用服务器的内存是有限,并且单独为了缓存去扩展应用服务器是非常不划算。所以,只使用本地缓存也是有很大局限性的。

至此我们是不是有一个想法了,两个一起用。将热点数据放本地缓存(一级缓存),将非热点数据放redis缓存(二级缓存)。

spring boot + spring cache

spring boot中已经整合了spring cache,并且提供了多种缓存的配置,在使用时只需要配置使用哪个缓存(enum CacheType)即可。

图片 5

spring boot中多增加了一个可以扩展的东西,就是CacheManagerCustomizer接口,可以自定义实现这个接口,然后对CacheManager做一些设置,比如:

package com.itopener.demo.cache.redis.config;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.springframework.boot.autoconfigure.cache.CacheManagerCustomizer;
import org.springframework.data.redis.cache.RedisCacheManager;

public class RedisCacheManagerCustomizer implements CacheManagerCustomizer<RedisCacheManager> {

 @Override
 public void customize(RedisCacheManager cacheManager) {
 // 默认过期时间,单位秒
 cacheManager.setDefaultExpiration(1000);
 cacheManager.setUsePrefix(false);
 Map<String, Long> expires = new ConcurrentHashMap<String, Long>();
 expires.put("userIdCache", 2000L);
 cacheManager.setExpires(expires);
 }

}

加载这个bean:

package com.itopener.demo.cache.redis.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author fuwei.deng
 * @date 2017年12月22日 上午10:24:54
 * @version 1.0.0
 */
@Configuration
public class CacheRedisConfiguration {

 @Bean
 public RedisCacheManagerCustomizer redisCacheManagerCustomizer() {
 return new RedisCacheManagerCustomizer();
 }
}

常用的缓存就是Redis了,Redis对于spring cache接口的实现是在spring-data-redis包中

图片 6

这里提下我认为的RedisCache实现中的缺陷:

1.在缓存失效的瞬间,如果有线程获取缓存数据,可能出现返回null的情况,原因是RedisCache实现中是如下步骤:

  1. 判断缓存key是否存在
  2. 如果key存在,再获取缓存数据,并返回

因此当判断key存在后缓存失效了,再去获取缓存是没有数据的,就返回null了。

2.RedisCacheManager中是否允许存储空值的属性(cacheNullValues)默认为false,即不允许存储空值,这样会存在缓存穿透的风险。缺陷是这个属性是final类型的,只能在创建对象是通过构造方法传入,所以要避免缓存穿透就只能自己在应用内声明RedisCacheManager这个bean了

3.RedisCacheManager中的属性无法通过配置文件直接配置,只能在应用内实现CacheManagerCustomizer接口来进行设置,个人认为不太方便

  • maven 方式

缓存的选择

  • 一级缓存:Caffeine是一个一个高性能的 Java 缓存库;使用 Window TinyLfu 回收策略,提供了一个近乎最佳的命中率。Caffeine 缓存详解
  • 二级缓存:redis是一高性能、高可用的key-value数据库,支持多种数据类型,支持集群,和应用服务器分开部署易于横向扩展。

Caffeine

Caffeine是一个基于Google开源的Guava设计理念的一个高性能内存缓存,使用java8开发,spring boot引入Caffeine后已经逐步废弃Guava的整合了。Caffeine源码及介绍地址:caffeine

caffeine提供了多种缓存填充策略、值回收策略,同时也包含了缓存命中次数等统计数据,对缓存的优化能够提供很大帮助

caffeine的介绍可以参考:

这里简单说下caffeine基于时间的回收策略有以下几种:

  1. expireAfterAccess:访问后到期,从上次读或写发生后的过期时间
  2. expireAfterWrite:写入后到期,从上次写入发生之后的过期时间
  3. 自定义策略:到期时间由实现Expiry接口后单独计算

解决思路

Spring 本来就提供了Cache的支持,最核心的就是实现Cache和CacheManager接口。

spring boot + spring cache 实现两级缓存(redis + caffeine)

本人开头提到了,就算是使用了redis缓存,也会存在一定程度的网络传输上的消耗,在实际应用当中,会存在一些变更频率非常低的数据,就可以直接缓存在应用内部,对于一些实时性要求不太高的数据,也可以在应用内部缓存一定时间,减少对redis的访问,提高响应速度

由于spring-data-redis框架中redis对spring cache的实现有一些不足,在使用起来可能会出现一些问题,所以就不基于原来的实现去扩展了,直接参考实现方式,去实现Cache和CacheManager接口

还需要注意一点,一般应用都部署了多个节点,一级缓存是在应用内的缓存,所以当对数据更新和清除时,需要通知所有节点进行清理缓存的操作。可以有多种方式来实现这种效果,比如:zookeeper、MQ等,但是既然用了redis缓存,redis本身是有支持订阅/发布功能的,所以就不依赖其他组件了,直接使用redis的通道来通知其他节点进行清理缓存的操作

以下就是对spring boot + spring cache实现两级缓存(redis + caffeine)的starter封装步骤和源码

定义properties配置属性类

package com.itopener.cache.redis.caffeine.spring.boot.autoconfigure;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.springframework.boot.context.properties.ConfigurationProperties;
/** 
 * @author fuwei.deng
 * @date 2018年1月29日 上午11:32:15
 * @version 1.0.0
 */
@ConfigurationProperties(prefix = "spring.cache.multi")
public class CacheRedisCaffeineProperties { 
 private Set<String> cacheNames = new HashSet<>(); 
 /** 是否存储空值,默认true,防止缓存穿透*/
 private boolean cacheNullValues = true; 
 /** 是否动态根据cacheName创建Cache的实现,默认true*/
 private boolean dynamic = true;

 /** 缓存key的前缀*/
 private String cachePrefix; 
 private Redis redis = new Redis(); 
 private Caffeine caffeine = new Caffeine();
 public class Redis { 
 /** 全局过期时间,单位毫秒,默认不过期*/
 private long defaultExpiration = 0;

 /** 每个cacheName的过期时间,单位毫秒,优先级比defaultExpiration高*/
 private Map<String, Long> expires = new HashMap<>();

 /** 缓存更新时通知其他节点的topic名称*/
 private String topic = "cache:redis:caffeine:topic";

 public long getDefaultExpiration() {
  return defaultExpiration;
 }

 public void setDefaultExpiration(long defaultExpiration) {
  this.defaultExpiration = defaultExpiration;
 }

 public Map<String, Long> getExpires() {
  return expires;
 }

 public void setExpires(Map<String, Long> expires) {
  this.expires = expires;
 }

 public String getTopic() {
  return topic;
 }

 public void setTopic(String topic) {
  this.topic = topic;
 }

 }

 public class Caffeine { 
 /** 访问后过期时间,单位毫秒*/
 private long expireAfterAccess;

 /** 写入后过期时间,单位毫秒*/
 private long expireAfterWrite;

 /** 写入后刷新时间,单位毫秒*/
 private long refreshAfterWrite;

 /** 初始化大小*/
 private int initialCapacity;

 /** 最大缓存对象个数,超过此数量时之前放入的缓存将失效*/
 private long maximumSize;

 /** 由于权重需要缓存对象来提供,对于使用spring cache这种场景不是很适合,所以暂不支持配置*/
// private long maximumWeight;

 public long getExpireAfterAccess() {
  return expireAfterAccess;
 }

 public void setExpireAfterAccess(long expireAfterAccess) {
  this.expireAfterAccess = expireAfterAccess;
 }

 public long getExpireAfterWrite() {
  return expireAfterWrite;
 }

 public void setExpireAfterWrite(long expireAfterWrite) {
  this.expireAfterWrite = expireAfterWrite;
 }

 public long getRefreshAfterWrite() {
  return refreshAfterWrite;
 }

 public void setRefreshAfterWrite(long refreshAfterWrite) {
  this.refreshAfterWrite = refreshAfterWrite;
 }

 public int getInitialCapacity() {
  return initialCapacity;
 }

 public void setInitialCapacity(int initialCapacity) {
  this.initialCapacity = initialCapacity;
 }

 public long getMaximumSize() {
  return maximumSize;
 }

 public void setMaximumSize(long maximumSize) {
  this.maximumSize = maximumSize;
 }
 }

 public Set<String> getCacheNames() {
 return cacheNames;
 }

 public void setCacheNames(Set<String> cacheNames) {
 this.cacheNames = cacheNames;
 }

 public boolean isCacheNullValues() {
 return cacheNullValues;
 }

 public void setCacheNullValues(boolean cacheNullValues) {
 this.cacheNullValues = cacheNullValues;
 }

 public boolean isDynamic() {
 return dynamic;
 }

 public void setDynamic(boolean dynamic) {
 this.dynamic = dynamic;
 }

 public String getCachePrefix() {
 return cachePrefix;
 }

 public void setCachePrefix(String cachePrefix) {
 this.cachePrefix = cachePrefix;
 }

 public Redis getRedis() {
 return redis;
 }

 public void setRedis(Redis redis) {
 this.redis = redis;
 }

 public Caffeine getCaffeine() {
 return caffeine;
 }

 public void setCaffeine(Caffeine caffeine) {
 this.caffeine = caffeine;
 }
}

spring cache中有实现Cache接口的一个抽象类AbstractValueAdaptingCache,包含了空值的包装和缓存值的包装,所以就不用实现Cache接口了,直接实现AbstractValueAdaptingCache抽象类

package com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.support;
import java.lang.reflect.Constructor;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.support.AbstractValueAdaptingCache;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.util.StringUtils;
import com.github.benmanes.caffeine.cache.Cache;
import com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.CacheRedisCaffeineProperties;

/**
 * @author fuwei.deng
 * @date 2018年1月26日 下午5:24:11
 * @version 1.0.0
 */
public class RedisCaffeineCache extends AbstractValueAdaptingCache { 
 private final Logger logger = LoggerFactory.getLogger(RedisCaffeineCache.class);
 private String name;
 private RedisTemplate<Object, Object> redisTemplate;
 private Cache<Object, Object> caffeineCache;
 private String cachePrefix;
 private long defaultExpiration = 0;
 private Map<String, Long> expires;
 private String topic = "cache:redis:caffeine:topic"; 
 protected RedisCaffeineCache(boolean allowNullValues) {
 super(allowNullValues);
 }

 public RedisCaffeineCache(String name, RedisTemplate<Object, Object> redisTemplate, Cache<Object, Object> caffeineCache, CacheRedisCaffeineProperties cacheRedisCaffeineProperties) {
 super(cacheRedisCaffeineProperties.isCacheNullValues());
 this.name = name;
 this.redisTemplate = redisTemplate;
 this.caffeineCache = caffeineCache;
 this.cachePrefix = cacheRedisCaffeineProperties.getCachePrefix();
 this.defaultExpiration = cacheRedisCaffeineProperties.getRedis().getDefaultExpiration();
 this.expires = cacheRedisCaffeineProperties.getRedis().getExpires();
 this.topic = cacheRedisCaffeineProperties.getRedis().getTopic();
 }

 @Override
 public String getName() {
 return this.name;
 }

 @Override
 public Object getNativeCache() {
 return this;
 }

 @SuppressWarnings("unchecked")
 @Override
 public <T> T get(Object key, Callable<T> valueLoader) {
 Object value = lookup(key);
 if(value != null) {
  return (T) value;
 }

 ReentrantLock lock = new ReentrantLock();
 try {
  lock.lock();
  value = lookup(key);
  if(value != null) {
  return (T) value;
  }
  value = valueLoader.call();
  Object storeValue = toStoreValue(valueLoader.call());
  put(key, storeValue);
  return (T) value;
 } catch (Exception e) {
  try {
        Class<?> c = Class.forName("org.springframework.cache.Cache$ValueRetrievalException");
        Constructor<?> constructor = c.getConstructor(Object.class, Callable.class, Throwable.class);
        RuntimeException exception = (RuntimeException) constructor.newInstance(key, valueLoader, e.getCause());
        throw exception;        
      } catch (Exception e1) {
        throw new IllegalStateException(e1);
      }
 } finally {
  lock.unlock();
 }
 }

 @Override
 public void put(Object key, Object value) {
 if (!super.isAllowNullValues() && value == null) {
  this.evict(key);
      return;
    }
 long expire = getExpire();
 if(expire > 0) {
  redisTemplate.opsForValue().set(getKey(key), toStoreValue(value), expire, TimeUnit.MILLISECONDS);
 } else {
  redisTemplate.opsForValue().set(getKey(key), toStoreValue(value));
 }

 push(new CacheMessage(this.name, key));

 caffeineCache.put(key, value);
 }

 @Override
 public ValueWrapper putIfAbsent(Object key, Object value) {
 Object cacheKey = getKey(key);
 Object prevValue = null;
 // 考虑使用分布式锁,或者将redis的setIfAbsent改为原子性操作
 synchronized (key) {
  prevValue = redisTemplate.opsForValue().get(cacheKey);
  if(prevValue == null) {
  long expire = getExpire();
  if(expire > 0) {
   redisTemplate.opsForValue().set(getKey(key), toStoreValue(value), expire, TimeUnit.MILLISECONDS);
  } else {
   redisTemplate.opsForValue().set(getKey(key), toStoreValue(value));
  }

  push(new CacheMessage(this.name, key));

  caffeineCache.put(key, toStoreValue(value));
  }
 }
 return toValueWrapper(prevValue);
 }

 @Override
 public void evict(Object key) {
 // 先清除redis中缓存数据,然后清除caffeine中的缓存,避免短时间内如果先清除caffeine缓存后其他请求会再从redis里加载到caffeine中
 redisTemplate.delete(getKey(key));

 push(new CacheMessage(this.name, key));

 caffeineCache.invalidate(key);
 }

 @Override
 public void clear() {
 // 先清除redis中缓存数据,然后清除caffeine中的缓存,避免短时间内如果先清除caffeine缓存后其他请求会再从redis里加载到caffeine中
 Set<Object> keys = redisTemplate.keys(this.name.concat(":"));
 for(Object key : keys) {
  redisTemplate.delete(key);
 }

 push(new CacheMessage(this.name, null));

 caffeineCache.invalidateAll();
 }

 @Override
 protected Object lookup(Object key) {
 Object cacheKey = getKey(key);
 Object value = caffeineCache.getIfPresent(key);
 if(value != null) {
  logger.debug("get cache from caffeine, the key is : {}", cacheKey);
  return value;
 }

 value = redisTemplate.opsForValue().get(cacheKey);

 if(value != null) {
  logger.debug("get cache from redis and put in caffeine, the key is : {}", cacheKey);
  caffeineCache.put(key, value);
 }
 return value;
 }

 private Object getKey(Object key) {
 return this.name.concat(":").concat(StringUtils.isEmpty(cachePrefix) ? key.toString() : cachePrefix.concat(":").concat(key.toString()));
 }

 private long getExpire() {
 long expire = defaultExpiration;
 Long cacheNameExpire = expires.get(this.name);
 return cacheNameExpire == null ? expire : cacheNameExpire.longValue();
 }

 /**
 * @description 缓存变更时通知其他节点清理本地缓存
 * @author fuwei.deng
 * @date 2018年1月31日 下午3:20:28
 * @version 1.0.0
 * @param message
 */
 private void push(CacheMessage message) {
 redisTemplate.convertAndSend(topic, message);
 }

 /**
 * @description 清理本地缓存
 * @author fuwei.deng
 * @date 2018年1月31日 下午3:15:39
 * @version 1.0.0
 * @param key
 */
 public void clearLocal(Object key) {
 logger.debug("clear local cache, the key is : {}", key);
 if(key == null) {
  caffeineCache.invalidateAll();
 } else {
  caffeineCache.invalidate(key);
 }
 }
}

实现CacheManager接口

package com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.support;

import java.util.Collection;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.data.redis.core.RedisTemplate;

import com.github.benmanes.caffeine.cache.Caffeine;
import com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.CacheRedisCaffeineProperties;

/**
 * @author fuwei.deng
 * @date 2018年1月26日 下午5:24:52
 * @version 1.0.0
 */
public class RedisCaffeineCacheManager implements CacheManager {

 private final Logger logger = LoggerFactory.getLogger(RedisCaffeineCacheManager.class);

 private ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap<String, Cache>();

 private CacheRedisCaffeineProperties cacheRedisCaffeineProperties;

 private RedisTemplate<Object, Object> redisTemplate;

 private boolean dynamic = true;

 private Set<String> cacheNames;

 public RedisCaffeineCacheManager(CacheRedisCaffeineProperties cacheRedisCaffeineProperties,
  RedisTemplate<Object, Object> redisTemplate) {
 super();
 this.cacheRedisCaffeineProperties = cacheRedisCaffeineProperties;
 this.redisTemplate = redisTemplate;
 this.dynamic = cacheRedisCaffeineProperties.isDynamic();
 this.cacheNames = cacheRedisCaffeineProperties.getCacheNames();
 }

 @Override
 public Cache getCache(String name) {
 Cache cache = cacheMap.get(name);
 if(cache != null) {
  return cache;
 }
 if(!dynamic && !cacheNames.contains(name)) {
  return cache;
 }

 cache = new RedisCaffeineCache(name, redisTemplate, caffeineCache(), cacheRedisCaffeineProperties);
 Cache oldCache = cacheMap.putIfAbsent(name, cache);
 logger.debug("create cache instance, the cache name is : {}", name);
 return oldCache == null ? cache : oldCache;
 }

 public com.github.benmanes.caffeine.cache.Cache<Object, Object> caffeineCache(){
 Caffeine<Object, Object> cacheBuilder = Caffeine.newBuilder();
 if(cacheRedisCaffeineProperties.getCaffeine().getExpireAfterAccess() > 0) {
  cacheBuilder.expireAfterAccess(cacheRedisCaffeineProperties.getCaffeine().getExpireAfterAccess(), TimeUnit.MILLISECONDS);
 }
 if(cacheRedisCaffeineProperties.getCaffeine().getExpireAfterWrite() > 0) {
  cacheBuilder.expireAfterWrite(cacheRedisCaffeineProperties.getCaffeine().getExpireAfterWrite(), TimeUnit.MILLISECONDS);
 }
 if(cacheRedisCaffeineProperties.getCaffeine().getInitialCapacity() > 0) {
  cacheBuilder.initialCapacity(cacheRedisCaffeineProperties.getCaffeine().getInitialCapacity());
 }
 if(cacheRedisCaffeineProperties.getCaffeine().getMaximumSize() > 0) {
  cacheBuilder.maximumSize(cacheRedisCaffeineProperties.getCaffeine().getMaximumSize());
 }
 if(cacheRedisCaffeineProperties.getCaffeine().getRefreshAfterWrite() > 0) {
  cacheBuilder.refreshAfterWrite(cacheRedisCaffeineProperties.getCaffeine().getRefreshAfterWrite(), TimeUnit.MILLISECONDS);
 }
 return cacheBuilder.build();
 }

 @Override
 public Collection<String> getCacheNames() {
 return this.cacheNames;
 }

 public void clearLocal(String cacheName, Object key) {
 Cache cache = cacheMap.get(cacheName);
 if(cache == null) {
  return ;
 }

 RedisCaffeineCache redisCaffeineCache = (RedisCaffeineCache) cache;
 redisCaffeineCache.clearLocal(key);
 }
}

redis消息发布/订阅,传输的消息类

package com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.support;
import java.io.Serializable;

/** 
 * @author fuwei.deng
 * @date 2018年1月29日 下午1:31:17
 * @version 1.0.0
 */
public class CacheMessage implements Serializable {

 /** */
 private static final long serialVersionUID = 5987219310442078193L;

 private String cacheName; 
 private Object key;
 public CacheMessage(String cacheName, Object key) {
 super();
 this.cacheName = cacheName;
 this.key = key;
 }

 public String getCacheName() {
 return cacheName;
 }

 public void setCacheName(String cacheName) {
 this.cacheName = cacheName;
 }

 public Object getKey() {
 return key;
 }

 public void setKey(Object key) {
 this.key = key;
 }
}

监听redis消息需要实现MessageListener接口

package com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.support;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.data.redis.core.RedisTemplate;
/** 
 * @author fuwei.deng
 * @date 2018年1月30日 下午5:22:33
 * @version 1.0.0
 */
public class CacheMessageListener implements MessageListener { 
 private final Logger logger = LoggerFactory.getLogger(CacheMessageListener.class);
 private RedisTemplate<Object, Object> redisTemplate;
 private RedisCaffeineCacheManager redisCaffeineCacheManager;
 public CacheMessageListener(RedisTemplate<Object, Object> redisTemplate,
  RedisCaffeineCacheManager redisCaffeineCacheManager) {
 super();
 this.redisTemplate = redisTemplate;
 this.redisCaffeineCacheManager = redisCaffeineCacheManager;
 }

 @Override
 public void onMessage(Message message, byte[] pattern) {
 CacheMessage cacheMessage = (CacheMessage) redisTemplate.getValueSerializer().deserialize(message.getBody());
 logger.debug("recevice a redis topic message, clear local cache, the cacheName is {}, the key is {}", cacheMessage.getCacheName(), cacheMessage.getKey());
 redisCaffeineCacheManager.clearLocal(cacheMessage.getCacheName(), cacheMessage.getKey());
 }
}

增加spring boot配置类

package com.itopener.cache.redis.caffeine.spring.boot.autoconfigure;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.support.CacheMessageListener;
import com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.support.RedisCaffeineCacheManager;
/** 
 * @author fuwei.deng
 * @date 2018年1月26日 下午5:23:03
 * @version 1.0.0
 */
@Configuration
@AutoConfigureAfter(RedisAutoConfiguration.class)
@EnableConfigurationProperties(CacheRedisCaffeineProperties.class)
public class CacheRedisCaffeineAutoConfiguration {

 @Autowired
 private CacheRedisCaffeineProperties cacheRedisCaffeineProperties;

 @Bean
 @ConditionalOnBean(RedisTemplate.class)
 public RedisCaffeineCacheManager cacheManager(RedisTemplate<Object, Object> redisTemplate) {
 return new RedisCaffeineCacheManager(cacheRedisCaffeineProperties, redisTemplate);
 }

 @Bean
 public RedisMessageListenerContainer redisMessageListenerContainer(RedisTemplate<Object, Object> redisTemplate, 
  RedisCaffeineCacheManager redisCaffeineCacheManager) {
 RedisMessageListenerContainer redisMessageListenerContainer = new RedisMessageListenerContainer();
 redisMessageListenerContainer.setConnectionFactory(redisTemplate.getConnectionFactory());
 CacheMessageListener cacheMessageListener = new CacheMessageListener(redisTemplate, redisCaffeineCacheManager);
 redisMessageListenerContainer.addMessageListener(cacheMessageListener, new ChannelTopic(cacheRedisCaffeineProperties.getRedis().getTopic()));
 return redisMessageListenerContainer;
 }
}

在resources/META-INF/spring.factories文件中增加spring boot配置扫描

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=
com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.CacheRedisCaffeineAutoConfiguration

接下来就可以使用maven引入使用了

<dependency>
  <groupId>com.itopener</groupId>
  <artifactId>cache-redis-caffeine-spring-boot-starter</artifactId>
  <version>1.0.0-SNAPSHOT</version>
  <type>pom</type>
</dependency>

在启动类上增加@EnableCaching注解,在需要缓存的方法上增加@Cacheable注解

package com.itopener.demo.cache.redis.caffeine.service;
import java.util.Random;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import com.itopener.demo.cache.redis.caffeine.vo.UserVO;
import com.itopener.utils.TimestampUtil;

@Service
public class CacheRedisCaffeineService {

 private final Logger logger = LoggerFactory.getLogger(CacheRedisCaffeineService.class);

 @Cacheable(key = "'cache_user_id_' + #id", value = "userIdCache", cacheManager = "cacheManager")
 public UserVO get(long id) {
 logger.info("get by id from db");
 UserVO user = new UserVO();
 user.setId(id);
 user.setName("name" + id);
 user.setCreateTime(TimestampUtil.current());
 return user;
 }

 @Cacheable(key = "'cache_user_name_' + #name", value = "userNameCache", cacheManager = "cacheManager")
 public UserVO get(String name) {
 logger.info("get by name from db");
 UserVO user = new UserVO();
 user.setId(new Random().nextLong());
 user.setName(name);
 user.setCreateTime(TimestampUtil.current());
 return user;
 }

 @CachePut(key = "'cache_user_id_' + #userVO.id", value = "userIdCache", cacheManager = "cacheManager")
 public UserVO update(UserVO userVO) {
 logger.info("update to db");
 userVO.setCreateTime(TimestampUtil.current());
 return userVO;
 }

 @CacheEvict(key = "'cache_user_id_' + #id", value = "userIdCache", cacheManager = "cacheManager")
 public void delete(long id) {
 logger.info("delete from db");
 }
}

properties文件中redis的配置跟使用redis是一样的,可以增加两级缓存的配置

#两级缓存的配置
spring.cache.multi.caffeine.expireAfterAccess=5000
spring.cache.multi.redis.defaultExpiration=60000

#spring cache配置
spring.cache.cache-names=userIdCache,userNameCache

#redis配置
#spring.redis.timeout=10000
#spring.redis.password=redispwd
#redis pool
#spring.redis.pool.maxIdle=10
#spring.redis.pool.minIdle=2
#spring.redis.pool.maxActive=10
#spring.redis.pool.maxWait=3000
#redis cluster
spring.redis.cluster.nodes=127.0.0.1:7001,127.0.0.1:7002,127.0.0.1:7003,127.0.0.1:7004,127.0.0.1:7005,127.0.0.1:7006
spring.redis.cluster.maxRedirects=3

扩展

个人认为redisson的封装更方便一些

  1. 对于spring cache缓存的实现没有那么多的缺陷
  2. 使用redis的HASH结构,可以针对不同的hashKey设置过期时间,清理的时候会更方便
  3. 如果基于redisson来实现多级缓存,可以继承RedissonCache,在对应方法增加一级缓存的操作即可
  4. 如果有使用分布式锁的情况就更方便了,可以直接使用Redisson中封装的分布式锁
  5. redisson中的发布订阅封装得更好用

后续可以增加对于缓存命中率的统计endpoint,这样就可以更好的监控各个缓存的命中情况,以便对缓存配置进行优化

源码下载

starter目录:springboot / itopener-parent / spring-boot-starters-parent / cache-redis-caffeine-spring-boot-starter-parent

示例代码目录: springboot / itopener-parent / demo-parent / demo-cache-redis-caffeine

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持帮客之家。

boot+spring cache实现两级缓存(redis+caffeine),rediscaffeine spring boot中集成了spring cache,并有多种缓存方式的实现,如:Redis、Caffeine、JCache、...

<dependency> <groupId>com.github.xiaolyuh</groupId> <artifactId>layering-cache-aspectj</artifactId> <version>${layering.version}</version></dependency>

Cache接口

主要是实现对缓存的操作,如增删查等。

public interface Cache {
    String getName();

    Object getNativeCache();

    <T> T get(Object key, Class<T> type);

    <T> T get(Object key, Callable<T> valueLoader);

    void put(Object key, Object value);

    ValueWrapper putIfAbsent(Object key, Object value);

    void evict(Object key);

    void clear();

    ...
}
  • gradle 方式

CacheManager接口

根据缓存名称来管理Cache,核心方法就是通过缓存名称获取Cache。

public interface CacheManager {

    Cache getCache(String name);

    Collection<String> getCacheNames();

}

通过上面的两个接口我的大致思路是,写一个LayeringCache来实现Cache接口,LayeringCache类中集成对Caffeine和redis的操作。
写一个LayeringCacheManager来管理LayeringCache就行了。

这里的redis缓存使用的是我扩展后的RedisCache详情请看:

  • Spring Boot缓存实战 Redis 设置有效时间和自动刷新缓存,时间支持在配置文件中配置
  • Spring Boot缓存实战 Redis 设置有效时间和自动刷新缓存-2。

LayeringCache

LayeringCache类,因为需要集成对Caffeine和Redis的操作,所以至少需要有name(缓存名称)、CaffeineCache和CustomizedRedisCache三个属性,还增加了一个是否使用一级缓存的开关usedFirstCache。在LayeringCache类的方法里面分别去调用操作一级缓存的和操作二级缓存的方法就可以了。

在这里特别说明一下:

  • 在查询方法如get等,先去查询一级缓存,如果没查到再去查二级缓存。
  • 如果是删除方法如evict和clear等,需要先删掉二级缓存的数据,再去删掉一级缓存的数据,否则有并发问题。
  • put方法没有顺序要求,但是建议将一级缓存的操作放在前面。

完整代码:

/**
 * @author yuhao.wang
 */
public class LayeringCache extends AbstractValueAdaptingCache {

    Logger logger = LoggerFactory.getLogger(LayeringCache.class);

    /**
     * 缓存的名称
     */
    private final String name;

    /**
     * 是否使用一级缓存
     */
    private boolean usedFirstCache = true;

    /**
     * redi缓存
     */
    private final CustomizedRedisCache redisCache;

    /**
     * Caffeine缓存
     */
    private final CaffeineCache caffeineCache;

    /**
     * @param name              缓存名称
     * @param prefix            缓存前缀
     * @param redisOperations   操作Redis的RedisTemplate
     * @param expiration        redis缓存过期时间
     * @param preloadSecondTime redis缓存自动刷新时间
     * @param allowNullValues   是否允许存NULL,默认是false
     * @param usedFirstCache    是否使用一级缓存,默认是true
     * @param forceRefresh      是否强制刷新(走数据库),默认是false
     * @param caffeineCache     Caffeine缓存
     */
    public LayeringCache(String name, byte[] prefix, RedisOperations<? extends Object, ? extends Object> redisOperations,
                         long expiration, long preloadSecondTime, boolean allowNullValues, boolean usedFirstCache,
                         boolean forceRefresh, com.github.benmanes.caffeine.cache.Cache<Object, Object> caffeineCache) {

        super(allowNullValues);
        this.name = name;
        this.usedFirstCache = usedFirstCache;
        this.redisCache = new CustomizedRedisCache(name, prefix, redisOperations, expiration, preloadSecondTime, forceRefresh, allowNullValues);
        this.caffeineCache = new CaffeineCache(name, caffeineCache, allowNullValues);
    }

    @Override
    public String getName() {
        return this.name;
    }

    @Override
    public Object getNativeCache() {
        return this;
    }

    public CustomizedRedisCache getSecondaryCache() {
        return this.redisCache;
    }

    @Override
    public ValueWrapper get(Object key) {
        ValueWrapper wrapper = null;
        if (usedFirstCache) {
            // 查询一级缓存
            wrapper = caffeineCache.get(key);
            logger.debug("查询一级缓存 key:{},返回值是:{}", key, wrapper);
        }

        if (wrapper == null) {
            // 查询二级缓存
            wrapper = redisCache.get(key);
            logger.debug("查询二级缓存 key:{},返回值是:{}", key, wrapper);
        }
        return wrapper;
    }

    @Override
    public <T> T get(Object key, Class<T> type) {
        T value = null;
        if (usedFirstCache) {
            // 查询一级缓存
            value = caffeineCache.get(key, type);
            logger.debug("查询一级缓存 key:{},返回值是:{}", key);
        }

        if (value == null) {
            // 查询二级缓存
            value = redisCache.get(key, type);
            caffeineCache.put(key, value);
            logger.debug("查询二级缓存 key:{},返回值是:{}", key);
        }
        return value;
    }

    @Override
    public <T> T get(Object key, Callable<T> valueLoader) {
        T value = null;
        if (usedFirstCache) {
            // 查询一级缓存,如果一级缓存没有值则调用getForSecondaryCache(k, valueLoader)查询二级缓存
            value = (T) caffeineCache.getNativeCache().get(key, k -> getForSecondaryCache(k, valueLoader));
        } else {
            // 直接查询二级缓存
            value = (T) getForSecondaryCache(key, valueLoader);
        }
        return value;
    }

    @Override
    public void put(Object key, Object value) {
        if (usedFirstCache) {
            caffeineCache.put(key, value);
        }
        redisCache.put(key, value);
    }

    @Override
    public ValueWrapper putIfAbsent(Object key, Object value) {
        if (usedFirstCache) {
            caffeineCache.putIfAbsent(key, value);
        }
        return redisCache.putIfAbsent(key, value);
    }

    @Override
    public void evict(Object key) {
        // 删除的时候要先删除二级缓存再删除一级缓存,否则有并发问题
        redisCache.evict(key);
        if (usedFirstCache) {
            caffeineCache.evict(key);
        }
    }

    @Override
    public void clear() {
        redisCache.clear();
        if (usedFirstCache) {
            caffeineCache.clear();
        }
    }

    @Override
    protected Object lookup(Object key) {
        Object value = null;
        if (usedFirstCache) {
            value = caffeineCache.get(key);
            logger.debug("查询一级缓存 key:{},返回值是:{}", key);
        }
        if (value == null) {
            value = redisCache.get(key);
            logger.debug("查询二级缓存 key:{},返回值是:{}", key);
        }
        return value;
    }

    /**
     * 查询二级缓存
     *
     * @param key
     * @param valueLoader
     * @return
     */
    private <T> Object getForSecondaryCache(Object key, Callable<T> valueLoader) {
        T value = redisCache.get(key, valueLoader);
        logger.debug("查询二级缓存 key:{},返回值是:{}", key, value);
        return value;
    }
}
compile 'com.github.xiaolyuh:layering-cache:${layering.version}'

LayeringCacheManager

因为我们需要在CacheManager中来管理缓存,所以我们需要在CacheManager定义一个容器来存储缓存。在这里我们新建一个ConcurrentMap<String, Cache> cacheMap来存在缓存,CacheManager的两个方法getCache和getCacheNames都通过操作这个cacheMap来实现。Map<String, FirstCacheSetting> firstCacheSettings和Map<String, SecondaryCacheSetting> secondaryCacheSettings属性是针对每一个缓存的特殊配置,如一级缓存的过期时间配置,二级缓存的过期时间和自动刷新时间配置。剩下的属性就不一一介绍了,可直接看下面的源码。

  1. 声明RedisTemplate声明RedisTemplate

  2. 声明CacheManager和LayeringAspect

getCache 方法

这可以说是CacheManager最核心的方法,所有CacheManager操作都围绕这个方法进行。

@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 = createCache(name);
                this.cacheMap.put(name, cache);
            }
        }
    }
    return cache;
}

从这段逻辑我们可以看出这个方法就是根据名称获取缓存,如果没有找到并且动态创建缓存的开关dynamic为true的话,就调用createCache方法动态的创建缓存。

createCache 方法

去创建一个LayeringCache

protected Cache createCache(String name) {

    return new LayeringCache(name, (usePrefix ? cachePrefix.prefix(name) : null), redisOperations,
            getSecondaryCacheExpirationSecondTime(name), getSecondaryCachePreloadSecondTime(name),
            isAllowNullValues(), getUsedFirstCache(name), getForceRefresh(name), createNativeCaffeineCache(name));
}

在创建缓存的时候我们会调用getSecondaryCacheExpirationSecondTime、getSecondaryCachePreloadSecondTime和getForceRefresh等方法去获取二级缓存的过期时间、自动刷新时间和是否强制刷新(走数据库)等值,这些都在secondaryCacheSettings属性中获取;调用createNativeCaffeineCache方法去创建一个一级缓存Caffeine的实例。createNativeCaffeineCache在这个方法里面会调用getCaffeine方法动态的去读取一级缓存的配置,并根据配置创建一级缓存,如果没有找到特殊配置,就使用默认配置,而这里的特殊配置则在firstCacheSettings属性中获取。

/** * 多级缓存配置 * * @author yuhao.wang3 */@Configuration@EnableAspectJAutoProxypublic class CacheConfig { @Bean public CacheManager cacheManager(RedisTemplate<String, Object> redisTemplate) { return new LayeringCacheManager(redisTemplate); } @Bean public LayeringAspect layeringAspect() { return new LayeringAspect(); }}

getCaffeine

动态的获取一级缓存配置,并创建对应Caffeine对象。

private Caffeine<Object, Object> getCaffeine(String name) {
    if (!CollectionUtils.isEmpty(firstCacheSettings)) {
        FirstCacheSetting firstCacheSetting = firstCacheSettings.get(name);
        if (firstCacheSetting != null && StringUtils.isNotBlank(firstCacheSetting.getCacheSpecification())) {
            // 根据缓存名称获取一级缓存配置
            return Caffeine.from(CaffeineSpec.parse(firstCacheSetting.getCacheSpecification()));
        }
    }

    return this.cacheBuilder;
}private Caffeine<Object, Object> getCaffeine(String name) {
    if (!CollectionUtils.isEmpty(firstCacheSettings)) {
        FirstCacheSetting firstCacheSetting = firstCacheSettings.get(name);
        if (firstCacheSetting != null && StringUtils.isNotBlank(firstCacheSetting.getCacheSpecification())) {
            // 根据缓存名称获取一级缓存配置
            return Caffeine.from(CaffeineSpec.parse(firstCacheSetting.getCacheSpecification()));
        }
    }

    return this.cacheBuilder;
}

引入layering-cache 就可以了

setFirstCacheSettings和setSecondaryCacheSettings

我们借用了RedisCacheManager的setExpires(Map<String, Long> expires)方法的思想。用setFirstCacheSettings和setSecondaryCacheSettings方法对一级缓存和二级缓存的特殊配置进行设值。

/**
 * 根据缓存名称设置一级缓存的有效时间和刷新时间,单位秒
 *
 * @param firstCacheSettings
 */
public void setFirstCacheSettings(Map<String, FirstCacheSetting> firstCacheSettings) {
    this.firstCacheSettings = (!CollectionUtils.isEmpty(firstCacheSettings) ? new ConcurrentHashMap<>(firstCacheSettings) : null);
}

/**
 * 根据缓存名称设置二级缓存的有效时间和刷新时间,单位秒
 *
 * @param secondaryCacheSettings
 */
public void setSecondaryCacheSettings(Map<String, SecondaryCacheSetting> secondaryCacheSettings) {
    this.secondaryCacheSettings = (!CollectionUtils.isEmpty(secondaryCacheSettings) ? new ConcurrentHashMap<>(secondaryCacheSettings) : null);
}

完整代码:

/**
 * @author yuhao.wang
 */
@SuppressWarnings("rawtypes")
public class LayeringCacheManager implements CacheManager {
    // 常量
    static final int DEFAULT_EXPIRE_AFTER_WRITE = 60;
    static final int DEFAULT_INITIAL_CAPACITY = 5;
    static final int DEFAULT_MAXIMUM_SIZE = 1_000;

    private final ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap<String, Cache>(16);


    /**
     * 一级缓存配置
     */
    private Map<String, FirstCacheSetting> firstCacheSettings = null;

    /**
     * 二级缓存配置
     */
    private Map<String, SecondaryCacheSetting> secondaryCacheSettings = null;

    /**
     * 是否允许动态创建缓存,默认是true
     */
    private boolean dynamic = true;

    /**
     * 缓存值是否允许为NULL
     */
    private boolean allowNullValues = false;

    // Caffeine 属性
    /**
     * expireAfterWrite:60
     * initialCapacity:5
     * maximumSize: 1_000
     */
    private Caffeine<Object, Object> cacheBuilder = Caffeine.newBuilder()
            .expireAfterWrite(DEFAULT_EXPIRE_AFTER_WRITE, TimeUnit.SECONDS)
            .initialCapacity(DEFAULT_INITIAL_CAPACITY)
            .maximumSize(DEFAULT_MAXIMUM_SIZE);

    // redis 属性
    /**
     * 操作redis的RedisTemplate
     */
    private final RedisOperations redisOperations;

    /**
     * 二级缓存使用使用前缀,默认是false,建议设置成true
     */
    private boolean usePrefix = false;
    private RedisCachePrefix cachePrefix = new DefaultRedisCachePrefix();

    /**
     * redis缓存默认时间,默认是0 永不过期
     */
    private long defaultExpiration = 0;

    public LayeringCacheManager(RedisOperations redisOperations) {
        this(redisOperations, Collections.<String>emptyList());
    }

    public LayeringCacheManager(RedisOperations redisOperations, Collection<String> cacheNames) {
        this(redisOperations, cacheNames, false);
    }

    public LayeringCacheManager(RedisOperations redisOperations, Collection<String> cacheNames, boolean allowNullValues) {
        this.allowNullValues = allowNullValues;
        this.redisOperations = redisOperations;

        setCacheNames(cacheNames);
    }

    @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 = createCache(name);
                    this.cacheMap.put(name, cache);
                }
            }
        }
        return cache;
    }

    @Override
    public Collection<String> getCacheNames() {
        return Collections.unmodifiableSet(this.cacheMap.keySet());
    }

    @SuppressWarnings("unchecked")
    protected Cache createCache(String name) {
        return new LayeringCache(name, (usePrefix ? cachePrefix.prefix(name) : null), redisOperations,
                getSecondaryCacheExpirationSecondTime(name), getSecondaryCachePreloadSecondTime(name),
                isAllowNullValues(), getUsedFirstCache(name), getForceRefresh(name), createNativeCaffeineCache(name));
    }

    /**
     * Create a native Caffeine Cache instance for the specified cache name.
     *
     * @param name the name of the cache
     * @return the native Caffeine Cache instance
     */
    protected com.github.benmanes.caffeine.cache.Cache<Object, Object> createNativeCaffeineCache(String name) {
        return getCaffeine(name).build();
    }

    /**
     * 使用该CacheManager的当前状态重新创建已知的缓存。
     */
    private void refreshKnownCaches() {
        for (Map.Entry<String, Cache> entry : this.cacheMap.entrySet()) {
            entry.setValue(createCache(entry.getKey()));
        }
    }

    /**
     * 在初始化CacheManager的时候初始化一组缓存。
     * 使用这个方法会在CacheManager初始化的时候就会将一组缓存初始化好,并且在运行时不会再去创建更多的缓存。
     * 使用空的Collection或者重新在配置里面指定dynamic后,就可重新在运行时动态的来创建缓存。
     *
     * @param cacheNames
     */
    public void setCacheNames(Collection<String> cacheNames) {
        if (cacheNames != null) {
            for (String name : cacheNames) {
                this.cacheMap.put(name, createCache(name));
            }
            this.dynamic = cacheNames.isEmpty();
        }
    }

    /**
     * 设置是否允许Cache的值为null
     *
     * @param allowNullValues
     */
    public void setAllowNullValues(boolean allowNullValues) {
        if (this.allowNullValues != allowNullValues) {
            this.allowNullValues = allowNullValues;
            refreshKnownCaches();
        }
    }

    /**
     * 获取是否允许Cache的值为null
     *
     * @return
     */
    public boolean isAllowNullValues() {
        return this.allowNullValues;
    }

    /**
     * 在生成key的时候是否是否使用缓存名称来作为缓存前缀。默认是false,但是建议设置成true。
     *
     * @param usePrefix
     */
    public void setUsePrefix(boolean usePrefix) {
        this.usePrefix = usePrefix;
    }

    protected boolean isUsePrefix() {
        return usePrefix;
    }

    /**
     * 设置redis默认的过期时间(单位:秒)
     *
     * @param defaultExpireTime
     */
    public void setSecondaryCacheDefaultExpiration(long defaultExpireTime) {
        this.defaultExpiration = defaultExpireTime;
    }


    /**
     * 根据缓存名称设置一级缓存的有效时间和刷新时间,单位秒
     *
     * @param firstCacheSettings
     */
    public void setFirstCacheSettings(Map<String, FirstCacheSetting> firstCacheSettings) {
        this.firstCacheSettings = (!CollectionUtils.isEmpty(firstCacheSettings) ? new ConcurrentHashMap<>(firstCacheSettings) : null);
    }

    /**
     * 根据缓存名称设置二级缓存的有效时间和刷新时间,单位秒
     *
     * @param secondaryCacheSettings
     */
    public void setSecondaryCacheSettings(Map<String, SecondaryCacheSetting> secondaryCacheSettings) {
        this.secondaryCacheSettings = (!CollectionUtils.isEmpty(secondaryCacheSettings) ? new ConcurrentHashMap<>(secondaryCacheSettings) : null);
    }


    /**
     * 获取过期时间
     *
     * @return
     */
    public long getSecondaryCacheExpirationSecondTime(String name) {
        if (StringUtils.isEmpty(name)) {
            return 0;
        }

        SecondaryCacheSetting secondaryCacheSetting = null;
        if (!CollectionUtils.isEmpty(secondaryCacheSettings)) {
            secondaryCacheSetting = secondaryCacheSettings.get(name);
        }
        Long expiration = secondaryCacheSetting != null ? secondaryCacheSetting.getExpirationSecondTime() : defaultExpiration;
        return expiration < 0 ? 0 : expiration;
    }

    /**
     * 获取自动刷新时间
     *
     * @return
     */
    private long getSecondaryCachePreloadSecondTime(String name) {
        // 自动刷新时间,默认是0
        SecondaryCacheSetting secondaryCacheSetting = null;
        if (!CollectionUtils.isEmpty(secondaryCacheSettings)) {
            secondaryCacheSetting = secondaryCacheSettings.get(name);
        }
        Long preloadSecondTime = secondaryCacheSetting != null ? secondaryCacheSetting.getPreloadSecondTime() : 0;
        return preloadSecondTime < 0 ? 0 : preloadSecondTime;
    }

    /**
     * 获取是否使用二级缓存,默认是true
     */
    public boolean getUsedFirstCache(String name) {
        SecondaryCacheSetting secondaryCacheSetting = null;
        if (!CollectionUtils.isEmpty(secondaryCacheSettings)) {
            secondaryCacheSetting = secondaryCacheSettings.get(name);
        }

        return secondaryCacheSetting != null ? secondaryCacheSetting.getUsedFirstCache() : true;
    }

    /**
     * 获取是否强制刷新(走数据库),默认是false
     */
    public boolean getForceRefresh(String name) {
        SecondaryCacheSetting secondaryCacheSetting = null;
        if (!CollectionUtils.isEmpty(secondaryCacheSettings)) {
            secondaryCacheSetting = secondaryCacheSettings.get(name);
        }

        return secondaryCacheSetting != null ? secondaryCacheSetting.getForceRefresh() : false;
    }

    public void setCaffeineSpec(CaffeineSpec caffeineSpec) {
        Caffeine<Object, Object> cacheBuilder = Caffeine.from(caffeineSpec);
        if (!ObjectUtils.nullSafeEquals(this.cacheBuilder, cacheBuilder)) {
            this.cacheBuilder = cacheBuilder;
            refreshKnownCaches();
        }
    }

    private Caffeine<Object, Object> getCaffeine(String name) {
        if (!CollectionUtils.isEmpty(firstCacheSettings)) {
            FirstCacheSetting firstCacheSetting = firstCacheSettings.get(name);
            if (firstCacheSetting != null && StringUtils.isNotBlank(firstCacheSetting.getCacheSpecification())) {
                // 根据缓存名称获取一级缓存配置
                return Caffeine.from(CaffeineSpec.parse(firstCacheSetting.getCacheSpecification()));
            }
        }

        return this.cacheBuilder;
    }
}
<dependency> <groupId>com.github.xiaolyuh</groupId> <artifactId>layering-cache-starter</artifactId> <version>${layering.version}</version></dependency>

FirstCacheSettings:

一级缓存配置类

public class FirstCacheSetting {

    /**
     * 一级缓存配置,配置项请点击这里 {@link CaffeineSpec#configure(String, String)}
     * @param cacheSpecification
     */
    public FirstCacheSetting(String cacheSpecification) {
        this.cacheSpecification = cacheSpecification;
    }

    private String cacheSpecification;

    public String getCacheSpecification() {
        return cacheSpecification;
    }
}

直接在需要缓存的方法上加上Cacheable、CacheEvict、CachePut注解。

SecondaryCacheSetting:

二级缓存的特殊配置类

/**
 * @author yuhao.wang
 */
public class SecondaryCacheSetting {

    /**
     * @param expirationSecondTime 设置redis缓存的有效时间,单位秒
     * @param preloadSecondTime    设置redis缓存的自动刷新时间,单位秒
     */
    public SecondaryCacheSetting(long expirationSecondTime, long preloadSecondTime) {
        this.expirationSecondTime = expirationSecondTime;
        this.preloadSecondTime = preloadSecondTime;
    }

    /**
     * @param usedFirstCache       是否启用一级缓存,默认true
     * @param expirationSecondTime 设置redis缓存的有效时间,单位秒
     * @param preloadSecondTime    设置redis缓存的自动刷新时间,单位秒
     */
    public SecondaryCacheSetting(boolean usedFirstCache, long expirationSecondTime, long preloadSecondTime) {
        this.expirationSecondTime = expirationSecondTime;
        this.preloadSecondTime = preloadSecondTime;
        this.usedFirstCache = usedFirstCache;
    }

    /**
     * @param expirationSecondTime 设置redis缓存的有效时间,单位秒
     * @param preloadSecondTime    设置redis缓存的自动刷新时间,单位秒
     * @param forceRefresh         是否使用强制刷新(走数据库),默认false
     */
    public SecondaryCacheSetting(long expirationSecondTime, long preloadSecondTime, boolean forceRefresh) {
        this.expirationSecondTime = expirationSecondTime;
        this.preloadSecondTime = preloadSecondTime;
        this.forceRefresh = forceRefresh;
    }

    /**
     * @param expirationSecondTime 设置redis缓存的有效时间,单位秒
     * @param preloadSecondTime    设置redis缓存的自动刷新时间,单位秒
     * @param usedFirstCache       是否启用一级缓存,默认true
     * @param forceRefresh         是否使用强制刷新(走数据库),默认false
     */
    public SecondaryCacheSetting(long expirationSecondTime, long preloadSecondTime, boolean usedFirstCache, boolean forceRefresh) {
        this.expirationSecondTime = expirationSecondTime;
        this.preloadSecondTime = preloadSecondTime;
        this.usedFirstCache = usedFirstCache;
        this.forceRefresh = forceRefresh;
    }

    /**
     * 缓存有效时间
     */
    private long expirationSecondTime;

    /**
     * 缓存主动在失效前强制刷新缓存的时间
     * 单位:秒
     */
    private long preloadSecondTime = 0;

    /**
     * 是否使用二级缓存,默认是true
     */
    private boolean usedFirstCache = true;

    /**
     * 是否使用强刷新(走数据库),默认是false
     */
    private boolean forceRefresh = false;

    public long getPreloadSecondTime() {
        return preloadSecondTime;
    }

    public long getExpirationSecondTime() {
        return expirationSecondTime;
    }

    public boolean getUsedFirstCache() {
        return usedFirstCache;
    }

    public boolean getForceRefresh() {
        return forceRefresh;
    }
}
  • Cacheable注解

使用方式

在上面我们定义好了LayeringCacheManager和LayeringCache接下来就是使用了。

新建一个配置类CacheConfig,在这里指定一个LayeringCacheManager的Bean。我那的缓存就生效了。完整代码如下:

/**
 * @author yuhao.wang
 */
@Configuration
@EnableConfigurationProperties(CacheProperties.class)
public class CacheConfig {

    // redis缓存的有效时间单位是秒
    @Value("${redis.default.expiration:3600}")
    private long redisDefaultExpiration;

    // 查询缓存有效时间
    @Value("${select.cache.timeout:1800}")
    private long selectCacheTimeout;
    // 查询缓存自动刷新时间
    @Value("${select.cache.refresh:1790}")
    private long selectCacheRefresh;

    @Autowired
    private CacheProperties cacheProperties;

    @Bean
    @Primary
    public CacheManager cacheManager(RedisTemplate<String, Object> redisTemplate) {
        LayeringCacheManager layeringCacheManager = new LayeringCacheManager(redisTemplate);
        // Caffeine缓存设置
        setFirstCacheConfig(layeringCacheManager);

        // redis缓存设置
        setSecondaryCacheConfig(layeringCacheManager);
        return layeringCacheManager;
    }

    private void setFirstCacheConfig(LayeringCacheManager layeringCacheManager) {
        // 设置默认的一级缓存配置
        String specification = this.cacheProperties.getCaffeine().getSpec();
        if (StringUtils.hasText(specification)) {
            layeringCacheManager.setCaffeineSpec(CaffeineSpec.parse(specification));
        }

        // 设置每个一级缓存的过期时间和自动刷新时间
        Map<String, FirstCacheSetting> firstCacheSettings = new HashMap<>();
        firstCacheSettings.put("people", new FirstCacheSetting("initialCapacity=5,maximumSize=500,expireAfterWrite=10s"));
        firstCacheSettings.put("people1", new FirstCacheSetting("initialCapacity=5,maximumSize=50,expireAfterAccess=10s"));
        layeringCacheManager.setFirstCacheSettings(firstCacheSettings);
    }

    private void setSecondaryCacheConfig(LayeringCacheManager layeringCacheManager) {
        // 设置使用缓存名称(value属性)作为redis缓存前缀
        layeringCacheManager.setUsePrefix(true);
        //这里可以设置一个默认的过期时间 单位是秒
        layeringCacheManager.setSecondaryCacheDefaultExpiration(redisDefaultExpiration);

        // 设置每个二级缓存的过期时间和自动刷新时间
        Map<String, SecondaryCacheSetting> secondaryCacheSettings = new HashMap<>();
        secondaryCacheSettings.put("people", new SecondaryCacheSetting(selectCacheTimeout, selectCacheRefresh));
        secondaryCacheSettings.put("people1", new SecondaryCacheSetting(selectCacheTimeout, selectCacheRefresh, true));
        secondaryCacheSettings.put("people2", new SecondaryCacheSetting(false, selectCacheTimeout, selectCacheRefresh));
        secondaryCacheSettings.put("people3", new SecondaryCacheSetting(selectCacheTimeout, selectCacheRefresh, false, true));
        layeringCacheManager.setSecondaryCacheSettings(secondaryCacheSettings);
    }

    /**
     * 显示声明缓存key生成器
     *
     * @return
     */
    @Bean
    public KeyGenerator keyGenerator() {

        return new SimpleKeyGenerator();
    }

}

在cacheManager中指定Bean的时候,我们通过调用LayeringCacheManager 的setFirstCacheSettings和setSecondaryCacheSettings方法为缓存设置一级缓存和二级缓存的特殊配置。

剩下的就是在Service方法上加注解了,如:

@Override
@Cacheable(value = "people1", key = "#person.id", sync = true)//3
public Person findOne1(Person person, String a, String[] b, List<Long> c) {
    Person p = personRepository.findOne(person.getId());
    logger.info("为id、key为:" + p.getId() + "数据做了缓存");
    return p;
}

@Cacheable的sync属性建议设置成true。

测试

最后通过jmeter测试,50个线程,使用多级缓存,比只使用redis级缓存性能提升2倍多,只是用redis吞吐量在1243左右,使用多级缓存后在2639左右。

源码地址:
https://github.com/wyh-spring-ecosystem-student/spring-boot-student/tree/releases

spring-boot-student-cache-redis-caffeine 工程

图片 7

版权声明:本文由龙竞技官网发布于龙竞技官网,转载请注明出处:为监控而生的多级缓存框架 layering-cache