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

相关推荐
有来技术8 小时前
Spring Boot 4 + Vue3 企业级多租户 SaaS:从共享 Schema 架构到商业化套餐设计
java·vue.js·spring boot·后端
东东5169 小时前
学院个人信息管理系统 (springboot+vue)
vue.js·spring boot·后端·个人开发·毕设
三水不滴9 小时前
Redis缓存更新策略
数据库·经验分享·redis·笔记·后端·缓存
小邓吖10 小时前
自己做了一个工具网站
前端·分布式·后端·中间件·架构·golang
大爱编程♡11 小时前
SpringBoot统一功能处理
java·spring boot·后端
好好研究13 小时前
总结SSM设置欢迎页的方式
xml·java·后端·mvc
小马爱打代码14 小时前
Spring Boot:第三方 API 调用的企业级容错设计
java·spring boot·后端
csdn2015_15 小时前
springboot task
java·spring boot·后端
czlczl2002092515 小时前
Spring Boot :如何高性能地在 Filter 中获取响应体(Response Body)
java·spring boot·后端
码界奇点16 小时前
基于Spring Boot和Vue3的无头内容管理系统设计与实现
java·spring boot·后端·vue·毕业设计·源代码管理