作者: 京东零售 王震

背景

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

服务引擎系统

订购履约系统:

基于Spring Cache实现Caffeine、jimDB多级缓存实战的更多相关文章

  1. 【开源项目系列】如何基于 Spring Cache 实现多级缓存(同时整合本地缓存 Ehcache 和分布式缓存 Redis)

    一.缓存 当系统的并发量上来了,如果我们频繁地去访问数据库,那么会使数据库的压力不断增大,在高峰时甚至可以出现数据库崩溃的现象.所以一般我们会使用缓存来解决这个数据库并发访问问题,用户访问进来,会先从 ...

  2. 基于Spring Cache实现二级缓存(Caffeine+Redis)

    一.聊聊什么是硬编码使用缓存? 在学习Spring Cache之前,笔者经常会硬编码的方式使用缓存. 我们来举个实际中的例子,为了提升用户信息的查询效率,我们对用户信息使用了缓存,示例代码如下: @A ...

  3. 一个缓存使用案例:Spring Cache VS Caffeine 原生 API

    最近在学习本地缓存发现,在 Spring 技术栈的开发中,既可以使用 Spring Cache 的注解形式操作缓存,也可用各种缓存方案的原生 API.那么是否 Spring 官方提供的就是最合适的方案 ...

  4. 【快学SpringBoot】Spring Cache+Redis实现高可用缓存解决方案

    前言 之前已经写过一篇文章介绍SpringBoot整合Spring Cache,SpringBoot默认使用的是ConcurrentMapCacheManager,在实际项目中,我们需要一个高可用的. ...

  5. SpringBoot 结合 Spring Cache 操作 Redis 实现数据缓存

    系统环境: Redis 版本:5.0.7 SpringBoot 版本:2.2.2.RELEASE 参考地址: Redus 官方网址:https://redis.io/ 博文示例项目 Github 地址 ...

  6. 基于 Spring Cloud 完整的微服务架构实战

    本项目是一个基于 Spring Boot.Spring Cloud.Spring Oauth2 和 Spring Cloud Netflix 等框架构建的微服务项目. @作者:Sheldon地址:ht ...

  7. JAVA缓存规范 —— 虽迟但到的JCache API与天生不俗的Spring Cache

    大家好,又见面了. 本文是笔者作为掘金技术社区签约作者的身份输出的缓存专栏系列内容,将会通过系列专题,讲清楚缓存的方方面面.如果感兴趣,欢迎关注以获取后续更新. 有诗云"纸上得来终觉浅,绝知 ...

  8. 「性能提升」扩展 Spring Cache 支持多级缓存

    为什么多级缓存 缓存的引入是现在大部分系统所必须考虑的 redis 作为常用中间件,虽然我们一般业务系统(毕竟业务量有限)不会遇到如下图 在随着 data-size 的增大和数据结构的复杂的造成性能下 ...

  9. Spring Cache缓存框架

    一.序言 Spring Cache是Spring体系下标准化缓存框架.Spring Cache有如下优势: 缓存品种多 支持缓存品种多,常见缓存Redis.EhCache.Caffeine均支持.它们 ...

  10. 使用 Spring data redis 结合 Spring cache 缓存数据配置

    使用 JavaConfig 方式配置 依赖 jar 包: jedis.spring-data-redis 首先需要进行 Redis 相关配置 @Configuration public class R ...

随机推荐

  1. 一文你带快速认识Vue-Router路由

    摘要:Vue Router是Vue.js 官方的路由管理器.它和Vue.js的核心深度集成,可以非常方便的用于SPA应用程序的开发. 本文分享自华为云社区<Vue-Router路由快速了解与应用 ...

  2. 带你了解WDR-GaussDB(DWS) 的性能监测报告

    摘要:通过本文,读者可知晓什么是WDR,如何创建性能数据快照以及生成WDR报告. 本文分享自华为云社区<WDR-GaussDB(DWS) 的性能监测报告>,作者:Zhang Jingyao ...

  3. Python FastAPI 获取 Neo4j 数据

    前提条件 先往Neo4j 里,准备数据 参考:https://www.cnblogs.com/vipsoft/p/17631347.html#创建传承人 搭建 FastAPI 项目:https://w ...

  4. Jenkins Pipeline 流水线 - 添加节点 使用代理

    Jenkins 安装在 Windows 上 Docker 在Linux 上 流程 将 Docker 在 Jenkins 节点中维护 Pipeline 中指定某些阶段使用哪个节点 添加节点 Checki ...

  5. NLog.config 配置

    NLog.confg 参考配置, NLog 热生效不需要重启服务 <?xml version="1.0" encoding="utf-8" ?> & ...

  6. 别再问我 2050 可以干什么,Make a Movie in a Day!

    2050 的每个年青人都是新物种.越是不可能见面的人见了面,就越会有奇迹发生,2050 努力让年青人见上另一位年青人,激发新的创造力.一起来 2050 看看? 2050 是什么? 2050 大会是由阿 ...

  7. 关于ABAP索引

    1.什么是索引 如果把数据库表看做一本书,索引就可以看做书的检索目录.目录中包含书中的大小标题(部分字段数据),并且有对应的数据表条目的页码(指针),可以快速的访问数据库表中对应行的所有字段内容 一个 ...

  8. WPF Window无边框窗体阴影效果

    WPF通过WindowChrome实现Window无边框窗体阴影效果 代码: <Window x:Class="SunCreate.PACP.Client.UI.GIS.CameraD ...

  9. 一个简单的例子看明白 async await Task

    测试代码: 1 using System; 2 using System.Collections.Generic; 3 using System.ComponentModel; 4 using Sys ...

  10. 数字U家,即刻出发!2022联合利华黑客马拉松启动!

    2022联合利华黑客马拉松火热报名倒计时! 欢迎各领域的个人及组织团队参与 人工智能.数据挖掘.市场规模预测.原材料与包装风险控制.AR/VR.低碳.消费者偏好研究等超多创新赛题,任选其一. 作为快消 ...