基于Spring Cache实现CaffeinejimDB多级缓存实战

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了基于Spring Cache实现CaffeinejimDB多级缓存实战相关的知识,希望对你有一定的参考价值。

作者: 京东零售 王震

背景

在早期参与涅槃氛围标签中台项目中,前台要求接口性能999要求50ms以下,通过设计Caffeine、ehcache堆外缓存、jimDB三级缓存,利用内存、堆外、jimDB缓存不同的特性提升接口性能,

内存缓存采用Caffeine缓存,利用W-TinyLFU算法获得更高的内存命中率;同时利用堆外缓存降低内存缓存大小,减少GC频率,同时也减少了网络IO带来的性能消耗;利用JimDB提升接口高可用、高并发;后期通过压测及性能调优999性能<20ms

基于Spring

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

开启多级缓存

基于Spring

监控数据

未开启缓存

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

接口UMP

基于Spring

服务引擎系统

基于Spring

订购履约系统

基于Spring

开启缓存

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

接口UMP

基于Spring

服务引擎系统

基于Spring

订购履约系统:

基于Spring

以上是关于基于Spring Cache实现CaffeinejimDB多级缓存实战的主要内容,如果未能解决你的问题,请参考以下文章

使用Spring Cache实现广告缓存并基于RabbitMQ实现双写一致

基于Spring Cache实现CaffeinejimDB多级缓存实战

Spring Cache

Spring Cache:如何使用redis进行缓存数据?

spring使用@Cache的简单实现

SpringBoot2.0 基础案例(13):基于Cache注解模式,管理Redis缓存