天天看點

分桶政策清理SpringCache中的緩存

背景介紹

我們使用SpringCache架構 + Redis來實作項目中的緩存實作,它能實作自動對資料緩存,也可以自動清理過期的緩存。大多數情況下,它都運作非常好。

這是因為我們需要緩存的資料,通常都是可序列化的,但是我們遲早會遇到不可序列化的對象。那麼我們隻能選擇SpringCache中的ConcurrentMapCache才能緩存這些不可序列化的對象,但是ConcurrentMapCache呢又不提供自動清理緩存的功能。

于是我開始自己設計一個本地的、高效的、能自動清理緩存擴充,同樣它能支援SpringCache。

為了高效的清理緩存,我采用分桶政策,這一設計思想來源于ZooKeeper的Session管理。分桶政策也是本文的精彩内容。

SpringCache的使用

SpringCache + Redis自動清理緩存

@EnableCaching
@Configuration
public class RedisCacheAutoConfiguration {
   
    @Autowired
    private RedisConnectionFactory redisConnectionFactory;

    @Primary
    @Bean("redisCacheManager")
    public CacheManager cacheManager() {
        RedisCacheManager cacheManager = new RedisCacheManager(
                RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory),
                getTtlRedisCacheConfiguration(CacheNameEnum.DEFAULT),
                getCustomizeTtlRedisCacheConfigurationMap());
        return cacheManager;
    }

   
    private Map<String, RedisCacheConfiguration> getCustomizeTtlRedisCacheConfigurationMap() {
        Map<String, RedisCacheConfiguration> redisCacheConfigurationMap = new HashMap<>();
        for (CacheNameEnum cacheNameEnum : CacheNameEnum.values()) {
            redisCacheConfigurationMap.put(cacheNameEnum.name(), getTtlRedisCacheConfiguration(cacheNameEnum));
        }
        return redisCacheConfigurationMap;
    }

    private RedisCacheConfiguration getTtlRedisCacheConfiguration(CacheNameEnum cacheNameEnum) {
        GenericFastJsonRedisSerializer fastJsonRedisSerializer = new GenericFastJsonRedisSerializer();
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();

        RedisSerializationContext.SerializationPair<Object> objectSerializationPair = RedisSerializationContext.SerializationPair.fromSerializer(fastJsonRedisSerializer);
        RedisSerializationContext.SerializationPair<String> stringSerializationPair = RedisSerializationContext.SerializationPair.fromSerializer(stringRedisSerializer);

        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();
        redisCacheConfiguration = redisCacheConfiguration.serializeKeysWith(stringSerializationPair)
                .serializeValuesWith(objectSerializationPair)
                .entryTtl(Duration.ofSeconds(cacheNameEnum.getTtl()));
        return redisCacheConfiguration;
    }

    enum CacheNameEnum {
        DEFAULT(60);
        private int ttl;

        CacheNameEnum(int ttl) {
            this.ttl = ttl;
        }

        public int getTtl() {
            return ttl;
        }
    }
}           

複制

那麼使用的時候,就隻需要增加注解就行了

@Cacheable(cacheManager = "redisCacheManager", cacheNames = "DEFAULT", key = "'nft:transafer:' + #mnemonic")
public Transafer recover(String mnemonic) {
    return new Transafer(mnemonic, wenchangChainPropertity);
}           

複制

SpringCache + Map本地緩存

// 記得加上@EnableCaching,開啟緩存
@Bean("localCacheManager")
public CacheManager localCacheManager() {
    ConcurrentMapCache publicKeyCache = new ConcurrentMapCache("localCache");
    Set<Cache> caches = new HashSet<>();
    caches.add(publicKeyCache);

    SimpleCacheManager cacheManager = new SimpleCacheManager();
    cacheManager.setCaches(caches);
    return cacheManager;
}           

複制

那麼使用的時候,就隻需要增加注解就行了

@Cacheable(cacheManager = "localCacheManager", cacheNames = "localCache", key = "'nft:transafer:' + #mnemonic")
public Transafer recover(String mnemonic) {
    return new Transafer(mnemonic, wenchangChainPropertity);
}           

