以前在github上找了一个开源的项目,改了改缓存的扩展,让其支持在缓存注解上控制缓存失效时间以及多长时间主动在后台刷新缓存以防止缓存失效( Spring Cache扩展:注解失效时间+主动刷新缓存 )。示意图以下:css
那篇文章存在两个问题:html
另外,当时项目是基于springboot 1.x,如今springboot2.0对缓存这块有所调整,须要从新适配。java
看看下面的构造函数,与1.x有比较大的改动,这里就不贴代码了。git
public RedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration) { this(cacheWriter, defaultCacheConfiguration, true); } public RedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration, String... initialCacheNames) { this(cacheWriter, defaultCacheConfiguration, true, initialCacheNames); }
既然上层的RedisCacheManager变更了,这里也就跟着变了。github
protected RedisCache(String name, RedisCacheWriter cacheWriter, RedisCacheConfiguration cacheConfig) { super(cacheConfig.getAllowCacheNullValues()); Assert.notNull(name, "Name must not be null!"); Assert.notNull(cacheWriter, "CacheWriter must not be null!"); Assert.notNull(cacheConfig, "CacheConfig must not be null!"); this.name = name; this.cacheWriter = cacheWriter; this.cacheConfig = cacheConfig; this.conversionService = cacheConfig.getConversionService(); }
针对上述的三个问题,分别应对。web
建立一个类用来描述缓存配置,避免在缓存注解上经过很是规手段完成特定的功能。redis
public class CacheItemConfig implements Serializable { /** * 缓存容器名称 */ private String name; /** * 缓存失效时间 */ private long expiryTimeSecond; /** * 当缓存存活时间达到此值时,主动刷新缓存 */ private long preLoadTimeSecond; }
具体的应用参见下面两步。spring
构造函数:缓存
public CustomizedRedisCacheManager( RedisConnectionFactory connectionFactory, RedisOperations redisOperations, List<CacheItemConfig> cacheItemConfigList)
参数说明:springboot
具体实现以下:核心思路就是调用RedisCacheManager的构造函数。
private RedisCacheWriter redisCacheWriter; private RedisCacheConfiguration defaultRedisCacheConfiguration; private RedisOperations redisOperations; public CustomizedRedisCacheManager( RedisConnectionFactory connectionFactory, RedisOperations redisOperations, List<CacheItemConfig> cacheItemConfigList) { this( RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory), RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofSeconds(30)), cacheItemConfigList .stream() .collect(Collectors.toMap(CacheItemConfig::getName,cacheItemConfig -> { RedisCacheConfiguration cacheConfiguration = RedisCacheConfiguration .defaultCacheConfig() .entryTtl(Duration.ofSeconds(cacheItemConfig.getExpiryTimeSecond())) .prefixKeysWith(cacheItemConfig.getName()); return cacheConfiguration; })) ); this.redisOperations=redisOperations; CacheContainer.init(cacheItemConfigList); } public CustomizedRedisCacheManager( RedisCacheWriter redisCacheWriter ,RedisCacheConfiguration redisCacheConfiguration, Map<String, RedisCacheConfiguration> redisCacheConfigurationMap) { super(redisCacheWriter,redisCacheConfiguration,redisCacheConfigurationMap); this.redisCacheWriter=redisCacheWriter; this.defaultRedisCacheConfiguration=redisCacheConfiguration; }
因为咱们须要主动刷新缓存,因此须要重写getCache方法:主要就是将RedisCache构造函数所须要的参数传递过去。
@Override public Cache getCache(String name) { Cache cache = super.getCache(name); if(null==cache){ return cache; } CustomizedRedisCache redisCache= new CustomizedRedisCache( name, this.redisCacheWriter, this.defaultRedisCacheConfiguration, this.redisOperations ); return redisCache; }
核心方法就一个,getCache:当获取到缓存时,实时获取缓存的存活时间,若是存活时间进入缓存刷新时间范围即调起异步任务完成缓存动态加载。ThreadTaskHelper是一个异常任务提交的工具类。下面方法中的参数key,并非最终存入redis的key,是@Cacheable注解中的key,要想获取缓存的存活时间就须要找到真正的key,而后让redisOptions去调用ttl命令。在springboot 1.5下面好像有个RedisCacheKey的对象,但在springboot2.0中并未发现,取而代之获取真正key是经过函数this.createCacheKey来完成。
public ValueWrapper get(final Object key) { ValueWrapper valueWrapper= super.get(key); if(null!=valueWrapper){ CacheItemConfig cacheItemConfig=CacheContainer.getCacheItemConfigByCacheName(key.toString()); long preLoadTimeSecond=cacheItemConfig.getPreLoadTimeSecond(); String cacheKey=this.createCacheKey(key); Long ttl= this.redisOperations.getExpire(cacheKey); if(null!=ttl&& ttl<=preLoadTimeSecond){ logger.info("key:{} ttl:{} preloadSecondTime:{}",cacheKey,ttl,preLoadTimeSecond); ThreadTaskHelper.run(new Runnable() { @Override public void run() { logger.info("refresh key:{}", cacheKey); CustomizedRedisCache.this.getCacheSupport() .refreshCacheByKey(CustomizedRedisCache.super.getName(), key.toString()); } }); } } return valueWrapper; }
CacheContainer,这是一个辅助数据存储,将前面设置的缓存配置放入容器以便后面的逻辑获取。其中包含一个默认的缓存配置,防止 在未设置的状况致使缓存获取异常。
public class CacheContainer { private static final String DEFAULT_CACHE_NAME="default"; private static final Map<String,CacheItemConfig> CACHE_CONFIG_HOLDER=new ConcurrentHashMap(){ { put(DEFAULT_CACHE_NAME,new CacheItemConfig(){ @Override public String getName() { return DEFAULT_CACHE_NAME; } @Override public long getExpiryTimeSecond() { return 30; } @Override public long getPreLoadTimeSecond() { return 25; } }); } }; public static void init(List<CacheItemConfig> cacheItemConfigs){ if(CollectionUtils.isEmpty(cacheItemConfigs)){ return; } cacheItemConfigs.forEach(cacheItemConfig -> { CACHE_CONFIG_HOLDER.put(cacheItemConfig.getName(),cacheItemConfig); }); } public static CacheItemConfig getCacheItemConfigByCacheName(String cacheName){ if(CACHE_CONFIG_HOLDER.containsKey(cacheName)) { return CACHE_CONFIG_HOLDER.get(cacheName); } return CACHE_CONFIG_HOLDER.get(DEFAULT_CACHE_NAME); } public static List<CacheItemConfig> getCacheItemConfigs(){ return CACHE_CONFIG_HOLDER .values() .stream() .filter(new Predicate<CacheItemConfig>() { @Override public boolean test(CacheItemConfig cacheItemConfig) { return !cacheItemConfig.getName().equals(DEFAULT_CACHE_NAME); } }) .collect(Collectors.toList()); } }
因为主动刷新缓存时须要用缓存操做,这里须要加载RedisTemplate,其实就是后面的RedisOptions接口。序列化机制可心随意调整。
@Bean public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) { RedisTemplate<Object, Object> template = new RedisTemplate<>(); template.setConnectionFactory(connectionFactory); Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer(Object.class); ObjectMapper mapper = new ObjectMapper(); mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); serializer.setObjectMapper(mapper); template.setValueSerializer(serializer); template.setKeySerializer(new StringRedisSerializer()); template.afterPropertiesSet(); return template; }
加载CacheManager,主要是配置缓存容器,其他的两个都是redis所须要的对象。
@Bean public CacheManager cacheManager(RedisConnectionFactory connectionFactory,RedisTemplate<Object, Object> redisTemplate) { CacheItemConfig productCacheItemConfig=new CacheItemConfig(); productCacheItemConfig.setName("Product"); productCacheItemConfig.setExpiryTimeSecond(10); productCacheItemConfig.setPreLoadTimeSecond(5); List<CacheItemConfig> cacheItemConfigs= Lists.newArrayList(productCacheItemConfig); CustomizedRedisCacheManager cacheManager = new CustomizedRedisCacheManager(connectionFactory,redisTemplate,cacheItemConfigs); return cacheManager; }
CustomizedRedisCache的get方法,当判断须要刷新缓存时,后台起了一个异步任务去更新缓存,此时若是有N个请求同时访问同一个缓存,就是发生相似缓存击穿的状况。为了不这种状况的发生最好的方法就是加锁,让其只有一个任务去作更新的事情。Spring Cache提供了一个同步的参数来支持并发更新控制,这里咱们能够模仿这个思路来处理。
public ValueWrapper get(final Object key) { ValueWrapper valueWrapper= super.get(key); if(null!=valueWrapper){ CacheItemConfig cacheItemConfig=CacheContainer.getCacheItemConfigByCacheName(key.toString()); long preLoadTimeSecond=cacheItemConfig.getPreLoadTimeSecond(); ; String cacheKey=this.createCacheKey(key); Long ttl= this.redisOperations.getExpire(cacheKey); if(null!=ttl&& ttl<=preLoadTimeSecond){ logger.info("key:{} ttl:{} preloadSecondTime:{}",cacheKey,ttl,preLoadTimeSecond); if(ThreadTaskHelper.hasRunningRefreshCacheTask(cacheKey)){ logger.info("do not need to refresh"); } else { ThreadTaskHelper.run(new Runnable() { @Override public void run() { try { REFRESH_CACKE_LOCK.lock(); if(ThreadTaskHelper.hasRunningRefreshCacheTask(cacheKey)){ logger.info("do not need to refresh"); } else { logger.info("refresh key:{}", cacheKey); CustomizedRedisCache.this.getCacheSupport().refreshCacheByKey(CustomizedRedisCache.super.getName(), key.toString()); ThreadTaskHelper.removeRefreshCacheTask(cacheKey); } } finally { REFRESH_CACKE_LOCK.unlock(); } } }); } } } return valueWrapper; }
以上方案是在单机状况下,若是是多机也会出现执行屡次刷新,但这种代码是可接受的,若是作到严格意义的一次刷新就须要引入分布式锁,但同时会带来系统复杂度以及性能消耗,有点得不尝失的感受,因此建议单机方式便可。
这里不须要在缓存容器名称上动刀子了,像正规使用Cacheable注解便可。
@Cacheable(value = "Product",key ="#id") @Override public Product getById(Long id) { this.logger.info("get product from db,id:{}",id); Product product=new Product(); product.setId(id); return product; }
文中代码是依赖上述项目的,若是有不明白的可下载源码