基於Spring Cache實現Caffeine、jimDB多級快取實戰

語言: CN / TW / HK
作者: 京東零售 王震

背景

在早期參與涅槃氛圍標籤中臺專案中,前臺要求介面效能999要求50ms以下,通過設計Caffeine、ehcache堆外快取、jimDB三級快取,利用記憶體、堆外、jimDB快取不同的特性提升介面效能,
記憶體快取採用Caffeine快取,利用W-TinyLFU演算法獲得更高的記憶體命中率;同時利用堆外快取降低記憶體快取大小,減少GC頻率,同時也減少了網路IO帶來的效能消耗;利用JimDB提升介面高可用、高併發;後期通過壓測及效能調優999效能<20ms
image.png

當時由於專案工期緊張,三級快取實現較為臃腫、業務侵入性強、可讀性差,在近期場景化推薦專案中,為B端商家場景化資源投放推薦,考慮到B端流量相對C端流量較小,但需保證介面效能穩定。採用SpringCache實現caffeine、jimDB多級快取方案,實現了低侵入性、可擴充套件、高可用的快取方案,極大提升了系統穩定性,保證介面效能小於100ms;

Spring Cache實現多級快取

多級快取例項MultilevelCache

/**
 * 分級快取
 * 基於Caffeine + jimDB 實現二級快取
 * @author wangzhen520
 * @date 2022/12/9
 */
public class MultilevelCache extends AbstractValueAdaptingCache {

    /**
     * 快取名稱
     */
    private String name;

    /**
     * 是否開啟一級快取
     */
    private boolean enableFirstCache = true;

    /**
     * 一級快取
     */
    private Cache firstCache;

    /**
     * 二級快取
     */
    private Cache secondCache;

    @Override
    protected Object lookup(Object key) {
        Object value;
        recordCount(getUmpKey(this.getName(), UMP_GET_CACHE, UMP_ALL));
        if(enableFirstCache){
            //查詢一級快取
            value = getWrapperValue(getForFirstCache(key));
            log.info("{}#lookup getForFirstCache key={} value={}", this.getClass().getSimpleName(), key, value);
            if(value != null){
                return value;
            }
        }
        value = getWrapperValue(getForSecondCache(key));
        log.info("{}#lookup getForSecondCache key={} value={}", this.getClass().getSimpleName(), key, value);
        //二級快取不為空,則更新一級快取
        boolean putFirstCache = (Objects.nonNull(value) || isAllowNullValues()) && enableFirstCache;
        if(putFirstCache){
            recordCount(getUmpKey(this.getName(), UMP_FIRST_CACHE, UMP_NO_HIT));
            log.info("{}#lookup put firstCache key={} value={}", this.getClass().getSimpleName(), key, value);
            firstCache.put(key, value);
        }
        return value;
    }
    

    @Override
    public void put(Object key, Object value) {
        if(enableFirstCache){
            checkFirstCache();
            firstCache.put(key, value);
        }
        secondCache.put(key, value);
    }

    /**
     * 查詢一級快取
     * @param key
     * @return
     */
    private ValueWrapper getForFirstCache(Object key){
        checkFirstCache();
        ValueWrapper valueWrapper = firstCache.get(key);
        if(valueWrapper == null || Objects.isNull(valueWrapper.get())){
            recordCount(getUmpKey(this.getName(), UMP_FIRST_CACHE, UMP_NO_HIT));
        }
        return valueWrapper;
    }

    /**
     * 查詢二級快取
     * @param key
     * @return
     */
    private ValueWrapper getForSecondCache(Object key){
        ValueWrapper valueWrapper = secondCache.get(key);
        if(valueWrapper == null || Objects.isNull(valueWrapper.get())){
            recordCount(getUmpKey(this.getName(), UMP_SECOND_CACHE, UMP_NO_HIT));
        }
        return valueWrapper;
    }

    private Object getWrapperValue(ValueWrapper valueWrapper){
        return Optional.ofNullable(valueWrapper).map(ValueWrapper::get).orElse(null);
    }

}

