目录:

  • 缓存、两级缓存
  • spring cache:主要包含spring cache定义的接口方法说明和注解中的属性说明
  • spring boot + spring cache
  • 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

Cache接口

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

接口分析

package org.springframework.cache;import java.util.concurrent.Callable;public interface Cache {// cacheName,缓存的名字,默认实现中一般是CacheManager创建Cache的bean时传入cacheNameString 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的实现都是成对出现,包结构也在上图中

package org.springframework.cache;import java.util.Collection;public interface CacheManager {// 通过cacheName创建Cache的实现bean,具体实现中需要存储已创建的Cache实现bean,避免重复创建,也避免内存缓存对象(如Caffeine)重新创建后原来缓存内容丢失的情况Cache getCache(String name);// 返回所有的cacheNameCollection<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生成器,默认实现是SimpleKeyGeneratorString keyGenerator() default "";// 指定使用哪个CacheManagerString 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;
}
  • @CachePut:放入缓存,主要用到对数据有更新的方法上。属性说明参考@Cacheable
  • @Caching:用于在一个方法上配置多种注解
  • @EnableCaching:启用spring cache缓存,作为总的开关,在spring boot的启动类或配置类上需要加上此注解才会生效

spring boot + spring cache


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

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> {@Overridepublic 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 {@Beanpublic RedisCacheManagerCustomizer redisCacheManagerCustomizer() {return new RedisCacheManagerCustomizer();}
}

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配置属性类
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;@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;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();}@Overridepublic String getName() {return this.name;}@Overridepublic Object getNativeCache() {return this;}@SuppressWarnings("unchecked")@Overridepublic <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();}}@Overridepublic 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);}@Overridepublic 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);}@Overridepublic void evict(Object key) {// 先清除redis中缓存数据,然后清除caffeine中的缓存,避免短时间内如果先清除caffeine缓存后其他请求会再从redis里加载到caffeine中redisTemplate.delete(getKey(key));push(new CacheMessage(this.name, key));caffeineCache.invalidate(key);}@Overridepublic 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();}@Overrideprotected 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 缓存变更时通知其他节点清理本地缓存* @param message*/private void push(CacheMessage message) {redisTemplate.convertAndSend(topic, message);}/*** @description 清理本地缓存* @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;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();}@Overridepublic 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();}@Overridepublic 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;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;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;}@Overridepublic 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;@Configuration
@AutoConfigureAfter(RedisAutoConfiguration.class)
@EnableConfigurationProperties(CacheRedisCaffeineProperties.class)
public class CacheRedisCaffeineAutoConfiguration {@Autowiredprivate CacheRedisCaffeineProperties cacheRedisCaffeineProperties;@Bean@ConditionalOnBean(RedisTemplate.class)public RedisCaffeineCacheManager cacheManager(RedisTemplate<Object, Object> redisTemplate) {return new RedisCaffeineCacheManager(cacheRedisCaffeineProperties, redisTemplate);}@Beanpublic 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

Springboot+caffeine 实现两级缓存相关推荐

  1. SpringBoot 集成 layering-cache 实现两级缓存调研与实践

    前言 对于系统查多改少的数据,可以通过缓存来提升系统的访问性能.一般情况下我们会采用 Redis ,但是如果仅仅依赖 Redis 很容易出现缓存雪崩的情况.为了防止缓存雪崩可以通过 Redis 高可用 ...

  2. 【Caffeine进阶】Redis+Caffeine 两级缓存实战,性能爆缸

    往期回顾 博主前面发过一篇[缓存框架Caffeine]初级篇,主要介绍了Caffeine的入门级使用!地址https://blog.csdn.net/Number_oneEngineer/articl ...

  3. Redis+Caffeine两级缓存

    1.前言 在高性能的服务架构设计中,缓存是一个不可或缺的环节.在实际的项目中,我们通常会将一些热点数据存储到Redis或MemCache这类缓存中间件中,只有当缓存的访问没有命中时再查询数据库.在提升 ...

  4. java l1 l2缓存,Java 两级缓存框架

    概述介绍 J2Cache 是 OSChina 目前正在使用的两级缓存框架(要求至少 Java 8).第一级缓存使用内存(同时支持 Ehcache 2.x.Ehcache 3.x 和 Caffeine) ...

  5. j2cache两级缓存框架

    j2cache介绍 j2cache是OSChina目前正在使用的两级缓存框架. j2cache的两级缓存结构: L1: 进程内缓存 caffeine/ehcache L2: 集中式缓存 Redis/M ...

  6. 高性能两级缓存J2Cache

    今天给大家推荐一个开源项目,J2Cache,一个很完善的两级缓存项目,作者是-红薯(开源中国CTO) 介绍 开源项目地址:https://gitee.com/ld/J2Cache/tree/maste ...

  7. J2cache两级缓存原理

    1.关于开源中国的一些数据 2.OSChina的几种缓存策略 3.Ehcache缓存架构 4.实际运行存在的问题 5.混村系统选型时的考量 6.J2Cache--两级缓存框架(内存,磁盘) 每天 IP ...

  8. SpringBoot+Caffeine+Redis声明式缓存

    目录 [博客目的] [应用场景] [相关知识] [代码实践] 引入组件 配置文件 配置类 启动类 业务层 实体类 接口层 [博客目的] 记录一个项目中同时整合了Caffeine和Redis时,怎么使用 ...

  9. J2Cache 两级缓存中的 Region 到底是什么东西?

    2019独角兽企业重金招聘Python工程师标准>>> 不时有人来询问 J2Cache 里的 Region 到底是什么概念,这里做统一的解答. J2Cache 的 Region 来源 ...

最新文章

  1. redmine 贴图操作
  2. S5PV210开发 -- 前言
  3. JZOJ 5703. 【gdoi2018 day2】木板(board)
  4. 基本数据类型的自动装箱
  5. 【转】[iOS] 关于 self = [super init];
  6. QT5.14.2 官方例子 - 学习系列
  7. 奥维kml文件制作工具_如何将平面控制点导入Google Earth、奥维互动地图及手机奥维互动地图APP里面?...
  8. 学计算机设计制图需啥基础,学习工程制图和CAD经验和方法 心得及体会
  9. pip升级scapy报错It is a distutils installed project and thus we cannot accurately determine which files
  10. text显示下标的字体 unity_Text Mesh Pro中文版
  11. 流逝的昨日,崭新的今天
  12. vue项目打包出错:Unexpected token arrow «=>», expected punc «,» [static/js/chunk-1558f5a0.b64bfa00.js:626,2
  13. 网络监控系统的建立及部署
  14. nas修改启动盘sn和mac
  15. via浏览器云同步国内服务器网页,Via浏览器
  16. 【U8】U8.11(8.12) access版本如何升级到用友T3及U8
  17. 计算机英语是啥意思啊,电脑的英文是什么意思
  18. 如何推算图纸上点坐标
  19. 调用office web 365接口实现在线预览word文档,PDF,PPT
  20. 1到n中能被3和7整除的数的和

热门文章

  1. MIME::Base64, encode_base64(), decode_base64()
  2. word分词器、ansj分词器、mmseg4j分词器、ik-analyzer分词器分词效果评估(转)
  3. 简单的模拟电话簿程序(java)
  4. 基于微哨自动打卡程序部署腾讯云函数
  5. 智能AI电话机器人系统
  6. IP地址管理工具——netbox——全
  7. Netty channelRegistered\ChannelActive---源码分析
  8. Tomcat 7.0安装与配置
  9. Java除法结果带小数、进一法的实现
  10. 数据结构(C语言第2版) 课后习题答案之第四章 串、数组和广义表