基于Spring Cache实现Caffeine、jimDB多级缓存实战

京东云开发者
• 阅读 270
作者: 京东零售 王震

背景

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

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

压测数据

未开启缓存

基于Spring Cache实现Caffeine、jimDB多级缓存实战

开启多级缓存

基于Spring Cache实现Caffeine、jimDB多级缓存实战

监控数据

未开启缓存

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

接口UMP

基于Spring Cache实现Caffeine、jimDB多级缓存实战

服务引擎系统

基于Spring Cache实现Caffeine、jimDB多级缓存实战

订购履约系统

基于Spring Cache实现Caffeine、jimDB多级缓存实战

开启缓存

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

接口UMP

基于Spring Cache实现Caffeine、jimDB多级缓存实战

服务引擎系统

基于Spring Cache实现Caffeine、jimDB多级缓存实战

订购履约系统:

基于Spring Cache实现Caffeine、jimDB多级缓存实战

点赞
收藏
评论区
推荐文章
京东云开发者 京东云开发者
11个月前
京东APP百亿级商品与车关系数据检索实践 | 京东云技术团队
本文主要讲解了京东百亿级商品车型适配数据存储结构设计以及怎样实现适配接口的高性能查询。通过京东百亿级数据缓存架构设计实践案例,简单剖析了jimdb的位图(bitmap)函数和lua脚本应用在高性能场景。希望通过本文,读者可以对缓存的内部结构知识有一定了解,并且能够以最小的内存使用代价将位图(bitmap)灵活应用到各个高性能实际场景。
Wesley13 Wesley13
2年前
J2Cache 和普通缓存框架有何不同,它解决了什么问题?
不少人看到J2Cache第一眼时,会认为这就是一个普普通通的缓存框架,和例如Ehcache、Caffeine、SpringCache之类的项目没什么区别,无非是造了一个新的轮子而已。事实上完全不是一回事!目前缓存的解决方案一般有两种:内存缓存(如Ehcache)——速度快,进程内可用集中式缓存(如Redis)——
Wesley13 Wesley13
2年前
J2Cache 没有 Redis 也可以实现多节点的缓存同步
J2Cache是一个两级的缓存框架,第一级是基于内存的数据缓存,支持caffeine、ehcache2和ehcache3,二级缓存只支持redis。在某些生产环境中你可能没有redis,但是又希望多个应用节点间的缓存数据是同步的。配置的方法很简单:1\.首先关闭二级缓存(使用none替代redis)j2cache
Wesley13 Wesley13
2年前
Java本地缓存框架系列
Caffeine是一个基于Java8的高性能本地缓存框架,其结构和GuavaCache基本一样,api也一样,基本上很容易就能替换。Caffeine实际上就是在GuavaCache的基础上,利用了一些Java8的新特性,提高了某些场景下的性能效率。这一章节我们会从Caffeine的使用引入,并提出一些问题,之后分析其源代码解
Stella981 Stella981
2年前
Spring Cache缓存技术的介绍
缓存用于提升系统的性能,特别适用于一些对资源需求比较高的操作。本文介绍如何基于springbootcache技术,使用caffeine作为具体的缓存实现,对操作的结果进行缓存。demo场景本demo将创建一个web应用,提供两个Rest接口。一个接口用于接受查询请求,并有条件的缓存查询结果。另一个接口用于获取所有缓存的数据,用于监控
Wesley13 Wesley13
2年前
CPU缓存和内存屏障
CPU性能优化手段缓存为了提高程序运行的性能,现代CPU在很多方面对程序进行了优化。例如:CPU高速缓存。尽可能地避免处理器访问主内存的时间开销,处理器大多会利用缓存(cache)以提高性能。!(https://oscimg.oschina.net/oscnet/bbe04d9c9b6eb586bfccbd23808
Stella981 Stella981
2年前
Redis5.0:简单的集群模式——主从模式详解
主从模式主从模式是最简单的集群模式,其实就是复制基本只能解决读写分离问题,主机服务器一旦宕机基本完蛋,不具备高可用。基本上redis的性能瓶颈主要在于网络IO和内存主频上面,单机版Redis在不考虑高可用的情况下基本满足80%的项目需要,因为单机版Redis可以实现10W/S的请求,除非缓存KV值过大,通过读写分离缓存网卡的压
Easter79 Easter79
2年前
Tachyon 0.7.1伪分布式集群安装与测试
Tachyon是一个高容错的分布式文件系统,允许文件以内存的速度在集群框架中进行可靠的共享,就像Spark和MapReduce那样。通过利用信息继承,内存侵入,Tachyon获得了高性能。Tachyon工作集文件缓存在内存中,并且让不同的Jobs/Queries以及框架都能内存的速度来访问缓存文件。因此,Tachyon可以减少那些需要经常使用的数据集通过
京东云开发者 京东云开发者
2星期前
一招MAX降低10倍,现在它是我的了 | 京东云技术团队
性能优化是一场永无止境的旅程。到家门店系统,作为到家核心基础服务之一,门店C端接口有着调用量高,性能要求高的特点。C端服务经过演进,核心接口先查询本地缓存,如果本地缓存没有命中,再查询Redis。本地缓存命中率99%,服务性能比较平稳。
京东云开发者 京东云开发者
2个月前
本地缓存Ehcache的应用实践 | 京东云技术团队
java本地缓存包含多个框架,其中常用的包括:Caffeine、GuavaCache和Ehcache,其中Caffeine号称本地缓存之王,也是近年来被众多程序员推崇的缓存框架,同时也是SpringBoot内置的本地缓存实现。但是除了Caffeine之外,