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

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

内容说明:

  • 缓存、两级缓存
  • spring cache:主要包含spring cache定义的接口方法说明和注解中的属性说明
  • spring boot + spring cache:RedisCache实现中的缺陷
  • caffeine简介
  • spring boot + spring cache 实现两级缓存(redis + caffeine)

缓存、两级缓存


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

spring cache


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

使用缓存的一般流程

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

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

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

spring cache包结构

Cache接口

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

spring框架中默认实现的Cache

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

spring-context-support包中的Cache接口实现类

1#Cache.java 2 3package org.springframework.cache; 4 5import java.util.concurrent.Callable; 6 7public interface Cache { 8 9 // cacheName,缓存的名字,默认实现中一般是CacheManager创建Cache的bean时传入cacheName 10 String getName(); 11 12 // 获取实际使用的缓存,如:RedisTemplate、com.github.benmanes.caffeine.cache.Cache<Object, Object>。暂时没发现实际用处,可能只是提供获取原生缓存的bean,以便需要扩展一些缓存操作或统计之类的东西 13 Object getNativeCache(); 14 15 // 通过key获取缓存值,注意返回的是ValueWrapper,为了兼容存储空值的情况,将返回值包装了一层,通过get方法获取实际值 16 ValueWrapper get(Object key); 17 18 // 通过key获取缓存值,返回的是实际值,即方法的返回值类型 19 <T> T get(Object key, Class<T> type); 20 21 // 通过key获取缓存值,可以使用valueLoader.call()来调使用@Cacheable注解的方法。当@Cacheable注解的sync属性配置为true时使用此方法。因此方法内需要保证回源到数据库的同步性。避免在缓存失效时大量请求回源到数据库 22 <T> T get(Object key, Callable<T> valueLoader); 23 24 // 将@Cacheable注解方法返回的数据放入缓存中 25 void put(Object key, Object value); 26 27 // 当缓存中不存在key时才放入缓存。返回值是当key存在时原有的数据 28 ValueWrapper putIfAbsent(Object key, Object value); 29 30 // 删除缓存 31 void evict(Object key); 32 33 // 删除缓存中的所有数据。需要注意的是,具体实现中只删除使用@Cacheable注解缓存的所有数据,不要影响应用内的其他缓存 34 void clear(); 35 36 // 缓存返回值的包装 37 interface ValueWrapper { 38 39 // 返回实际缓存的对象 40 Object get(); 41 } 42 43 // 当{@link #get(Object, Callable)}抛出异常时,会包装成此异常抛出 44 @SuppressWarnings("serial") 45 class ValueRetrievalException extends RuntimeException { 46 47 private final Object key; 48 49 public ValueRetrievalException(Object key, Callable<?> loader, Throwable ex) { 50 super(String.format("Value for key '%s' could not be loaded using '%s'", key, loader), ex); 51 this.key = key; 52 } 53 54 public Object getKey() { 55 return this.key; 56 } 57 } 58} 59 60

CacheManager接口

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

1#CacheManager.java 2 3package org.springframework.cache; 4 5import java.util.Collection; 6 7public interface CacheManager { 8 9 // 通过cacheName创建Cache的实现bean,具体实现中需要存储已创建的Cache实现bean,避免重复创建,也避免内存缓存对象(如Caffeine)重新创建后原来缓存内容丢失的情况 10 Cache getCache(String name); 11 12 // 返回所有的cacheName 13 Collection<String> getCacheNames(); 14} 15 16

常用注解说明

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

