乐趣区

关于spring:基于Spring-Cache实现CaffeinejimDB多级缓存实战

作者:京东批发 王震

背景

在晚期参加涅槃气氛标签中台我的项目中,前台要求接口性能 999 要求 50ms 以下,通过设计 Caffeine、ehcache 堆外缓存、jimDB 三级缓存,利用内存、堆外、jimDB 缓存不同的个性晋升接口性能,内存缓存采纳 Caffeine 缓存,利用 W -TinyLFU 算法取得更高的内存命中率;同时利用堆外缓存升高内存缓存大小,缩小 GC 频率,同时也缩小了网络 IO 带来的性能耗费;利用 JimDB 晋升接口高可用、高并发;前期通过压测及性能调优 999 性能 <20ms

过后因为我的项目工期缓和,三级缓存实现较为臃肿、业务侵入性强、可读性差,在近期场景化举荐我的项目中,为 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%

压测数据

未开启缓存
开启多级缓存

监控数据

未开启缓存

上游利用因为 4 分钟后磁盘打满,性能达到瓶颈

接口 UMP
服务引擎零碎
订购履约零碎
开启缓存

上游零碎 CPU 利用率 90% 左右,上游零碎调用量显著缩小,CPU 利用率仅 10% 左右

接口 UMP
服务引擎零碎
订购履约零碎:
退出移动版