基於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
服務引擎系統
訂購履約系統:
- 測試用例設計指南
- 當你對 redis 說你中意的女孩是 Mia
- 【程式設計師日記】---當“微服務”遇到了“電餅鐺“
- 雲原生引擎單元測試實踐
- 一種基於實時大資料的圖指標解決方案
- Android效能優化-ListView自適應效能問題
- 一種非同步延遲佇列的實現方式
- 複雜度分析:如何分析、統計演算法的執行效率和資源消耗
- 精準測試之分散式呼叫鏈底層邏輯
- 樸素系統優化思維的實踐
- 深入瞭解 JavaScript 記憶體洩漏
- 交易履約之產品中心實踐
- 如何實現雲資料治理中的資料安全?
- 作為移動開發你不能不瞭解的編譯流程
- 基於Kafka和Elasticsearch構建實時站內搜尋功能的實踐
- 在京東如何做好前端系統的可觀測性
- 【微電平臺】-高併發實戰經驗-奇葩問題解決之旅
- 測試的底層邏輯
- 震驚,一行MD5居然讓小夥伴都回不了家!!!
- 聯邦學習開源框架FATE架構