记一次 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_882727573 小时前
低代码配置式组态软件-BY组态
前端·后端·物联网·低代码·前端框架
追逐时光者4 小时前
.NET 在 Visual Studio 中的高效编程技巧集
后端·.net·visual studio
大梦百万秋4 小时前
Spring Boot实战:构建一个简单的RESTful API
spring boot·后端·restful
斌斌_____5 小时前
Spring Boot 配置文件的加载顺序
java·spring boot·后端
路在脚下@5 小时前
Spring如何处理循环依赖
java·后端·spring
海绵波波1076 小时前
flask后端开发(1):第一个Flask项目
后端·python·flask
小奏技术7 小时前
RocketMQ结合源码告诉你消息量大为啥不需要手动压缩消息
后端·消息队列
AI人H哥会Java8 小时前
【Spring】控制反转(IoC)与依赖注入(DI)—IoC容器在系统中的位置
java·开发语言·spring boot·后端·spring
凡人的AI工具箱8 小时前
每天40分玩转Django:Django表单集
开发语言·数据库·后端·python·缓存·django
奔跑草-9 小时前
【数据库】SQL应该如何针对数据倾斜问题进行优化
数据库·后端·sql·ubuntu