springboot+springcache实现两级缓存(redis+caffeine)
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)
缓存、两级缓存
简单的理解,缓存就是将数据从读取较慢的介质上读取出来放到读取较快的介质上,如磁盘-->内存。平时我们会将数据存储到磁盘上,如:数据库。如果每次都从数据库⾥去读取,会因为磁盘本⾝的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
Cache接⼝
提供缓存的具体操作,⽐如缓存的放⼊、读取、清理,spring框架中默认提供的实现有:
除了RedisCache是在spring-data-redis包中,其他的基本都是在spring-context-support包中
#Cache.java
package org.springframework.cache;
import urrent.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 urrent.Callable;
import annotation.AliasFor;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
spring framework组件@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,调⽤的是(key)⽅法;如果为true,调⽤的是(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 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的启动类或配置类上需要加上此注解才会⽣效
spring boot + spring cache
spring boot中已经整合了spring cache,并且提供了多种缓存的配置,在使⽤时只需要配置使⽤哪个缓存(enum CacheType)即可。
spring boot中多增加了⼀个可以扩展的东西,就是CacheManagerCustomizer接⼝,可以⾃定义实现这个接⼝,然后对CacheManager做⼀些设置,⽐如:
package com.itopener.fig;
import java.util.Map;
import urrent.ConcurrentHashMap;
import org.springframework.boot.autoconfigure.cache.CacheManagerCustomizer;
import org.dis.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.fig;
import t.annotation.Bean;
import t.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包中
这⾥提下我认为的RedisCache实现中的缺陷:
1.在缓存失效的瞬间,如果有线程获取缓存数据,可能出现返回null的情况,原因是RedisCache实现中是如下步骤:
1. 判断缓存key是否存在
2. 如果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基于时间的回收策略有以下⼏种:
1. expireAfterAccess:访问后到期,从上次读或写发⽣后的过期时间
2. expireAfterWrite:写⼊后到期,从上次写⼊发⽣之后的过期时间
3. ⾃定义策略:到期时间由实现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配置属性类
package com.dis.caffeine.spring.boot.autoconfigure;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.t.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) {
}
public String getTopic() {
return topic;
}
public void setTopic(String 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) {
}
public long getExpireAfterWrite() {
return expireAfterWrite;
}
public void setExpireAfterWrite(long expireAfterWrite) {
}
public long getRefreshAfterWrite() {
return refreshAfterWrite;
}
public void setRefreshAfterWrite(long 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) {
}
public Caffeine getCaffeine() {
return caffeine;
}
public void setCaffeine(Caffeine caffeine) {
this.caffeine = caffeine;
}
}
spring cache中有实现Cache接⼝的⼀个抽象类AbstractValueAdaptingCache,包含了空值的包装和缓存值的包装,所以就不⽤实现Cache接⼝了,直接实现AbstractValueAdaptingCache 抽象类
package com.dis.caffeine.spring.boot.autoconfigure.support;
import flect.Constructor;
import java.util.Map;
import java.util.Set;
import urrent.Callable;
import urrent.TimeUnit;
import urrent.locks.ReentrantLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.support.AbstractValueAdaptingCache;
import org.RedisTemplate;
import org.springframework.util.StringUtils;
import com.github.benmanes.caffeine.cache.Cache;
import com.dis.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 = Logger(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.caffeineCache = caffeineCache;
this.cachePrefix = CachePrefix();
this.defaultExpiration = Redis().getDefaultExpiration();
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论