记一次 Diamond 结合本地缓存的使用

需求背景

目前有个广告分发系统,需要把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 和本地缓存做到让配置可以被大流量尽可能安全地调用。这里面对缓存击穿情景说了一些自己的思考,如果有别的想法,欢迎大家多多讨论指教。

相关推荐
2401_857636394 分钟前
计算机课程管理平台:Spring Boot与工程认证的结合
java·spring boot·后端
也无晴也无风雨1 小时前
深入剖析输入URL按下回车,浏览器做了什么
前端·后端·计算机网络
2401_857610034 小时前
多维视角下的知识管理:Spring Boot应用
java·spring boot·后端
代码小鑫4 小时前
A027-基于Spring Boot的农事管理系统
java·开发语言·数据库·spring boot·后端·毕业设计
颜淡慕潇6 小时前
【K8S问题系列 | 9】如何监控集群CPU使用率并设置告警?
后端·云原生·容器·kubernetes·问题解决
独泪了无痕6 小时前
WebStorm 如何调试 Vue 项目
后端·webstorm
怒放吧德德7 小时前
JUC从实战到源码:JMM总得认识一下吧
java·jvm·后端
代码小鑫8 小时前
A025-基于SpringBoot的售楼管理系统的设计与实现
java·开发语言·spring boot·后端·毕业设计
前端SkyRain8 小时前
后端SpringBoot学习项目-项目基础搭建
spring boot·后端·学习
梦想画家8 小时前
理解Rust 生命周期、所有权和借用机制
开发语言·后端·rust