博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
spring boot + spring cache 实现两级缓存(redis + ehcache)
阅读量:2353 次
发布时间:2019-05-10

本文共 16853 字,大约阅读时间需要 56 分钟。

前言

本文参考了。

处理流程

与一致:

这里写图片描述

事项

  • spring cache中有实现Cache接口的一个抽象类AbstractValueAdaptingCache,包含了空值的包装和缓存值的包装,所以就不用实现Cache接口了,直接实现AbstractValueAdaptingCache抽象类
  • 利用redis的pub/sub功能,实现多服务实例的本地缓存一致性
  • 原来的有个缺点:服务1给缓存put完KV后推送给redis的消息,服务1本身也会接收到该消息,然后会将刚刚put的KV删除。这里把ehcacheCache的hashcode传过去,避免这个问题。
  • 代码用了lombok

配置

1.@EnableCaching:启用spring cache缓存,在spring boot的启动类或配置类上需要加上此注解才会生效

2.yml

# redis-starter的配置spring:  cache:    cache-names: cache1,cache2,cache3  redis:    timeout: 10000    pool:      max-idle: 10      min-idle: 2      max-active: 10      max-wait: 3000#自定义配置。expire统一单位为毫秒cache:  multi:    cacheNames: cache1,cache2,cache3    ehcache:      expireAfterWrite: 5000      maxEntry: 1000    redis:      defaultExpiration: 60000      expires:        cache1: 50000        cache2: 70000        cache3: 70000

3.POM

依赖项

org.springframework.boot
spring-boot-starter-redis
1.4.3.RELEASE
org.ehcache
ehcache
3.5.2

代码

定义properties配置属性类

@ConfigurationProperties(prefix = "cache.multi")@Datapublic class RedisEhcacheProperties {
private Set
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 Ehcache ehcache = new Ehcache(); public boolean isCacheNullValues() { return cacheNullValues; } @Data public class Redis {
/** 全局过期时间,单位毫秒,默认不过期*/ private long defaultExpiration = 0; /** 每个cacheName的过期时间,单位毫秒,优先级比defaultExpiration高*/ private Map
expires = new HashMap<>(); /** 缓存更新时通知其他节点的topic名称*/ private String topic = "cache:redis:ehcache:topic"; } @Data public class Ehcache {
/** * 访问后过期时间,单位毫秒 */// private long expireAfterAccess; /** * 写入后过期时间,单位毫秒 */ private long expireAfterWrite; /** * 初始化大小 */// private int initialCapacity; /** * 每个ehcache最大缓存对象个数,超过此数量时按照失效策略(默认为LRU) */ private long maxEntry = 500; }}

RedisEhcacheCache

import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.cache.annotation.Cacheable;import org.springframework.stereotype.Service;@Slf4jpublic class RedisEhcacheCache extends AbstractValueAdaptingCache {
private String name; private RedisTemplate
redisTemplate; private Cache
ehcacheCache; private String cachePrefix; private long defaultExpiration = 0; private Map
expires; private String topic = "cache:redis:ehcache:topic"; protected RedisEhcacheCache(boolean allowNullValues) { super(allowNullValues); } public RedisEhcacheCache(String name, RedisTemplate
redisTemplate, Cache
ehcacheCache, RedisEhcacheProperties redisEhcacheProperties) { super(redisEhcacheProperties.isCacheNullValues()); this.name = name; this.redisTemplate = redisTemplate; this.ehcacheCache = ehcacheCache; this.cachePrefix = redisEhcacheProperties.getCachePrefix(); this.defaultExpiration = redisEhcacheProperties.getRedis().getDefaultExpiration(); this.expires = redisEhcacheProperties.getRedis().getExpires(); this.topic = redisEhcacheProperties.getRedis().getTopic(); } @Override public String getName() { return this.name; } @Override public Object getNativeCache() { return this; } @SuppressWarnings("unchecked") @Override public
T get(Object key, Callable
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(); } } //从持久层读取value,然后存入缓存。允许value = null @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)); } //通过redis推送消息,使其他服务的ehcache失效。 //原来的有个缺点:服务1给缓存put完KV后推送给redis的消息,服务1本身也会接收到该消息, // 然后会将刚刚put的KV删除。这里把ehcacheCache的hashcode传过去,避免这个问题。 push(new CacheMessage(this.name, key, this.ehcacheCache.hashCode())); ehcacheCache.put(key, value); } //key的生成 name:cachePrefix:key 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(); } @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, this.ehcacheCache.hashCode())); ehcacheCache.put(key, toStoreValue(value)); } } return toValueWrapper(prevValue); } @Override public void evict(Object key) { // 先清除redis中缓存数据,然后清除ehcache中的缓存,避免短时间内如果先清除ehcache缓存后其他请求会再从redis里加载到ehcache中 redisTemplate.delete(getKey(key)); push(new CacheMessage(this.name, key, this.ehcacheCache.hashCode())); ehcacheCache.remove(key); } @Override public void clear() { // 先清除redis中缓存数据,然后清除ehcache中的缓存,避免短时间内如果先清除ehcache缓存后其他请求会再从redis里加载到ehcache中 Set
keys = redisTemplate.keys(this.name.concat(":")); for(Object key : keys) { redisTemplate.delete(key); } push(new CacheMessage(this.name, null)); ehcacheCache.clear(); } //获根据key取缓存,如果返回null,则要读取持久层 @Override protected Object lookup(Object key) { Object cacheKey = getKey(key); Object value = ehcacheCache.get(key); if(value != null) { log.debug("get cache from ehcache, the key is : {}", cacheKey); return value; } value = redisTemplate.opsForValue().get(cacheKey); if(value != null) { log.debug("get cache from redis and put in ehcache, the key is : {}", cacheKey); //将二级缓存重新复制到一级缓存。原理是最近访问的key很可能再次被访问 ehcacheCache.put(key, value); } return value; } /** * 缓存变更时,利用redis的消息订阅功能,通知其他节点清理本地缓存。 * @description * @param message */ private void push(CacheMessage message) { redisTemplate.convertAndSend(topic, message); } /** * @description 清理本地缓存 * @param key */ public void clearLocal(Object key) { log.debug("clear local cache, the key is : {}", key); if(key == null) { ehcacheCache.clear(); } else { ehcacheCache.remove(key); } } public Cache
getLocalCache(){ return ehcacheCache; }}