複制

SpringCache + Map自動清理本地緩存

為了實作自動清理緩存,我繼承了ConcurrentMapCache,采用分桶政策,定時清理。

  • • expirationInterval,桶的估計範圍,如果為1分鐘,那麼1分鐘内建立的緩存都存在一個桶,例如16:11:20和16:11:01,都會存放在16:12:00這個桶中。
  • • roundToNextInterval,用于根據目前時間計算,下一個桶的時間。
  • • executorService,用于清理緩存,僅僅在建立桶時,調用其該線程,并不會實時運作,占用CPU資源。
public class LocalExpiryCache extends ConcurrentMapCache {
    private static Logger log = LoggerFactory.getLogger(LocalExpiryCache.class);
    /**
     * 桶的範圍
     */
    public final long expirationInterval;
    private static ScheduledExecutorService executorService = new ScheduledThreadPoolExecutor(5, NftThreadFactory.create("cache-cleara", true));
    private static final Map<Long, Set<Object>> expiryMap = new ConcurrentHashMap<>();

    public LocalExpiryCache(String name, long expirationInterval) {
        super(name);
        this.expirationInterval = expirationInterval;
    }


    @Override
    public void put(Object key, Object value) {
        log.info("=======put=======");
        super.put(key, value);
        long now = System.currentTimeMillis();
        long expires = roundToNextInterval(now);
        log.info("expires: " + DateUtil.formatDate(new Date(expires), DateUtil.FORMAT_DATETIME_NORMAL));
        if (!expiryMap.containsKey(expires)) {
            synchronized (this) {
                if (!expiryMap.containsKey(expires)) {
                    expiryMap.put(expires, new ConcurrentHashSet<>());
                    executorService.schedule((Runnable) this::expiry, expires - now + 100
                            , TimeUnit.MILLISECONDS);
                }
            }
        }
        Set<Object> objects = expiryMap.get(expires);
        objects.add(key);
    }

    @Override
    public ValueWrapper putIfAbsent(Object key, Object value) {
        log.info("=======putIfAbsent=======");
        return super.putIfAbsent(key, value);
    }

    private long roundToNextInterval(long time) {
        return (time / expirationInterval + 1) * expirationInterval;
    }

    public Set expiry() {
        log.info("-------------------------------------");
        long now = System.currentTimeMillis();
        Set<Long> ttls = expiryMap.keySet();
        if (CollectionUtils.isEmpty(ttls)) {
            return Collections.emptySet();
        }
        Iterator<Long> iterator = ttls.iterator();
        Set result = new HashSet();
        while (iterator.hasNext()) {
            Long expirationTime = iterator.next();
            if (now < expirationTime) {
                break;
            }
            result.addAll(expiryMap.get(expirationTime));
            iterator.remove();
        }
        for (Object key : result) {
            super.evict(key);
        }
        log.info("evict size: " + result.size());
        return result;
    }

    public static void main(String[] args) throws Exception {

        LocalExpiryCache localCache = new LocalExpiryCache("", 1 * 60 * 1000);
        localCache.put("1", "");
        localCache.put("2", "");

        log.info(localCache.getNativeCache().size() + "");
        Thread.sleep(1 * 60 * 1000);
        log.info(localCache.getNativeCache().size() + "");

        localCache.put("2", "");
        Thread.sleep(1 * 60 * 1000);
        log.info(localCache.getNativeCache().size() + "");

        System.in.read();
    }
}           

複制

使用時,用LocalExpiryCache替換掉ConcurrentMapCache即可

@Bean("localCacheManager")
public CacheManager localCacheManager() {
    LocalExpiryCache publicKeyCache = new LocalExpiryCache("localCache", 1 * 60 * 1000);
    Set<Cache> caches = new HashSet<>();
    caches.add(publicKeyCache);

    SimpleCacheManager cacheManager = new SimpleCacheManager();
    cacheManager.setCaches(caches);
    return cacheManager;
}           

複制