多級快取管理器抽象

/**
 * 多級快取實現抽象類
 * 一級快取
 * @see AbstractMultilevelCacheManager#getFirstCache(String)
 * 二級快取
 * @see AbstractMultilevelCacheManager#getSecondCache(String)
 * @author wangzhen520
 * @date 2022/12/9
 */
public abstract class AbstractMultilevelCacheManager implements CacheManager {

    private final ConcurrentMap<String, MultilevelCache> cacheMap = new ConcurrentHashMap<>(16);

    /**
     * 是否動態生成
     * @see MultilevelCache
     */
    protected boolean dynamic = true;
    /**
     * 預設開啟一級快取
     */
    protected boolean enableFirstCache = true;
    /**
     * 是否允許空值
     */
    protected boolean allowNullValues = true;

    /**
     * ump監控字首 不設定不開啟監控
     */
    private String umpKeyPrefix;


    protected MultilevelCache createMultilevelCache(String name) {
        Assert.hasLength(name, "createMultilevelCache name is not null");
        MultilevelCache multilevelCache = new MultilevelCache(allowNullValues);
        multilevelCache.setName(name);
        multilevelCache.setUmpKeyPrefix(this.umpKeyPrefix);
        multilevelCache.setEnableFirstCache(this.enableFirstCache);
        multilevelCache.setFirstCache(getFirstCache(name));
        multilevelCache.setSecondCache(getSecondCache(name));
        return multilevelCache;
    }


    @Override
    public Cache getCache(String name) {
        MultilevelCache cache = this.cacheMap.get(name);
        if (cache == null && dynamic) {
            synchronized (this.cacheMap) {
                cache = this.cacheMap.get(name);
                if (cache == null) {
                    cache = createMultilevelCache(name);
                    this.cacheMap.put(name, cache);
                }
                return cache;
            }
      }
      return cache;
    }

    @Override
    public Collection<String> getCacheNames() {
        return Collections.unmodifiableSet(this.cacheMap.keySet());
    }

    /**
     * 一級快取
     * @param name
     * @return
     */
    protected abstract Cache getFirstCache(String name);

    /**
     * 二級快取
     * @param name
     * @return
     */
    protected abstract Cache getSecondCache(String name);

    public boolean isDynamic() {
        return dynamic;
    }

    public void setDynamic(boolean dynamic) {
        this.dynamic = dynamic;
    }

    public boolean isEnableFirstCache() {
        return enableFirstCache;
    }

    public void setEnableFirstCache(boolean enableFirstCache) {
        this.enableFirstCache = enableFirstCache;
    }

    public String getUmpKeyPrefix() {
        return umpKeyPrefix;
    }

    public void setUmpKeyPrefix(String umpKeyPrefix) {
        this.umpKeyPrefix = umpKeyPrefix;
    }
}

基於jimDB Caffiene快取實現多級快取管理器


/**
 * 二級快取實現
 * caffeine + jimDB 二級快取
 * @author wangzhen520
 * @date 2022/12/9
 */
public class CaffeineJimMultilevelCacheManager extends AbstractMultilevelCacheManager {

    private CaffeineCacheManager caffeineCacheManager;

    private JimCacheManager jimCacheManager;

    public CaffeineJimMultilevelCacheManager(CaffeineCacheManager caffeineCacheManager, JimCacheManager jimCacheManager) {
        this.caffeineCacheManager = caffeineCacheManager;
        this.jimCacheManager = jimCacheManager;
        caffeineCacheManager.setAllowNullValues(this.allowNullValues);
    }

    /**
     * 一級快取實現
     * 基於caffeine實現
     * @see org.springframework.cache.caffeine.CaffeineCache
     * @param name
     * @return
     */
    @Override
    protected Cache getFirstCache(String name) {
        if(!isEnableFirstCache()){
            return null;
        }
        return caffeineCacheManager.getCache(name);
    }

