以前在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; }
文中代碼是依賴上述項目的,若是有不明白的可下載源碼