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

作者: 京东零售 王震

背景

在早期参与涅槃氛围标签中台项目中,前台要求接口性能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

1/** 2 * 分级缓存 3 * 基于Caffeine + jimDB 实现二级缓存 4 * @author wangzhen520 5 * @date 2022/12/9 6 */ 7public class MultilevelCache extends AbstractValueAdaptingCache { 8 9 /** 10 * 缓存名称 11 */ 12 private String name; 13 14 /** 15 * 是否开启一级缓存 16 */ 17 private boolean enableFirstCache = true; 18 19 /** 20 * 一级缓存 21 */ 22 private Cache firstCache; 23 24 /** 25 * 二级缓存 26 */ 27 private Cache secondCache; 28 29 @Override 30 protected Object lookup(Object key) { 31 Object value; 32 recordCount(getUmpKey(this.getName(), UMP_GET_CACHE, UMP_ALL)); 33 if(enableFirstCache){ 34 //查询一级缓存 35 value = getWrapperValue(getForFirstCache(key)); 36 log.info("{}#lookup getForFirstCache key={} value={}", this.getClass().getSimpleName(), key, value); 37 if(value != null){ 38 return value; 39 } 40 } 41 value = getWrapperValue(getForSecondCache(key)); 42 log.info("{}#lookup getForSecondCache key={} value={}", this.getClass().getSimpleName(), key, value); 43 //二级缓存不为空,则更新一级缓存 44 boolean putFirstCache = (Objects.nonNull(value) || isAllowNullValues()) && enableFirstCache; 45 if(putFirstCache){ 46 recordCount(getUmpKey(this.getName(), UMP_FIRST_CACHE, UMP_NO_HIT)); 47 log.info("{}#lookup put firstCache key={} value={}", this.getClass().getSimpleName(), key, value); 48 firstCache.put(key, value); 49 } 50 return value; 51 } 52 53 54 @Override 55 public void put(Object key, Object value) { 56 if(enableFirstCache){ 57 checkFirstCache(); 58 firstCache.put(key, value); 59 } 60 secondCache.put(key, value); 61 } 62 63 /** 64 * 查询一级缓存 65 * @param key 66 * @return 67 */ 68 private ValueWrapper getForFirstCache(Object key){ 69 checkFirstCache(); 70 ValueWrapper valueWrapper = firstCache.get(key); 71 if(valueWrapper == null || Objects.isNull(valueWrapper.get())){ 72 recordCount(getUmpKey(this.getName(), UMP_FIRST_CACHE, UMP_NO_HIT)); 73 } 74 return valueWrapper; 75 } 76 77 /** 78 * 查询二级缓存 79 * @param key 80 * @return 81 */ 82 private ValueWrapper getForSecondCache(Object key){ 83 ValueWrapper valueWrapper = secondCache.get(key); 84 if(valueWrapper == null || Objects.isNull(valueWrapper.get())){ 85 recordCount(getUmpKey(this.getName(), UMP_SECOND_CACHE, UMP_NO_HIT)); 86 } 87 return valueWrapper; 88 } 89 90 private Object getWrapperValue(ValueWrapper valueWrapper){ 91 return Optional.ofNullable(valueWrapper).map(ValueWrapper::get).orElse(null); 92 } 93 94}

多级缓存管理器抽象

1/** 2 * 多级缓存实现抽象类 3 * 一级缓存 4 * @see AbstractMultilevelCacheManager#getFirstCache(String) 5 * 二级缓存 6 * @see AbstractMultilevelCacheManager#getSecondCache(String) 7 * @author wangzhen520 8 * @date 2022/12/9 9 */ 10public abstract class AbstractMultilevelCacheManager implements CacheManager { 11 12 private final ConcurrentMap<String, MultilevelCache> cacheMap = new ConcurrentHashMap<>(16); 13 14 /** 15 * 是否动态生成 16 * @see MultilevelCache 17 */ 18 protected boolean dynamic = true; 19 /** 20 * 默认开启一级缓存 21 */ 22 protected boolean enableFirstCache = true; 23 /** 24 * 是否允许空值 25 */ 26 protected boolean allowNullValues = true; 27 28 /** 29 * ump监控前缀 不设置不开启监控 30 */ 31 private String umpKeyPrefix; 32 33 34 protected MultilevelCache createMultilevelCache(String name) { 35 Assert.hasLength(name, "createMultilevelCache name is not null"); 36 MultilevelCache multilevelCache = new MultilevelCache(allowNullValues); 37 multilevelCache.setName(name); 38 multilevelCache.setUmpKeyPrefix(this.umpKeyPrefix); 39 multilevelCache.setEnableFirstCache(this.enableFirstCache); 40 multilevelCache.setFirstCache(getFirstCache(name)); 41 multilevelCache.setSecondCache(getSecondCache(name)); 42 return multilevelCache; 43 } 44 45 46 @Override 47 public Cache getCache(String name) { 48 MultilevelCache cache = this.cacheMap.get(name); 49 if (cache == null && dynamic) { 50 synchronized (this.cacheMap) { 51 cache = this.cacheMap.get(name); 52 if (cache == null) { 53 cache = createMultilevelCache(name); 54 this.cacheMap.put(name, cache); 55 } 56 return cache; 57 } 58 } 59 return cache; 60 } 61 62 @Override 63 public Collection<String> getCacheNames() { 64 return Collections.unmodifiableSet(this.cacheMap.keySet()); 65 } 66 67 /** 68 * 一级缓存 69 * @param name 70 * @return 71 */ 72 protected abstract Cache getFirstCache(String name); 73 74 /** 75 * 二级缓存 76 * @param name 77 * @return 78 */ 79 protected abstract Cache getSecondCache(String name); 80 81 public boolean isDynamic() { 82 return dynamic; 83 } 84 85 public void setDynamic(boolean dynamic) { 86 this.dynamic = dynamic; 87 } 88 89 public boolean isEnableFirstCache() { 90 return enableFirstCache; 91 } 92 93 public void setEnableFirstCache(boolean enableFirstCache) { 94 this.enableFirstCache = enableFirstCache; 95 } 96 97 public String getUmpKeyPrefix() { 98 return umpKeyPrefix; 99 } 100 101 public void setUmpKeyPrefix(String umpKeyPrefix) { 102 this.umpKeyPrefix = umpKeyPrefix; 103 } 104}

基于jimDB Caffiene缓存实现多级缓存管理器

1 2/** 3 * 二级缓存实现 4 * caffeine + jimDB 二级缓存 5 * @author wangzhen520 6 * @date 2022/12/9 7 */ 8public class CaffeineJimMultilevelCacheManager extends AbstractMultilevelCacheManager { 9 10 private CaffeineCacheManager caffeineCacheManager; 11 12 private JimCacheManager jimCacheManager; 13 14 public CaffeineJimMultilevelCacheManager(CaffeineCacheManager caffeineCacheManager, JimCacheManager jimCacheManager) { 15 this.caffeineCacheManager = caffeineCacheManager; 16 this.jimCacheManager = jimCacheManager; 17 caffeineCacheManager.setAllowNullValues(this.allowNullValues); 18 } 19 20 /** 21 * 一级缓存实现 22 * 基于caffeine实现 23 * @see org.springframework.cache.caffeine.CaffeineCache 24 * @param name 25 * @return 26 */ 27 @Override 28 protected Cache getFirstCache(String name) { 29 if(!isEnableFirstCache()){ 30 return null; 31 } 32 return caffeineCacheManager.getCache(name); 33 } 34 35 /** 36 * 二级缓存基于jimDB实现 37 * @see com.jd.jim.cli.springcache.JimStringCache 38 * @param name 39 * @return 40 */ 41 @Override 42 protected Cache getSecondCache(String name) { 43 return jimCacheManager.getCache(name); 44 } 45}

缓存配置

1/** 2 * @author wangzhen520 3 * @date 2022/12/9 4 */ 5@Configuration 6@EnableCaching 7public class CacheConfiguration { 8 9 /** 10 * 基于caffeine + JimDB 多级缓存Manager 11 * @param firstCacheManager 12 * @param secondCacheManager 13 * @return 14 */ 15 @Primary 16 @Bean(name = "caffeineJimCacheManager") 17 public CacheManager multilevelCacheManager(@Param("firstCacheManager") CaffeineCacheManager firstCacheManager, 18 @Param("secondCacheManager") JimCacheManager secondCacheManager){ 19 CaffeineJimMultilevelCacheManager cacheManager = new CaffeineJimMultilevelCacheManager(firstCacheManager, secondCacheManager); 20 cacheManager.setUmpKeyPrefix(String.format("%s.%s", UmpConstants.Key.PREFIX, UmpConstants.SYSTEM_NAME)); 21 cacheManager.setEnableFirstCache(true); 22 cacheManager.setDynamic(true); 23 return cacheManager; 24 } 25 26 /** 27 * 一级缓存Manager 28 * @return 29 */ 30 @Bean(name = "firstCacheManager") 31 public CaffeineCacheManager firstCacheManager(){ 32 CaffeineCacheManager firstCacheManager = new CaffeineCacheManager(); 33 firstCacheManager.setCaffeine(Caffeine.newBuilder() 34 .initialCapacity(firstCacheInitialCapacity) 35 .maximumSize(firstCacheMaximumSize) 36 .expireAfterWrite(Duration.ofSeconds(firstCacheDurationSeconds))); 37 firstCacheManager.setAllowNullValues(true); 38 return firstCacheManager; 39 } 40 41 /** 42 * 初始化二级缓存Manager 43 * @param jimClientLF 44 * @return 45 */ 46 @Bean(name = "secondCacheManager") 47 public JimCacheManager secondCacheManager(@Param("jimClientLF") Cluster jimClientLF){ 48 JimDbCache jimDbCache = new JimDbCache<>(); 49 jimDbCache.setJimClient(jimClientLF); 50 jimDbCache.setKeyPrefix(MultilevelCacheConstants.SERVICE_RULE_MATCH_CACHE); 51 jimDbCache.setEntryTimeout(secondCacheExpireSeconds); 52 jimDbCache.setValueSerializer(new JsonStringSerializer(ServiceRuleMatchResult.class)); 53 JimCacheManager secondCacheManager = new JimCacheManager(); 54 secondCacheManager.setCaches(Arrays.asList(jimDbCache)); 55 return secondCacheManager; 56 }

接口性能压测

压测环境

廊坊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

点赞
收藏

评论区

加载中...

相关推荐

京东APP百亿级商品与车关系数据检索实践 | 京东云技术团队

本文主要讲解了京东百亿级商品车型适配数据存储结构设计以及怎样实现适配接口的高性能查询。通过京东百亿级数据缓存架构设计实践案例,简单剖析了jimdb的位图(bitmap)函数和lua脚本应用在高性能场景。希望通过本文,读者可以对缓存的内部结构知识有一定了解,并且能够以最小的内存使用代价将位图(bitmap)灵活应用到各个高性能实际场景。

J2Cache 没有 Redis 也可以实现多节点的缓存同步

J2Cache是一个两级的缓存框架,第一级是基于内存的数据缓存,支持caffeine、ehcache2和ehcache3,二级缓存只支持redis。在某些生产环境中你可能没有redis,但是又希望多个应用节点间的缓存数据是同步的。配置的方法很简单:1\.首先关闭二级缓存(使用none替代redis)j2cache

Spring Cache缓存技术的介绍

缓存用于提升系统的性能,特别适用于一些对资源需求比较高的操作。本文介绍如何基于springbootcache技术,使用caffeine作为具体的缓存实现,对操作的结果进行缓存。demo场景本demo将创建一个web应用,提供两个Rest接口。一个接口用于接受查询请求,并有条件的缓存查询结果。另一个接口用于获取所有缓存的数据,用于监控

CPU缓存和内存屏障

CPU性能优化手段缓存为了提高程序运行的性能,现代CPU在很多方面对程序进行了优化。例如:CPU高速缓存。尽可能地避免处理器访问主内存的时间开销,处理器大多会利用缓存(cache)以提高性能。!(https://oscimg.oschina.net/oscnet/bbe04d9c9b6eb586bfccbd23808

Tachyon 0.7.1伪分布式集群安装与测试

Tachyon是一个高容错的分布式文件系统,允许文件以内存的速度在集群框架中进行可靠的共享,就像Spark和MapReduce那样。通过利用信息继承,内存侵入,Tachyon获得了高性能。Tachyon工作集文件缓存在内存中,并且让不同的Jobs/Queries以及框架都能内存的速度来访问缓存文件。因此,Tachyon可以减少那些需要经常使用的数据集通过

京东APP百亿级商品与车关系数据检索实践

作者:京东零售张强导读本文主要讲解了京东百亿级商品车型适配数据存储结构设计以及怎样实现适配接口的高性能查询。通过京东百亿级数据缓存架构设计实践案例,简单剖析了jimdb的位图(bitmap)函数和lua脚本应用在高性能场景。希望通过本文,读者可以对缓存的内