    /**
     * 二級快取基於jimDB實現
     * @see com.jd.jim.cli.springcache.JimStringCache
     * @param name
     * @return
     */
    @Override
    protected Cache getSecondCache(String name) {
        return jimCacheManager.getCache(name);
    }
}

快取配置

/**
 * @author wangzhen520
 * @date 2022/12/9
 */
@Configuration
@EnableCaching
public class CacheConfiguration {

    /**
     * 基於caffeine + JimDB 多級快取Manager
     * @param firstCacheManager
     * @param secondCacheManager
     * @return
     */
    @Primary
    @Bean(name = "caffeineJimCacheManager")
    public CacheManager multilevelCacheManager(@Param("firstCacheManager") CaffeineCacheManager firstCacheManager,
                                               @Param("secondCacheManager") JimCacheManager secondCacheManager){
        CaffeineJimMultilevelCacheManager cacheManager = new CaffeineJimMultilevelCacheManager(firstCacheManager, secondCacheManager);
        cacheManager.setUmpKeyPrefix(String.format("%s.%s", UmpConstants.Key.PREFIX, UmpConstants.SYSTEM_NAME));
        cacheManager.setEnableFirstCache(true);
        cacheManager.setDynamic(true);
        return cacheManager;
    }

    /**
     * 一級快取Manager
     * @return
     */
    @Bean(name = "firstCacheManager")
    public CaffeineCacheManager firstCacheManager(){
        CaffeineCacheManager firstCacheManager = new CaffeineCacheManager();
        firstCacheManager.setCaffeine(Caffeine.newBuilder()
                .initialCapacity(firstCacheInitialCapacity)
                .maximumSize(firstCacheMaximumSize)
                .expireAfterWrite(Duration.ofSeconds(firstCacheDurationSeconds)));
        firstCacheManager.setAllowNullValues(true);
        return firstCacheManager;
    }

    /**
     * 初始化二級快取Manager
     * @param jimClientLF
     * @return
     */
    @Bean(name = "secondCacheManager")
    public JimCacheManager secondCacheManager(@Param("jimClientLF") Cluster jimClientLF){
        JimDbCache jimDbCache = new JimDbCache<>();
        jimDbCache.setJimClient(jimClientLF);
        jimDbCache.setKeyPrefix(MultilevelCacheConstants.SERVICE_RULE_MATCH_CACHE);
        jimDbCache.setEntryTimeout(secondCacheExpireSeconds);
        jimDbCache.setValueSerializer(new JsonStringSerializer(ServiceRuleMatchResult.class));
        JimCacheManager secondCacheManager = new JimCacheManager();
        secondCacheManager.setCaches(Arrays.asList(jimDbCache));
        return secondCacheManager;
    }

介面效能壓測

壓測環境

廊坊4C8G * 3

壓測結果

1、50併發時,未開啟快取,壓測5min,TP99: 67ms,TP999: 223ms,TPS:2072.39筆/秒,此時服務引擎cpu利用率40%左右;訂購履約cpu利用率70%左右,磁碟使用率4min後被打滿;

2、50併發時,開啟二級快取,壓測10min,TP99: 33ms,TP999: 38ms,TPS:28521.18.筆/秒,此時服務引擎cpu利用率90%左右,訂購履約cpu利用率10%左右,磁碟使用率3%左右;

快取命中分析

總呼叫次數:1840486/min 一級快取命中:1822820 /min 二級快取命中:14454/min
一級快取命中率:99.04%
二級快取命中率:81.81%

壓測資料

未開啟快取

image.png

開啟多級快取

image.png

監控資料

未開啟快取

下游應用由於4分鐘後磁碟打滿,效能到達瓶頸

介面UMP

image.png

服務引擎系統

image.png

訂購履約系統

image.png

開啟快取

上游系統CPU利用率90%左右,下游系統呼叫量明顯減少,CPU利用率僅10%左右

介面UMP

image.png

服務引擎系統

image.png

訂購履約系統:

image.png