美文网首页
基于Spring Cache实现Caffeine、jimDB多级

基于Spring Cache实现Caffeine、jimDB多级

作者: 京东云开发者 | 来源:发表于2023-01-30 10:18 被阅读0次
    作者: 京东零售 王震

    背景

    在早期参与涅槃氛围标签中台项目中,前台要求接口性能999要求50ms以下,通过设计Caffeine、ehcache堆外缓存、jimDB三级缓存,利用内存、堆外、jimDB缓存不同的特性提升接口性能,
    内存缓存采用Caffeine缓存,利用W-TinyLFU算法获得更高的内存命中率;同时利用堆外缓存降低内存缓存大小,减少GC频率,同时也减少了网络IO带来的性能消耗;利用JimDB提升接口高可用、高并发;后期通过压测及性能调优999性能<20ms
    [图片上传失败...(image-d88535-1675131436137)]

    当时由于项目工期紧张,三级缓存实现较为臃肿、业务侵入性强、可读性差,在近期场景化推荐项目中,为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-30f4a2-1675131436137)]

    开启多级缓存

    [图片上传失败...(image-593c7d-1675131436137)]

    监控数据

    未开启缓存

    下游应用由于4分钟后磁盘打满,性能到达瓶颈

    接口UMP

    [图片上传失败...(image-9d0f2b-1675131436137)]

    服务引擎系统

    [图片上传失败...(image-e315e2-1675131436137)]

    订购履约系统

    [图片上传失败...(image-7e77b8-1675131436137)]

    开启缓存

    上游系统CPU利用率90%左右,下游系统调用量明显减少,CPU利用率仅10%左右

    接口UMP

    [图片上传失败...(image-59e903-1675131436137)]

    服务引擎系统

    [图片上传失败...(image-d6fdc5-1675131436137)]

    订购履约系统:

    [图片上传失败...(image-d35bf6-1675131436137)]

    相关文章

      网友评论

          本文标题:基于Spring Cache实现Caffeine、jimDB多级

          本文链接:https://www.haomeiwen.com/subject/xjdmhdtx.html