实现CacheManager接口

import lombok.extern.slf4j.Slf4j;import org.ehcache.config.CacheConfiguration;import org.ehcache.config.builders.*;import org.springframework.cache.Cache;import org.springframework.cache.CacheManager;import org.springframework.data.redis.core.RedisTemplate;import java.time.Duration;import java.util.Collection;import java.util.Set;import java.util.concurrent.ConcurrentHashMap;import java.util.concurrent.ConcurrentMap;@Slf4jpublic class RedisEhcacheCacheManager implements CacheManager {
private ConcurrentMap
cacheMap = new ConcurrentHashMap
(); private RedisEhcacheProperties redisEhcacheProperties; private RedisTemplate
redisTemplate; private boolean dynamic = true; private Set
cacheNames; private org.ehcache.CacheManager ehCacheManager; private CacheConfiguration configuration; public RedisEhcacheCacheManager(RedisEhcacheProperties redisEhcacheProperties, RedisTemplate
redisTemplate) { super(); this.redisEhcacheProperties = redisEhcacheProperties; this.redisTemplate = redisTemplate; this.dynamic = redisEhcacheProperties.isDynamic(); this.cacheNames = redisEhcacheProperties.getCacheNames(); setAboutEhCache(); } private void setAboutEhCache(){ long ehcacheExpire = redisEhcacheProperties.getEhcache().getExpireAfterWrite(); this.configuration = CacheConfigurationBuilder .newCacheConfigurationBuilder(Object.class, Object.class, ResourcePoolsBuilder.heap(redisEhcacheProperties.getEhcache().getMaxEntry())) .withExpiry(ExpiryPolicyBuilder.timeToLiveExpiration(Duration.ofSeconds(ehcacheExpire))) .build(); this.ehCacheManager = CacheManagerBuilder .newCacheManagerBuilder() .build(); this.ehCacheManager.init(); } @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 RedisEhcacheCache(name, redisTemplate, getEhcache(name), redisEhcacheProperties); Cache oldCache = cacheMap.putIfAbsent(name, cache); log.debug("create cache instance, the cache name is : {}", name); return oldCache == null ? cache : oldCache; } public org.ehcache.Cache
getEhcache(String name){ org.ehcache.Cache
res = ehCacheManager.getCache(name, Object.class, Object.class); if(res != null){ return res; } return ehCacheManager.createCache(name, configuration); } @Override public Collection
getCacheNames() { return this.cacheNames; } public void clearLocal(String cacheName, Object key, Integer sender) { Cache cache = cacheMap.get(cacheName); if(cache == null) { return ; } RedisEhcacheCache redisEhcacheCache = (RedisEhcacheCache) cache; //如果是发送者本身发送的消息,就不进行key的清除 if(redisEhcacheCache.getLocalCache().hashCode() != sender) { redisEhcacheCache.clearLocal(key); } }}

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

@Datapublic class CacheMessage implements Serializable {
private static final long serialVersionUID = 5987219310442078193L; private String cacheName; private Object key; private Integer sender; public CacheMessage(String cacheName, Object key) { super(); this.cacheName = cacheName; this.key = key; } public CacheMessage(String cacheName, Object key, Integer sender) { super(); this.cacheName = cacheName; this.key = key; this.sender = sender; }}

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

import lombok.extern.slf4j.Slf4j;import org.springframework.data.redis.connection.Message;import org.springframework.data.redis.connection.MessageListener;import org.springframework.data.redis.core.RedisTemplate;/** * 监听redis消息需要实现MessageListener接口 */@Slf4jpublic class CacheMessageListener implements MessageListener {
private RedisTemplate
redisTemplate; private RedisEhcacheCacheManager redisEhcacheCacheManager; public CacheMessageListener(RedisTemplate
redisTemplate, RedisEhcacheCacheManager redisEhcacheCacheManager) { super(); this.redisTemplate = redisTemplate; this.redisEhcacheCacheManager = redisEhcacheCacheManager; } @Override public void onMessage(Message message, byte[] pattern) { CacheMessage cacheMessage = (CacheMessage) redisTemplate.getValueSerializer().deserialize(message.getBody()); log.debug("recevice a redis topic message, clear local cache, the cacheName is {}, the key is {}", cacheMessage.getCacheName(), cacheMessage.getKey()); redisEhcacheCacheManager.clearLocal(cacheMessage.getCacheName(), cacheMessage.getKey(), cacheMessage.getSender()); }}

增加spring boot配置类

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;@Configuration@AutoConfigureAfter(RedisAutoConfiguration.class)@EnableConfigurationProperties(RedisEhcacheProperties.class)public class CacheRedisEhcacheAutoConfiguration {    @Autowired    private RedisEhcacheProperties redisEhcacheProperties;    @Bean    public RedisEhcacheCacheManager cacheManager(RedisTemplate
redisTemplate) { return new RedisEhcacheCacheManager(redisEhcacheProperties, redisTemplate); } @Bean @ConditionalOnBean(RedisEhcacheCacheManager.class) public RedisMessageListenerContainer redisMessageListenerContainer(RedisTemplate
redisTemplate, RedisEhcacheCacheManager redisEhcacheCacheManager) { RedisMessageListenerContainer redisMessageListenerContainer = new RedisMessageListenerContainer(); redisMessageListenerContainer.setConnectionFactory(redisTemplate.getConnectionFactory()); CacheMessageListener cacheMessageListener = new CacheMessageListener(redisTemplate, redisEhcacheCacheManager); redisMessageListenerContainer.addMessageListener(cacheMessageListener, new ChannelTopic(redisEhcacheProperties.getRedis().getTopic())); return redisMessageListenerContainer; }}

缓存使用

//cacheManager = "cacheManager"可以不指定    @Cacheable(value = "gerritCache", key = "#projectName + '_' + #from + '_' + #to"/*, 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;    }

二级缓存和一级缓存切换

RedisCacheConfiguration和我们自定义的CacheRedisEhcacheAutoConfiguration都有注解:

@AutoConfigureAfter(RedisAutoConfiguration.class)

不过由于RedisCacheConfiguration有:

@ConditionalOnMissingBean(CacheManager.class)

保证了唯一性:如果CacheRedisEhcacheAutoConfiguration被执行了,那么RedisCacheConfiguration就不会被执行。

我们可以基于这一点做一个二级缓存开关。在yml加入

cache:  use2L: true #开启二级缓存

CacheRedisEhcacheAutoConfiguration加上(yml没有配置或者配置为false,二级缓存都不起作用):

@ConditionalOnProperty(name = "cache.use2L", havingValue = "true", matchIfMissing = false)

加上CacheConfig对单独一级redis缓存进行配置:

import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;import org.springframework.boot.context.properties.EnableConfigurationProperties;import org.springframework.cache.CacheManager;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.data.redis.cache.RedisCacheManager;import org.springframework.data.redis.core.RedisTemplate;import java.util.HashMap;import java.util.Map;@Configuration@ConditionalOnProperty(name = "cache.use2L", havingValue = "false", matchIfMissing = true)@EnableConfigurationProperties(RedisEhcacheProperties.class)public class CacheConfig {    @Autowired    private RedisEhcacheProperties redisEhcacheProperties;    @Bean    public CacheManager cacheManager(RedisTemplate redisTemplate) {        RedisCacheManager rcm = new RedisCacheManager(redisTemplate);        //设置各个cache的缓存过期时间        Map
expires = new HashMap<>(redisEhcacheProperties.getRedis().getExpires()); //毫秒->秒 expires.forEach((k, v) -> expires.put(k, v/1000)); rcm.setExpires(expires); rcm.setDefaultExpiration(redisEhcacheProperties.getRedis().getDefaultExpiration());//默认过期时间 return rcm; }}

ref:

转载地址:http://siztb.baihongyu.com/

你可能感兴趣的文章
Jquery表格奇偶行不同颜色
查看>>
struts2防止表单重复提交<s:token/>
查看>>
利用XStream在Java对象和XML之间相互转换
查看>>
trivial note for Formal Languages and Automata
查看>>
myeclipse 解决内存溢出
查看>>
Java反编译小工具!
查看>>
KMP Algorithm
查看>>
第一使用Project
查看>>
PageRank&HITS算法
查看>>
POS Tagger in Java
查看>>
不断叛逆,不断克制
查看>>
10-fold cross-validation 十折交叉验证 .
查看>>
F1-Measure
查看>>
K最近邻(KNN)算法原理和java实现
查看>>
paper report《Finding high-quality content in social media》
查看>>
卑微的基于关系数据库的关键词Prototype系统
查看>>
聚类与分类区别
查看>>
个人推荐的Weka教程,包含了数据格式、数据准备、分类和聚类Demo
查看>>
How To Write an Academic Paper in Text Mining
查看>>
克服长尾挑战 (Overcoming the Long Tail Challenge)
查看>>