基于Spring Cache实现Caffeine、jimDB多级缓存实战
作者: 京东零售 王震
背景
在早期参与涅槃氛围标签中台项目中,前台要求接口性能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
服务引擎系统
订购履约系统:

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
#HarmonyOS小课堂# 速览 课程精华第三期:ArkTS基础知识
#HarmonyOS小课堂# 从“0”开始走进HarmonyOS应用开发 了解3.1版本 #HarmonyOS第一课#第三期精彩内容 一图速解HarmonyOS应用开发语言ArkTS 回顾了解3.1版本 #HarmonyOS第一课# 第一期精彩内容 回顾了解3.1版本 #HarmonyOS第一课# 第二期精彩内容 继续了解3.1版本 #HarmonyOS第一课# 第四期上精彩内容 点击学习#HarmonyOS第一课# 完整版课程 与华为开发者联盟学堂携手筑建鸿蒙世界,从“0”开始走进HarmonyOS应用开发
- 下一篇
Go语言DDD实战初级篇
导读 领域驱动设计(DDD)最简洁的描述可能是:如何在明确的限界上下文中创建通用语言的模型。通过 DDD思想设计开发的软件,在领域专家、开发者和软件本身之间不存在“翻译”,三者通过在限界上下文下的通用语言直接表示。而这个系列则是我们团队对 DDD 模式的探索和落地,旨在能帮助大家逐步揭开DDD的神秘面纱。 全文5259字,预计阅读时间14分钟。 一、限界上下文 1.1 前言 DDD分为战略设计和战术设计,战略设计就是划分子域和限界上下文的过程。领域划分为子域的通用划分形式是把领域划分为 核心子域、支撑子域、通用子域。我们在落地过程中常常会很容易划分出核心子域,一般设计mvp的时候mvp就是核心子域。但是领域划分出核心子域、支撑子域和通用子域之后就算划分完成了吗? 1.2 子域和限界上下文 实际上子域也是领域,一个公司不同部门关注的是一个大领域的不同子领域,在你关注的领域内也需要做这种子域的划分。 比如百度这个大公司,有很多部门,这些部门都属于互联网领域,但是每个部门又有自己关注的领域,比如游戏部门关注的是游戏领域、搜索部门关注的是搜索领域。 不同部门的领域还可以再继续划分出自己关注的领...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- Red5直播服务器,属于Java语言的直播服务器
- CentOS7设置SWAP分区,小内存服务器的救世主
- CentOS7安装Docker,走上虚拟化容器引擎之路
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
- CentOS关闭SELinux安全模块
- CentOS7,CentOS8安装Elasticsearch6.8.6
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- Linux系统CentOS6、CentOS7手动修改IP地址
- Windows10,CentOS7,CentOS8安装MongoDB4.0.16