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

相关推荐
潘多编程26 分钟前
Spring Boot微服务架构设计与实战
spring boot·后端·微服务
2402_8575893632 分钟前
新闻推荐系统:Spring Boot框架详解
java·spring boot·后端
2401_8576226633 分钟前
新闻推荐系统:Spring Boot的可扩展性
java·spring boot·后端
Amagi.2 小时前
Spring中Bean的作用域
java·后端·spring
2402_857589362 小时前
Spring Boot新闻推荐系统设计与实现
java·spring boot·后端
J老熊3 小时前
Spring Cloud Netflix Eureka 注册中心讲解和案例示范
java·后端·spring·spring cloud·面试·eureka·系统架构
Benaso3 小时前
Rust 快速入门(一)
开发语言·后端·rust
sco52823 小时前
SpringBoot 集成 Ehcache 实现本地缓存
java·spring boot·后端
原机小子3 小时前
在线教育的未来:SpringBoot技术实现
java·spring boot·后端
吾日三省吾码3 小时前
详解JVM类加载机制
后端