作者: 京东零售 王震
在早期参与涅槃氛围标签中台项目中,前台要求接口性能999要求50ms以下,通过设计Caffeine、ehcache堆外缓存、jimDB三级缓存,利用内存、堆外、jimDB缓存不同的特性提升接口性能, 内存缓存采用Caffeine缓存,利用W-TinyLFU算法获得更高的内存命中率;同时利用堆外缓存降低内存缓存大小,减少GC频率,同时也减少了网络IO带来的性能消耗;利用JimDB提升接口高可用、高并发;后期通过压测及性能调优999性能<20ms
当时由于项目工期紧张,三级缓存实现较为臃肿、业务侵入性强、可读性差,在近期场景化推荐项目中,为B端商家场景化资源投放推荐,考虑到B端流量相对C端流量较小,但需保证接口性能稳定。采用SpringCache实现caffeine、jimDB多级缓存方案,实现了低侵入性、可扩展、高可用的缓存方案,极大提升了系统稳定性,保证接口性能小于100ms;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 | / * * * 分级缓存 * 基于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); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 | / * * * 多级缓存实现抽象类 * 一级缓存 * @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; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | / * * * 二级缓存实现 * 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); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | / * * * @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; } |
1 | 廊坊 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分钟后磁盘打满,性能到达瓶颈
上游系统CPU利用率90%左右,下游系统调用量明显减少,CPU利用率仅10%左右