Spring Cache擴展:註解失效時間+主動刷新緩存(二)

目的

以前在github上找了一個開源的項目,改了改緩存的擴展,讓其支持在緩存註解上控制緩存失效時間以及多長時間主動在後臺刷新緩存以防止緩存失效( Spring Cache擴展:註解失效時間+主動刷新緩存 )。示意圖以下:css

那篇文章存在兩個問題:html

  • 全部的配置是創建在修改緩存容器的名稱基礎上,與傳統緩存註解的寫法有所區別,後續維護成本會增長;
  • 後臺刷新緩存時會存在併發更新的問題

另外,當時項目是基於springboot 1.x,如今springboot2.0對緩存這塊有所調整,須要從新適配。java

SpringBoot 2.0對緩存的變更

RedisCacheManager

看看下面的構造函數,與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);
}

RedisCache

既然上層的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

適配springboot 2.0

修改CustomizedRedisCacheManager

構造函數:緩存

public CustomizedRedisCacheManager(
        RedisConnectionFactory connectionFactory,
        RedisOperations redisOperations,
        List<CacheItemConfig> cacheItemConfigList)

參數說明:springboot

  • connectionFactory,這是一個redis鏈接工廠,用於後續操做redis
  • redisOperations,這個一個redis的操做實例,具體負責執行redis命令
  • cacheItemConfigList,這是緩存的配置,好比名稱,失效時間,主動刷新時間,用於取代在註解上個性化的配置。

具體實現以下:核心思路就是調用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;
}

修改CustomizedRedisCache

核心方法就一個,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());
    }
}

修改CacheManager加載方式

因爲主動刷新緩存時須要用緩存操做,這裏須要加載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提供了一個同步的參數來支持併發更新控制,這裏咱們能夠模仿這個思路來處理。

  • 將正在進行緩存刷新的KEY放入一個容器,其它線程訪問時若是發現KEY已經存在就直接跳過;
  • 緩存刷新完成後從容器中刪除對應的KEY
  • 在容器中未發現正在進行緩存刷新的KEY時,利用鎖機制確保只有一個任務執行刷新,相似雙重檢查
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;
}

本文源碼

文中代碼是依賴上述項目的,若是有不明白的可下載源碼

相關文章
相關標籤/搜索