需求背景
目前有个广告分发系统,需要把DB里的广告配置保存到 Diamond 上去。本文主要就是介绍在本次需求中如何针对面对的问题使用 Diamond。
Diamond是一个持久配置管理中间件。可以实现分布式场景下,中心化的持久配置管理,同时也支持基于发布订阅模型配置动态变更推送。 Diamond 介绍
设计
背景调研和痛点问题
在方案设计过程中,我们发现现有 4 个痛点问题:
1.Diamond 作为一个持久配置管理中间件,本身不适合于持续接受大流量的持续请求,需要增加本地缓存,增加本地缓存后,如果有非法的 dataid ,则会导致请求 Diamond 和 本地缓存时被击穿。
解决方案:在请求 Diamond 为空时,向本地缓存写入 Optional.empty() 而不是直接存入 null;
2.每次首次请求 Diamond 获取新一类广告配置时,需要注册监听器监听这份广告配置后续的变化,如果立即注册监听器,那么遇到非法dataId拉取 Diamond 返回 null 的情况时,会出现很多无效的注册;如果只在 Diamond 不返回 null 的情况下才注册监听器,那么可能遇到合法 dataId 无法正常注册监听器的情况。
解决方案:设计 2 个本地缓存,一个是会自动过期的本地缓存,存储那些拉取 Diamond 返回 null 的广告配置,这样被误伤的广告配置在调整回来后还能有机会注册监听器;另一个是不会自动过期的本地缓存,存储那些拉取 Diamond 返回非空的广告配置;
3.拉取 Diamond 配置可能存在慢调用的情况,会拉长整个调用前台应用的时间。
解决方案:利用 sentinel 做慢调用降级处理,遇到慢调用就返回 null。
4.每次机器重启时,本地缓存会被清理,而有些广告配置请求的流量很大, 本地缓存失效时,流量会都打到 Diamond 上造成不必要的压力
解决方案:在机器重启时,从 Diamond 里预加载那些请求量大的场景的广告配置到本地缓存上。
具体设计
同步请求广告配置链路
异步监听Diamond及更新本地缓存链路
核心思路代码
java
/**
* 获取缓存或者Diamond中的配置, 首次请求Diamond会写入缓存
*/
private AdSceneConfig getConfig(String adScene) {
Entry entry = null;
try {
Optional<AdSceneConfig> cacheResult = adConfigCacheRepo.getConfigFromCache(adScene);
if (Objects.nonNull(cacheResult)) {
return cacheResult.orElse(null);
}
// diamond 慢调用请求降级
entry = SphU.entry(DIAMOND_REQUEST_DOWNGRADE_CONTROL_KEY);
// 拿不到缓存数据,强制拉Diamond
return getConfigFromDiamond(adScene);
} catch (BlockException e) {
Logger.log(ResultEnum.ERROR_SENTINEL_BLOCK.getCode(), DIAMOND_REQUEST_DOWNGRADE_CONTROL_KEY,
adScene, EagleEye.getTraceId(), e);
return null;
} catch (Throwable e) {
Logger.log(ResultEnum.ERROR_LOAD_ADSCENE_CONFIG_EXCEPTION.getCode(), adScene, EagleEye.getTraceId(), e);
return null;
} finally {
if (Objects.nonNull(entry)) {
entry.exit();
}
}
}
java
/**
* 从diamond中获取配置
* @param adScene 广告场景标识
* @return 广告场景配置
* @throws Exception 获取异常
*/
public AdSceneConfig getConfigFromDiamond(String adScene) throws Exception {
String dataId = adScene;
String oriConfig = Diamond.getConfig(dataId, GROUP_ID, 500);
ErrorTraceLogger.log("queryDiamondData", adScene, oriConfig, EagleEye.getTraceId());
if (StringUtils.isBlank(oriConfig)) {
Logger.log(ResultEnum.ERROR_DATAID_IS_INVALID.getCode(), dataId, oriConfig, EagleEye.getTraceId());
// 避免同一个dataId持续缓存穿透, 同时写入过期缓存避免影响后续正常的 adScene 注册监听
adConfigCacheRepo.updateCacheWithExpire(adScene, Optional.empty());
return null;
}
// 注册监听
addListener4AdSceneConfig(dataId);
// 放入缓存
AdSceneConfig parsedConfig = JSON.parseObject(oriConfig, AdSceneConfig.class);
if (Objects.isNull(parsedConfig.getAdConfig())) {
adConfigCacheRepo.updateCacheNoExpire(adScene, Optional.empty());
Logger.log(ResultEnum.ERROR_DIAMOND_CONFIG_IS_EMPTY.getCode(), adScene, EagleEye.getTraceId());
return null;
}
adConfigCacheRepo.updateCacheNoExpire(adScene, Optional.of(parsedConfig));
return parsedConfig;
}
java
/**
* 不自动过期缓存
*/
private static final Cache<String, Optional<AdSceneConfig>> CACHE = CacheBuilder.newBuilder()
.initialCapacity(xx)
.maximumSize(xx)
.build();
/**
* 自动过期缓存
*/
private static final Cache<String, Optional<AdSceneConfig>> EXPIRE_CACHE = CacheBuilder.newBuilder()
.initialCapacity(xx)
.maximumSize(xx)
.expireAfterWrite(1, TimeUnit.MINUTES)
.build();
/**
* 从本地缓存获取广告配置
*
* @param adScene 广告场景标识
* @return 广告配置
*/
public Optional<AdSceneConfig> getConfigFromCache(String adScene) {
Optional<AdSceneConfig> cacheResult = CACHE.getIfPresent(adScene);
if (Objects.nonNull(cacheResult)) {
return cacheResult;
}
Optional<AdSceneConfig> expireCacheResult = EXPIRE_CACHE.getIfPresent(adScene);
if (Objects.nonNull(expireCacheResult)) {
return expireCacheResult;
}
// 缓存中没有,再去拉diamond中的数据
return null;
}
/**
* 更新不自动过期缓存
*
* @param adScene 广告场景标识
* @param cacheResult 广告配置
*/
public void updateCacheNoExpire(String adScene, Optional<AdSceneConfig> cacheResult) {
CACHE.put(adScene, cacheResult);
}
/**
* 更新自动过期缓存
*
* @param adScene 广告场景标识
* @param cacheResult 广告配置
*/
public void updateCacheWithExpire(String adScene, Optional<AdSceneConfig> cacheResult) {
EXPIRE_CACHE.put(adScene, cacheResult);
}
总结
本文重要简单介绍了如何结合 Diamond 和本地缓存做到让配置可以被大流量尽可能安全地调用。这里面对缓存击穿情景说了一些自己的思考,如果有别的想法,欢迎大家多多讨论指教。