1package org.springframework.cache.annotation; 2 3import java.lang.annotation.Documented; 4import java.lang.annotation.ElementType; 5import java.lang.annotation.Inherited; 6import java.lang.annotation.Retention; 7import java.lang.annotation.RetentionPolicy; 8import java.lang.annotation.Target; 9import java.util.concurrent.Callable; 10 11import org.springframework.core.annotation.AliasFor; 12 13@Target({ElementType.METHOD, ElementType.TYPE}) 14@Retention(RetentionPolicy.RUNTIME) 15@Inherited 16@Documented 17public @interface Cacheable { 18 19 // cacheNames,CacheManager就是通过这个名称创建对应的Cache实现bean 20 @AliasFor("cacheNames") 21 String[] value() default {}; 22 23 @AliasFor("value") 24 String[] cacheNames() default {}; 25 26 // 缓存的key,支持SpEL表达式。默认是使用所有参数及其计算的hashCode包装后的对象(SimpleKey) 27 String key() default ""; 28 29 // 缓存key生成器,默认实现是SimpleKeyGenerator 30 String keyGenerator() default ""; 31 32 // 指定使用哪个CacheManager 33 String cacheManager() default ""; 34 35 // 缓存解析器 36 String cacheResolver() default ""; 37 38 // 缓存的条件,支持SpEL表达式,当达到满足的条件时才缓存数据。在调用方法前后都会判断 39 String condition() default ""; 40 41 // 满足条件时不更新缓存,支持SpEL表达式,只在调用方法后判断 42 String unless() default ""; 43 44 // 回源到实际方法获取数据时,是否要保持同步,如果为false,调用的是Cache.get(key)方法;如果为true,调用的是Cache.get(key, Callable)方法 45 boolean sync() default false; 46 47} 48 49
  • @CacheEvict:清除缓存,主要应用到删除数据的方法上。相比Cacheable多了两个属性

1package org.springframework.cache.annotation; 2 3import java.lang.annotation.Documented; 4import java.lang.annotation.ElementType; 5import java.lang.annotation.Inherited; 6import java.lang.annotation.Retention; 7import java.lang.annotation.RetentionPolicy; 8import java.lang.annotation.Target; 9 10import org.springframework.core.annotation.AliasFor; 11 12@Target({ElementType.METHOD, ElementType.TYPE}) 13@Retention(RetentionPolicy.RUNTIME) 14@Inherited 15@Documented 16public @interface CacheEvict { 17 18 // ...相同属性说明请参考@Cacheable中的说明 19 20 // 是否要清除所有缓存的数据,为false时调用的是Cache.evict(key)方法;为true时调用的是Cache.clear()方法 21 boolean allEntries() default false; 22 23 // 调用方法之前或之后清除缓存 24 boolean beforeInvocation() default false; 25} 26 27
  • @CachePut:放入缓存,主要用到对数据有更新的方法上。属性说明参考@Cacheable

  • @Caching:用于在一个方法上配置多种注解

  • @EnableCaching:启用spring cache缓存,作为总的开关,在spring boot的启动类或配置类上需要加上此注解才会生效

spring boot + spring cache


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

spring boot autoconfigure中的缓存配置

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

1package com.itopener.demo.cache.redis.config; 2 3import java.util.Map; 4import java.util.concurrent.ConcurrentHashMap; 5 6import org.springframework.boot.autoconfigure.cache.CacheManagerCustomizer; 7import org.springframework.data.redis.cache.RedisCacheManager; 8 9public class RedisCacheManagerCustomizer implements CacheManagerCustomizer<RedisCacheManager> { 10 11 @Override 12 public void customize(RedisCacheManager cacheManager) { 13 // 默认过期时间,单位秒 14 cacheManager.setDefaultExpiration(1000); 15 cacheManager.setUsePrefix(false); 16 Map<String, Long> expires = new ConcurrentHashMap<String, Long>(); 17 expires.put("userIdCache", 2000L); 18 cacheManager.setExpires(expires); 19 } 20 21} 22 23

加载这个bean:

1package com.itopener.demo.cache.redis.config; 2 3import org.springframework.context.annotation.Bean; 4import org.springframework.context.annotation.Configuration; 5 6/** 7 * @author fuwei.deng 8 * @date 2017年12月22日 上午10:24:54 9 * @version 1.0.0 10 */ 11@Configuration 12public class CacheRedisConfiguration { 13 14 @Bean 15 public RedisCacheManagerCustomizer redisCacheManagerCustomizer() { 16 return new RedisCacheManagerCustomizer(); 17 } 18} 19 20

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

spring-data-redis中spring cache的实现

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

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

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

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

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

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

Caffeine


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

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

caffeine的介绍可以参考:http://www.cnblogs.com/oopsguy/p/7731659.html

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

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

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配置属性类

1package com.itopener.cache.redis.caffeine.spring.boot.autoconfigure; 2 3import java.util.HashMap; 4import java.util.HashSet; 5import java.util.Map; 6import java.util.Set; 7 8import org.springframework.boot.context.properties.ConfigurationProperties; 9 10/** 11 * @author fuwei.deng 12 * @date 2018年1月29日 上午11:32:15 13 * @version 1.0.0 14 */ 15@ConfigurationProperties(prefix = "spring.cache.multi") 16public class CacheRedisCaffeineProperties { 17 18 private Set<String> cacheNames = new HashSet<>(); 19 20 /** 是否存储空值,默认true,防止缓存穿透*/ 21 private boolean cacheNullValues = true; 22 23 /** 是否动态根据cacheName创建Cache的实现,默认true*/ 24 private boolean dynamic = true; 25 26 /** 缓存key的前缀*/ 27 private String cachePrefix; 28 29 private Redis redis = new Redis(); 30 31 private Caffeine caffeine = new Caffeine(); 32 33 public class Redis { 34 35 /** 全局过期时间,单位毫秒,默认不过期*/ 36 private long defaultExpiration = 0; 37 38 /** 每个cacheName的过期时间,单位毫秒,优先级比defaultExpiration高*/ 39 private Map<String, Long> expires = new HashMap<>(); 40 41 /** 缓存更新时通知其他节点的topic名称*/ 42 private String topic = "cache:redis:caffeine:topic"; 43 44 public long getDefaultExpiration() { 45 return defaultExpiration; 46 } 47 48 public void setDefaultExpiration(long defaultExpiration) { 49 this.defaultExpiration = defaultExpiration; 50 } 51 52 public Map<String, Long> getExpires() { 53 return expires; 54 } 55 56 public void setExpires(Map<String, Long> expires) { 57 this.expires = expires; 58 } 59 60 public String getTopic() { 61 return topic; 62 } 63 64 public void setTopic(String topic) { 65 this.topic = topic; 66 } 67 68 } 69 70 public class Caffeine { 71 72 /** 访问后过期时间,单位毫秒*/ 73 private long expireAfterAccess; 74 75 /** 写入后过期时间,单位毫秒*/ 76 private long expireAfterWrite; 77 78 /** 写入后刷新时间,单位毫秒*/ 79 private long refreshAfterWrite; 80 81 /** 初始化大小*/ 82 private int initialCapacity; 83 84 /** 最大缓存对象个数,超过此数量时之前放入的缓存将失效*/ 85 private long maximumSize; 86 87 /** 由于权重需要缓存对象来提供,对于使用spring cache这种场景不是很适合,所以暂不支持配置*/ 88// private long maximumWeight; 89 90 public long getExpireAfterAccess() { 91 return expireAfterAccess; 92 } 93 94 public void setExpireAfterAccess(long expireAfterAccess) { 95 this.expireAfterAccess = expireAfterAccess; 96 } 97 98 public long getExpireAfterWrite() { 99 return expireAfterWrite; 100 } 101 102 public void setExpireAfterWrite(long expireAfterWrite) { 103 this.expireAfterWrite = expireAfterWrite; 104 } 105 106 public long getRefreshAfterWrite() { 107 return refreshAfterWrite; 108 } 109 110 public void setRefreshAfterWrite(long refreshAfterWrite) { 111 this.refreshAfterWrite = refreshAfterWrite; 112 } 113 114 public int getInitialCapacity() { 115 return initialCapacity; 116 } 117 118 public void setInitialCapacity(int initialCapacity) { 119 this.initialCapacity = initialCapacity; 120 } 121 122 public long getMaximumSize() { 123 return maximumSize; 124 } 125 126 public void setMaximumSize(long maximumSize) { 127 this.maximumSize = maximumSize; 128 } 129 } 130 131 public Set<String> getCacheNames() { 132 return cacheNames; 133 } 134 135 public void setCacheNames(Set<String> cacheNames) { 136 this.cacheNames = cacheNames; 137 } 138 139 public boolean isCacheNullValues() { 140 return cacheNullValues; 141 } 142 143 public void setCacheNullValues(boolean cacheNullValues) { 144 this.cacheNullValues = cacheNullValues; 145 } 146 147 public boolean isDynamic() { 148 return dynamic; 149 } 150 151 public void setDynamic(boolean dynamic) { 152 this.dynamic = dynamic; 153 } 154 155 public String getCachePrefix() { 156 return cachePrefix; 157 } 158 159 public void setCachePrefix(String cachePrefix) { 160 this.cachePrefix = cachePrefix; 161 } 162 163 public Redis getRedis() { 164 return redis; 165 } 166 167 public void setRedis(Redis redis) { 168 this.redis = redis; 169 } 170 171 public Caffeine getCaffeine() { 172 return caffeine; 173 } 174 175 public void setCaffeine(Caffeine caffeine) { 176 this.caffeine = caffeine; 177 } 178 179} 180 181
  • spring cache中有实现Cache接口的一个抽象类AbstractValueAdaptingCache,包含了空值的包装和缓存值的包装,所以就不用实现Cache接口了,直接实现AbstractValueAdaptingCache抽象类

1package com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.support; 2 3import java.lang.reflect.Constructor; 4import java.util.Map; 5import java.util.Set; 6import java.util.concurrent.Callable; 7import java.util.concurrent.TimeUnit; 8import java.util.concurrent.locks.ReentrantLock; 9 10import org.slf4j.Logger; 11import org.slf4j.LoggerFactory; 12import org.springframework.cache.support.AbstractValueAdaptingCache; 13import org.springframework.data.redis.core.RedisTemplate; 14import org.springframework.util.StringUtils; 15 16import com.github.benmanes.caffeine.cache.Cache; 17import com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.CacheRedisCaffeineProperties; 18 19/** 20 * @author fuwei.deng 21 * @date 2018年1月26日 下午5:24:11 22 * @version 1.0.0 23 */ 24public class RedisCaffeineCache extends AbstractValueAdaptingCache { 25 26 private final Logger logger = LoggerFactory.getLogger(RedisCaffeineCache.class); 27 28 private String name; 29 30 private RedisTemplate<Object, Object> redisTemplate; 31 32 private Cache<Object, Object> caffeineCache; 33 34 private String cachePrefix; 35 36 private long defaultExpiration = 0; 37 38 private Map<String, Long> expires; 39 40 private String topic = "cache:redis:caffeine:topic"; 41 42 protected RedisCaffeineCache(boolean allowNullValues) { 43 super(allowNullValues); 44 } 45 46 public RedisCaffeineCache(String name, RedisTemplate<Object, Object> redisTemplate, Cache<Object, Object> caffeineCache, CacheRedisCaffeineProperties cacheRedisCaffeineProperties) { 47 super(cacheRedisCaffeineProperties.isCacheNullValues()); 48 this.name = name; 49 this.redisTemplate = redisTemplate; 50 this.caffeineCache = caffeineCache; 51 this.cachePrefix = cacheRedisCaffeineProperties.getCachePrefix(); 52 this.defaultExpiration = cacheRedisCaffeineProperties.getRedis().getDefaultExpiration(); 53 this.expires = cacheRedisCaffeineProperties.getRedis().getExpires(); 54 this.topic = cacheRedisCaffeineProperties.getRedis().getTopic(); 55 } 56 57 @Override 58 public String getName() { 59 return this.name; 60 } 61 62 @Override 63 public Object getNativeCache() { 64 return this; 65 } 66 67 @SuppressWarnings("unchecked") 68 @Override 69 public <T> T get(Object key, Callable<T> valueLoader) { 70 Object value = lookup(key); 71 if(value != null) { 72 return (T) value; 73 } 74 75 ReentrantLock lock = new ReentrantLock(); 76 try { 77 lock.lock(); 78 value = lookup(key); 79 if(value != null) { 80 return (T) value; 81 } 82 value = valueLoader.call(); 83 Object storeValue = toStoreValue(valueLoader.call()); 84 put(key, storeValue); 85 return (T) value; 86 } catch (Exception e) { 87 try { 88 Class<?> c = Class.forName("org.springframework.cache.Cache$ValueRetrievalException"); 89 Constructor<?> constructor = c.getConstructor(Object.class, Callable.class, Throwable.class); 90 RuntimeException exception = (RuntimeException) constructor.newInstance(key, valueLoader, e.getCause()); 91 throw exception; 92 } catch (Exception e1) { 93 throw new IllegalStateException(e1); 94 } 95 } finally { 96 lock.unlock(); 97 } 98 } 99 100 @Override 101 public void put(Object key, Object value) { 102 if (!super.isAllowNullValues() && value == null) { 103 this.evict(key); 104 return; 105 } 106 long expire = getExpire(); 107 if(expire > 0) { 108 redisTemplate.opsForValue().set(getKey(key), toStoreValue(value), expire, TimeUnit.MILLISECONDS); 109 } else { 110 redisTemplate.opsForValue().set(getKey(key), toStoreValue(value)); 111 } 112 113 push(new CacheMessage(this.name, key)); 114 115 caffeineCache.put(key, value); 116 } 117 118 @Override 119 public ValueWrapper putIfAbsent(Object key, Object value) { 120 Object cacheKey = getKey(key); 121 Object prevValue = null; 122 // 考虑使用分布式锁,或者将redis的setIfAbsent改为原子性操作 123 synchronized (key) { 124 prevValue = redisTemplate.opsForValue().get(cacheKey); 125 if(prevValue == null) { 126 long expire = getExpire(); 127 if(expire > 0) { 128 redisTemplate.opsForValue().set(getKey(key), toStoreValue(value), expire, TimeUnit.MILLISECONDS); 129 } else { 130 redisTemplate.opsForValue().set(getKey(key), toStoreValue(value)); 131 } 132 133 push(new CacheMessage(this.name, key)); 134 135 caffeineCache.put(key, toStoreValue(value)); 136 } 137 } 138 return toValueWrapper(prevValue); 139 } 140 141 @Override 142 public void evict(Object key) { 143 // 先清除redis中缓存数据,然后清除caffeine中的缓存,避免短时间内如果先清除caffeine缓存后其他请求会再从redis里加载到caffeine中 144 redisTemplate.delete(getKey(key)); 145 146 push(new CacheMessage(this.name, key)); 147 148 caffeineCache.invalidate(key); 149 } 150 151 @Override 152 public void clear() { 153 // 先清除redis中缓存数据,然后清除caffeine中的缓存,避免短时间内如果先清除caffeine缓存后其他请求会再从redis里加载到caffeine中 154 Set<Object> keys = redisTemplate.keys(this.name.concat(":")); 155 for(Object key : keys) { 156 redisTemplate.delete(key); 157 } 158 159 push(new CacheMessage(this.name, null)); 160 161 caffeineCache.invalidateAll(); 162 } 163 164 @Override 165 protected Object lookup(Object key) { 166 Object cacheKey = getKey(key); 167 Object value = caffeineCache.getIfPresent(key); 168 if(value != null) { 169 logger.debug("get cache from caffeine, the key is : {}", cacheKey); 170 return value; 171 } 172 173 value = redisTemplate.opsForValue().get(cacheKey); 174 175 if(value != null) { 176 logger.debug("get cache from redis and put in caffeine, the key is : {}", cacheKey); 177 caffeineCache.put(key, value); 178 } 179 return value; 180 } 181 182 private Object getKey(Object key) { 183 return this.name.concat(":").concat(StringUtils.isEmpty(cachePrefix) ? key.toString() : cachePrefix.concat(":").concat(key.toString())); 184 } 185 186 private long getExpire() { 187 long expire = defaultExpiration; 188 Long cacheNameExpire = expires.get(this.name); 189 return cacheNameExpire == null ? expire : cacheNameExpire.longValue(); 190 } 191 192 /** 193 * @description 缓存变更时通知其他节点清理本地缓存 194 * @author fuwei.deng 195 * @date 2018年1月31日 下午3:20:28 196 * @version 1.0.0 197 * @param message 198 */ 199 private void push(CacheMessage message) { 200 redisTemplate.convertAndSend(topic, message); 201 } 202 203 /** 204 * @description 清理本地缓存 205 * @author fuwei.deng 206 * @date 2018年1月31日 下午3:15:39 207 * @version 1.0.0 208 * @param key 209 */ 210 public void clearLocal(Object key) { 211 logger.debug("clear local cache, the key is : {}", key); 212 if(key == null) { 213 caffeineCache.invalidateAll(); 214 } else { 215 caffeineCache.invalidate(key); 216 } 217 } 218} 219 220
  • 实现CacheManager接口

1package com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.support; 2 3import java.util.Collection; 4import java.util.Set; 5import java.util.concurrent.ConcurrentHashMap; 6import java.util.concurrent.ConcurrentMap; 7import java.util.concurrent.TimeUnit; 8 9import org.slf4j.Logger; 10import org.slf4j.LoggerFactory; 11import org.springframework.cache.Cache; 12import org.springframework.cache.CacheManager; 13import org.springframework.data.redis.core.RedisTemplate; 14 15import com.github.benmanes.caffeine.cache.Caffeine; 16import com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.CacheRedisCaffeineProperties; 17 18/** 19 * @author fuwei.deng 20 * @date 2018年1月26日 下午5:24:52 21 * @version 1.0.0 22 */ 23public class RedisCaffeineCacheManager implements CacheManager { 24 25 private final Logger logger = LoggerFactory.getLogger(RedisCaffeineCacheManager.class); 26 27 private ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap<String, Cache>(); 28 29 private CacheRedisCaffeineProperties cacheRedisCaffeineProperties; 30 31 private RedisTemplate<Object, Object> redisTemplate; 32 33 private boolean dynamic = true; 34 35 private Set<String> cacheNames; 36 37 public RedisCaffeineCacheManager(CacheRedisCaffeineProperties cacheRedisCaffeineProperties, 38 RedisTemplate<Object, Object> redisTemplate) { 39 super(); 40 this.cacheRedisCaffeineProperties = cacheRedisCaffeineProperties; 41 this.redisTemplate = redisTemplate; 42 this.dynamic = cacheRedisCaffeineProperties.isDynamic(); 43 this.cacheNames = cacheRedisCaffeineProperties.getCacheNames(); 44 } 45 46 @Override 47 public Cache getCache(String name) { 48 Cache cache = cacheMap.get(name); 49 if(cache != null) { 50 return cache; 51 } 52 if(!dynamic && !cacheNames.contains(name)) { 53 return cache; 54 } 55 56 cache = new RedisCaffeineCache(name, redisTemplate, caffeineCache(), cacheRedisCaffeineProperties); 57 Cache oldCache = cacheMap.putIfAbsent(name, cache); 58 logger.debug("create cache instance, the cache name is : {}", name); 59 return oldCache == null ? cache : oldCache; 60 } 61 62 public com.github.benmanes.caffeine.cache.Cache<Object, Object> caffeineCache(){ 63 Caffeine<Object, Object> cacheBuilder = Caffeine.newBuilder(); 64 if(cacheRedisCaffeineProperties.getCaffeine().getExpireAfterAccess() > 0) { 65 cacheBuilder.expireAfterAccess(cacheRedisCaffeineProperties.getCaffeine().getExpireAfterAccess(), TimeUnit.MILLISECONDS); 66 } 67 if(cacheRedisCaffeineProperties.getCaffeine().getExpireAfterWrite() > 0) { 68 cacheBuilder.expireAfterWrite(cacheRedisCaffeineProperties.getCaffeine().getExpireAfterWrite(), TimeUnit.MILLISECONDS); 69 } 70 if(cacheRedisCaffeineProperties.getCaffeine().getInitialCapacity() > 0) { 71 cacheBuilder.initialCapacity(cacheRedisCaffeineProperties.getCaffeine().getInitialCapacity()); 72 } 73 if(cacheRedisCaffeineProperties.getCaffeine().getMaximumSize() > 0) { 74 cacheBuilder.maximumSize(cacheRedisCaffeineProperties.getCaffeine().getMaximumSize()); 75 } 76 if(cacheRedisCaffeineProperties.getCaffeine().getRefreshAfterWrite() > 0) { 77 cacheBuilder.refreshAfterWrite(cacheRedisCaffeineProperties.getCaffeine().getRefreshAfterWrite(), TimeUnit.MILLISECONDS); 78 } 79 return cacheBuilder.build(); 80 } 81 82 @Override 83 public Collection<String> getCacheNames() { 84 return this.cacheNames; 85 } 86 87 public void clearLocal(String cacheName, Object key) { 88 Cache cache = cacheMap.get(cacheName); 89 if(cache == null) { 90 return ; 91 } 92 93 RedisCaffeineCache redisCaffeineCache = (RedisCaffeineCache) cache; 94 redisCaffeineCache.clearLocal(key); 95 } 96} 97 98
  • redis消息发布/订阅,传输的消息类

1package com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.support; 2 3import java.io.Serializable; 4 5/** 6 * @author fuwei.deng 7 * @date 2018年1月29日 下午1:31:17 8 * @version 1.0.0 9 */ 10public class CacheMessage implements Serializable { 11 12 /** */ 13 private static final long serialVersionUID = 5987219310442078193L; 14 15 private String cacheName; 16 17 private Object key; 18 19 public CacheMessage(String cacheName, Object key) { 20 super(); 21 this.cacheName = cacheName; 22 this.key = key; 23 } 24 25 public String getCacheName() { 26 return cacheName; 27 } 28 29 public void setCacheName(String cacheName) { 30 this.cacheName = cacheName; 31 } 32 33 public Object getKey() { 34 return key; 35 } 36 37 public void setKey(Object key) { 38 this.key = key; 39 } 40 41} 42 43
  • 监听redis消息需要实现MessageListener接口

1package com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.support; 2 3import org.slf4j.Logger; 4import org.slf4j.LoggerFactory; 5import org.springframework.data.redis.connection.Message; 6import org.springframework.data.redis.connection.MessageListener; 7import org.springframework.data.redis.core.RedisTemplate; 8 9/** 10 * @author fuwei.deng 11 * @date 2018年1月30日 下午5:22:33 12 * @version 1.0.0 13 */ 14public class CacheMessageListener implements MessageListener { 15 16 private final Logger logger = LoggerFactory.getLogger(CacheMessageListener.class); 17 18 private RedisTemplate<Object, Object> redisTemplate; 19 20 private RedisCaffeineCacheManager redisCaffeineCacheManager; 21 22 public CacheMessageListener(RedisTemplate<Object, Object> redisTemplate, 23 RedisCaffeineCacheManager redisCaffeineCacheManager) { 24 super(); 25 this.redisTemplate = redisTemplate; 26 this.redisCaffeineCacheManager = redisCaffeineCacheManager; 27 } 28 29 @Override 30 public void onMessage(Message message, byte[] pattern) { 31 CacheMessage cacheMessage = (CacheMessage) redisTemplate.getValueSerializer().deserialize(message.getBody()); 32 logger.debug("recevice a redis topic message, clear local cache, the cacheName is {}, the key is {}", cacheMessage.getCacheName(), cacheMessage.getKey()); 33 redisCaffeineCacheManager.clearLocal(cacheMessage.getCacheName(), cacheMessage.getKey()); 34 } 35 36} 37 38
  • 增加spring boot配置类

1package com.itopener.cache.redis.caffeine.spring.boot.autoconfigure; 2 3import org.springframework.beans.factory.annotation.Autowired; 4import org.springframework.boot.autoconfigure.AutoConfigureAfter; 5import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; 6import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; 7import org.springframework.boot.context.properties.EnableConfigurationProperties; 8import org.springframework.context.annotation.Bean; 9import org.springframework.context.annotation.Configuration; 10import org.springframework.data.redis.core.RedisTemplate; 11import org.springframework.data.redis.listener.ChannelTopic; 12import org.springframework.data.redis.listener.RedisMessageListenerContainer; 13 14import com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.support.CacheMessageListener; 15import com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.support.RedisCaffeineCacheManager; 16 17/** 18 * @author fuwei.deng 19 * @date 2018年1月26日 下午5:23:03 20 * @version 1.0.0 21 */ 22@Configuration 23@AutoConfigureAfter(RedisAutoConfiguration.class) 24@EnableConfigurationProperties(CacheRedisCaffeineProperties.class) 25public class CacheRedisCaffeineAutoConfiguration { 26 27 @Autowired 28 private CacheRedisCaffeineProperties cacheRedisCaffeineProperties; 29 30 @Bean 31 @ConditionalOnBean(RedisTemplate.class) 32 public RedisCaffeineCacheManager cacheManager(RedisTemplate<Object, Object> redisTemplate) { 33 return new RedisCaffeineCacheManager(cacheRedisCaffeineProperties, redisTemplate); 34 } 35 36 @Bean 37 public RedisMessageListenerContainer redisMessageListenerContainer(RedisTemplate<Object, Object> redisTemplate, 38 RedisCaffeineCacheManager redisCaffeineCacheManager) { 39 RedisMessageListenerContainer redisMessageListenerContainer = new RedisMessageListenerContainer(); 40 redisMessageListenerContainer.setConnectionFactory(redisTemplate.getConnectionFactory()); 41 CacheMessageListener cacheMessageListener = new CacheMessageListener(redisTemplate, redisCaffeineCacheManager); 42 redisMessageListenerContainer.addMessageListener(cacheMessageListener, new ChannelTopic(cacheRedisCaffeineProperties.getRedis().getTopic())); 43 return redisMessageListenerContainer; 44 } 45} 46 47
  • 在resources/META-INF/spring.factories文件中增加spring boot配置扫描

1# Auto Configure 2org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ 3com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.CacheRedisCaffeineAutoConfiguration 4 5
  • 接下来就可以使用maven引入使用了

1<dependency> 2 <groupId>com.itopener</groupId> 3 <artifactId>cache-redis-caffeine-spring-boot-starter</artifactId> 4 <version>1.0.0-SNAPSHOT</version> 5 <type>pom</type> 6</dependency> 7 8
  • 在启动类上增加@EnableCaching注解,在需要缓存的方法上增加@Cacheable注解

1package com.itopener.demo.cache.redis.caffeine.service; 2 3import java.util.Random; 4 5import org.slf4j.Logger; 6import org.slf4j.LoggerFactory; 7import org.springframework.cache.annotation.CacheEvict; 8import org.springframework.cache.annotation.CachePut; 9import org.springframework.cache.annotation.Cacheable; 10import org.springframework.stereotype.Service; 11 12import com.itopener.demo.cache.redis.caffeine.vo.UserVO; 13import com.itopener.utils.TimestampUtil; 14 15@Service 16public class CacheRedisCaffeineService { 17 18 private final Logger logger = LoggerFactory.getLogger(CacheRedisCaffeineService.class); 19 20 @Cacheable(key = "'cache_user_id_' + #id", value = "userIdCache", cacheManager = "cacheManager") 21 public UserVO get(long id) { 22 logger.info("get by id from db"); 23 UserVO user = new UserVO(); 24 user.setId(id); 25 user.setName("name" + id); 26 user.setCreateTime(TimestampUtil.current()); 27 return user; 28 } 29 30 @Cacheable(key = "'cache_user_name_' + #name", value = "userNameCache", cacheManager = "cacheManager") 31 public UserVO get(String name) { 32 logger.info("get by name from db"); 33 UserVO user = new UserVO(); 34 user.setId(new Random().nextLong()); 35 user.setName(name); 36 user.setCreateTime(TimestampUtil.current()); 37 return user; 38 } 39 40 @CachePut(key = "'cache_user_id_' + #userVO.id", value = "userIdCache", cacheManager = "cacheManager") 41 public UserVO update(UserVO userVO) { 42 logger.info("update to db"); 43 userVO.setCreateTime(TimestampUtil.current()); 44 return userVO; 45 } 46 47 @CacheEvict(key = "'cache_user_id_' + #id", value = "userIdCache", cacheManager = "cacheManager") 48 public void delete(long id) { 49 logger.info("delete from db"); 50 } 51} 52 53
  • properties文件中redis的配置跟使用redis是一样的,可以增加两级缓存的配置

1#两级缓存的配置 2spring.cache.multi.caffeine.expireAfterAccess=5000 3spring.cache.multi.redis.defaultExpiration=60000 4 5#spring cache配置 6spring.cache.cache-names=userIdCache,userNameCache 7 8#redis配置 9#spring.redis.timeout=10000 10#spring.redis.password=redispwd 11#redis pool 12#spring.redis.pool.maxIdle=10 13#spring.redis.pool.minIdle=2 14#spring.redis.pool.maxActive=10 15#spring.redis.pool.maxWait=3000 16#redis cluster 17spring.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 18spring.redis.cluster.maxRedirects=3 19 20

扩展


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

  • 对于spring cache缓存的实现没有那么多的缺陷

    • 使用redis的HASH结构,可以针对不同的hashKey设置过期时间,清理的时候会更方便
    • 如果基于redisson来实现多级缓存,可以继承RedissonCache,在对应方法增加一级缓存的操作即可
    • 如果有使用分布式锁的情况就更方便了,可以直接使用Redisson中封装的分布式锁
    • redisson中的发布订阅封装得更好用
  • 后续可以增加对于缓存命中率的统计endpoint,这样就可以更好的监控各个缓存的命中情况,以便对缓存配置进行优化

源码

https://gitee.com/itopener/springboot

代码交流 2021