Bootstrap

多级缓存建设方案

项目背景

   xx系统中对容量和耗时有较高要求,以支付优惠立减为例,每个用户咨询可用立减时,都会过一遍全量生效活动。目前日常活动数3000+,目标2w+;日常秒级咨询量1w+,大促22w+。所以如何支撑日常和大促的业务非常具有挑战性。

   对此我们做了很多优化,其中缓存是整个优化的基石。我们对优惠模型、优惠活动预算和优惠周期预算等数据都做了缓存,但在日常开发中存在以下问题:

  1. 没有标准的缓存设计方案,需要翻其他系统的代码去理解怎么做;
  2. 开发成本高,搭建一套缓存要创建多个类,其中较多代码可复用;
  3. 容易踩坑,比如数据一致性问题、缓存击穿、热点key问题等。

   本次缓存设计参考自yy系统,其代码经过了长时间的验证,所以我们希望输出一套缓存标准化方法,尽量满足当前已有业务场景,经过验证后能推广至其他系统,帮助大家夯实缓存系统,为业务发展保驾护航。

多级缓存整体设计

为什么需要多级缓存?

   通过建设 可选的分级缓存结构、各层级缓存数据的不同scope、各层级不同的更新策略 的多级缓存,减少网络IO,极大的提高各个应用节点获取数据的速度。

适用场景

  1. 针对数据变更较少。如大促期间的优惠配置、商户支付规则配置等。
  2. 访问量非常大的。这个概念需要case by case去看,一般是某个接口会对某批数据高频查询使用的场景,数据QPS > 10w or 接口TPS > 10w
  3. 接受较短时间的数据同步延迟的场景。
  4. 不接受 既要 非常满足CAP能力,又要 保证数据吞吐量,还要 多级缓存结构的业务通用性,这是非常不合理的述求,异常情况兼容处理的 ROI 太低,建议由提出这个想法的人来做。分布式多级缓存的 Consistency 和 Availability,只能是尽量满足,如果业务能接受一些技术层面的规则,我们的架构就能在 Consistency 和 Availability 上做的更好。

分级缓存结构

参照已有的系统,有较为常用的三级缓存结构:

  1. 常驻缓存:存放静态数据或热点数据,一般没有超时时间也不会被剔除。为解决变更数据的一致性问题,需要数据推送更新一定成功,定时任务只校验数据一致性问题;
  2. LRU缓存:存放懒加载的热点数据,使用LRU淘汰机制打散的过期时间维护缓存数据;
  3. 远端缓存:存放近期的全量数据,会设置较长的过期时间,尽量不被击穿,保护数据源;
  4. 数据源:存放全量数据,通常是数据库或者外部查询接口。

各级缓存形成一个数据正金字塔,流量访问倒金字塔,越上层存放着越经常访问的数据,承担着更多的流量。

在这里插入图片描述
说明:

  1. 为什么要常驻缓存和LRU缓存?
    • 预读失效和缓存污染问题
      • 如果只有一个LRU缓存,那么在预热时或批量读取数据时,导致真正的热点数据不在LRU中,导致缓存命中率降低。
    • 两个缓存的设计,参考的是InnoDB和Linux缓存页读取的思路,更多的内容可以参考
  2. 常驻缓存、LRU缓存的数据有什么差异?
    a. 长期来看,常驻缓存和LRU缓存中的数据会呈现互补的关系。

缓存更新机制(Consistency保障)

多级缓存更新设计时,应考虑的问题

先抛出业务问题,再看解决的方案。

  1. DB 数据变更后,如何尽快的让业务使用到最新的数据?
    • 需要主动更新的方式,尽快变更缓存中的数据
  2. 在1更新数据时,由于一些未知因素导致缓存数据更新失败,该如何处理?
    • 为避免部分数据更新成功,部分数据更新失败,需要保证更新操作的原子性,既有更新的操作,有更新失败的回滚操作即可。(不建议对小概率的异常场景,进行过多的设计)
  3. 如何及时发现各级缓存的数据不一致问题和使用情况
    • 主动增量变更数据时,保证操作的原子性
    • 定时任务定时扫描
  4. 为什么不要延时双删?怎么解决在更新数据的同时,把历史数据加载到了缓存中,导致脏数据长时间在Cache中。延时双删方案
    • 本业务方案中,不使用删除的方式,在完成DB的数据变更后,使用更新DB的数据更新到各级缓存,可以解决延时双删方案中,读取并使用老的数据更新缓存的问题。
    • 同步更新DB和Cache 或 延时双删的策略,需按业务场景自行决策。
  5. 如何解决热点key的大批量请求影响系统的运行问题?
    • 热点key一般都会被放在一级常驻缓存中,正常来讲,不影响单台node的运行
    • 若疏忽上面的步骤,且一级LRU缓存中,没有对应的数据,那么将请求到远端缓存中,此时应当可以得到数据,也可以解决热点key问题对应用的影响
    • 若上面两级缓存中都不存在,我们在远端缓存查询DB时,使用分布式排他锁,避免大量请求到DB端,也可以解决绝大部分场景下的热点key问题对应用的影响。

   Consistency 由推拉结合的方式来保障,但不同层级的缓存操作流程不一样,整体架构图如下:
在这里插入图片描述

通过业务平台被动的数据变更流程

在这里插入图片描述
备注:

  1. 蓝色为业务管理系统,绿色为应用服务

多级缓存主动增量更新机制

请添加图片描述

备注:

  1. 缓存穿透问题的解决:
    • 查询前校验key的合法性
    • 设置守护值,设置一个较短的过期时间
    • 在查询DB的前一级缓存中添加锁(分布式或单机锁),控制缓存击穿。只解决DB层的缓存击穿,其他缓存层级间不处理该问题
  2. 缓存雪崩问题的解决:
    • 设置过期时间时,添加了随机值

定时器缓存核对任务

  1. 通过指定的策略(loop:30min),扫描缓存中的所有Key,并和DB的数据源进行对比;若数据有差异,则抛出告警,不更新缓存;
  2. 记录各个缓存层级基础信息,如命中率、内存使用情况、key情况等

缓存预热加载机制

  1. 注册应用启动事件,预热失败时,阻塞应用的启动,由人工来排查原因
  2. 通过缓存管理器中的LevelCacheManager#levelCacheLoad(Collection< K > keys),将不同的缓存进行刷新,流程:
    getAll >> get >> memCache >> lruCache >> remoteCache >> dataSrouce >> putRemoteCache >> putLruCache
    • 参数keys的捞取策略,默认最近最新1k条数据,可以在管理器中指定方案
  3. 常驻缓存的内容是提前分析出来,通过缓存管理器主动加载数据

类结构设计

抽象分级缓存组件主要由三个部分组成:

  1. 缓存:定义了缓存的标准方法,实现类主要有常驻缓存、LRU缓存、远端缓存
  2. 数据源:定义了数据源的标准方法,目前仅查询方法;
  3. 分级缓存管理器:定义了分级缓存系统的标准方法,包括查询方法、缓存更新方法、各级缓存获取方法。

请添加图片描述

​ 另外用单例模式分别实现了DummyCache和DummyDataSource,用于实现缓存层级可选。如果没有定义某一级缓存,则其功能默认由Dummy实例承载,Dummy实例本身不做任何事情,所以自动将请求向下传递,直到某一真实层级。

缓存抽象类设计:

/**
 * 抽象缓存
 * @author 
 * @version $Id: AbstractCache.java, v 0.1 2022/9/20 11:27  Exp $
 * @see Cache
 */
public abstract class AbstractCache<K, V> implements Cache<K, V> {

    /**
     * LOGGER
     */
    protected final Logger logger;

    /**
     * 缓存名称
     */
    private final String   cacheName;

    /**
     * 构造函数
     * @param logger 日志
     * @param cacheName 缓存名称
     */
    public AbstractCache(Logger logger, String cacheName) {
        this.logger = logger;
        this.cacheName = cacheName;
    }

    /**
     * 从缓存中查询,为null则不存在
     * @see Cache#exists(Object)
     */
    @Override
    public boolean exists(K key) {
        return get(key) != null;
    }

    /**
     * 提供穿透保护,保护值不为null表示开启
     * @see Cache#get(Object) 
     */
    @Override
    public V get(K key) {
        if (!checkKey(key)) {
            return null;
        }
        V val = getPurely(key);
        if (val != null) {
            hit(key, val);
            return val;
        }
        miss(key);
        return getGuardValue();
    }

    /**
     * 直接获取
     * @param key 缓存键
     * @return 缓存值
     */
    protected abstract V getPurely(K key);

    /**
     * 命中时的处理
     * @param key 缓存键
     * @param val 缓存值
     */
    protected void hit(K key, V val) {
    }

    /**
     * 未命中时的处理
     * @param key 缓存键
     */
    protected void miss(K key) {
    }

    /**
     * 获取穿透保护值,为null表示不开启
     * @return 保护值,一般为不可变常量
     */
    protected V getGuardValue() {
        return null;
    }

    /**
     * 先直接查询,为null则用加载器加载。没有加锁,谨慎使用默认实现
     * @see Cache#get(Object, ValueLoader) 
     */
    @Override
    public V get(K key, ValueLoader<K, V> loader) {
        if (!checkKey(key)) {
            return null;
        }

        // 优先调用get,使用保护值
        V val = get(key);
        if (val != null) {
            return val;
        }

        // 走到这里说明没有保护值,需要加载,并放入缓存
        val = loader.load(key);
        // 由子类checkVal
        put(key, val);
        return val;
    }

    /**
     * 迭代缓存键列表单个查询
     * @see Cache#getAll(Collection) 
     */
    @Override
    public Map<K, V> getAll(Collection<K> keys) {
        Map<K, V> valMap = new HashMap<>();
        if (CollectionUtils.isEmpty(keys)) {
            return valMap;
        }
        keys.forEach(key -> AssetMapUtil.putValueExceptNull(valMap, key, get(key)));
        return valMap;
    }

    /**
     * @see Cache#put(Object, Object) 
     */
    @Override
    public void put(K key, V val) {
        put(key, val, expireTimeFor(val));
    }

    /**
     * 迭代缓存键值对Map单个存入缓存
     * @see Cache#putAll(Map) 
     */
    @Override
    public void putAll(Map<K, V> valMap) {
        AssetDefaultUtil.defaultMap(valMap).forEach(this::put);
    }

    /**
     * 迭代缓存键单个删除
     * @see Cache#removeAll(Collection) 
     */
    @Override
    public void removeAll(Collection<K> keys) {
        if (CollectionUtils.isEmpty(keys)) {
            return;
        }
        keys.forEach(this::remove);
    }

    /**
     * 校验缓存键合法
     * @param key 缓存键
     * @return true=合法
     */
    public boolean checkKey(K key) {
        return key != null;
    }

    /**
     * 校验缓存值合法
     * @param val 缓存值
     * @return true=合法
     */
    public boolean checkVal(V val) {
        return val != null;
    }

    /**
     * 获取过期时间
     * <ul>
     *     <li>根据缓存值判断过期时间,穿透保护值可以设置较短时间</li>
     *     <li>除了常驻缓存,过期时间都要大于0</li>
     * </ul>
     * @param val 缓存值
     * @return 过期时间
     */
    public abstract long expireTimeFor(V val);

    /**
     * 校验过期时间
     * @param expireTime 过期时间,单位毫秒
     * @return true通过
     */
    protected boolean checkExpireTime(long expireTime) {
        return expireTime > 0L;
    }

    /**
     * Getter method for property <tt>cacheName</tt>.
     *
     * @return property value of cacheName
     */
    public String getCacheName() {
        return cacheName;
    }

}

抽象数据源:

/**
 * 抽象数据源
 * @author 
 * @version $Id: AbstractDataSource.java, v 0.1 2022/9/20 11:48  Exp $
 * @see DataSource
 */
public abstract class AbstractDataSource<K, V> implements DataSource<K, V> {

    /**
     * LOGGER
     */
    protected final Logger logger;

    /**
     * 数据源名称
     */
    private final String   dataSourceName;

    /**
     * 构造函数
     * @param logger 日志
     * @param dataSourceName 数据源名称
     */
    public AbstractDataSource(Logger logger, String dataSourceName) {
        this.logger = logger;
        this.dataSourceName = dataSourceName;
    }

    /**
     * 提供穿透保护,保护值不为null表示开启
     * @see Cache#get(Object)
     */
    @Override
    public V get(K key) {
        if (!checkKey(key)) {
            return null;
        }
        V val = getPurely(key);
        if (val != null) {
            hit(key, val);
            return val;
        }
        miss(key);
        return getGuardValue();
    }

    /**
     * 直接获取
     * @param key 缓存键
     * @return 缓存值
     */
    protected abstract V getPurely(K key);

    /**
     * 命中时的处理
     * @param key 缓存键
     * @param val 缓存值
     */
    protected void hit(K key, V val) {
    }

    /**
     * 未命中时的处理
     * @param key 缓存键
     */
    protected void miss(K key) {
    }

    /**
     * 获取穿透保护值,为null表示不开启
     * @return 保护值,一般为不可变常量
     */
    protected V getGuardValue() {
        return null;
    }

    /**
     * @see DataSource#getAll(Collection)  
     */
    @Override
    public Map<K, V> getAll(Collection<K> keys) {
        Map<K, V> valMap = new HashMap<>();
        if (CollectionUtils.isEmpty(keys)) {
            return valMap;
        }
        keys.forEach(key -> AssetMapUtil.putValueExceptNull(valMap, key, get(key)));
        return valMap;
    }

    /**
     * 校验缓存键合法
     * @param key 缓存键
     * @return true=合法
     */
    public boolean checkKey(K key) {
        return key != null;
    }

    /**
     * Getter method for property <tt>dataSourceName</tt>.
     *
     * @return property value of dataSourceName
     */
    public String getDataSourceName() {
        return dataSourceName;
    }

}

抽象分级缓存管理器

/**
 * 抽象分级缓存系统,层级可选。只支持读,更新缓存时要区分单机和全局操作
 * <ul>
 *     <li>一级:常驻缓存</li>
 *     <li>二级:LRU缓存</li>
 *     <li>三级:远端缓存</li>
 *     <li>兜底:数据源</li>
 * </ul>
 * @author 
 * @version $Id: AbstractLevelCacheManager.java, v 0.1 2022/9/19 21:58  Exp $
 * @see Cache
 * @see LevelCacheManager
 */
public abstract class AbstractLevelCacheManager<K, V> implements LevelCacheManager<K, V> {

    /**
     * LOGGER
     */
    protected final Logger     logger;

    /**
     * 管理器名称
     */
    private final String       managerName;

    /**
     * 常驻缓存
     * <ul>
     *     <li>存放不过期数据</li>
     *     <li>不设置过期时间,最好设置最大容量</li>
     *     <li>一般由DRM推送加载</li>
     *     <li>缓存更新时,如果常驻缓存中存在也会更新</li>
     * </ul>
     */
    protected Cache<K, V>      memCache    = DummyCache.getInstance();

    /**
     * 本地缓存
     * <ul>
     *     <li>存放热点数据,一般要设置较短的过期时间</li>
     *     <li>注意计算容量,避免占用过多内存</li>
     * </ul>
     */
    protected Cache<K, V>      lruCache    = DummyCache.getInstance();

    /**
     * <p>远端缓存</p>
     * <ul>
     *     <li>存放全量数据</li>
     *     <li>一般要设置较长的过期时间,并由消息或定时任务刷新</li>
     * </ul>
     */
    protected Cache<K, V>      remoteCache = DummyCache.getInstance();

    /**
     * 数据源
     */
    protected DataSource<K, V> dataSource  = DummyDataSource.getInstance();

    /**
     * 构造函数
     * @param logger 日志
     * @param managerName 管理器名称
     */
    public AbstractLevelCacheManager(Logger logger, String managerName) {
        this.logger = logger;
        this.managerName = managerName;
    }

    /**
     * 判断分级缓存系统中是否存在缓存键
     * @see Cache#exists(Object)
     */
    public boolean exists(K key) {
        return get(key) != null;
    }

    /**
     * 从分级缓存系统中获取缓存值
     * <ul>
     *     <li>查询路径:常驻缓存 -> 本地缓存 -> 远端缓存 -> 数据源</li>
     *     <li>除常驻缓存外,下一层查询到的值会回填到上一层</li>
     * </ul>
     * @see Cache#get(Object)
     */
    @Override
    public V get(K key) {
        // 校验key是否合法
        if (!checkKey(key)) {
            return null;
        }

        // 从常驻缓存查询
        V val = getMemCache().get(key);
        if (val != null) {
            return val;
        }

        // LRU缓存 -> 远端缓存 -> 数据源
        return getStartFromLru(key);
    }

    /**
     * LRU缓存 -> 远端缓存 -> 数据源
     * @param key 缓存键
     * @return 缓存值
     */
    private V getStartFromLru(K key) {
        return getLruCache().get(key, remoteKey -> getRemoteCache().get(remoteKey,
            dataSourceKey -> getDataSource().get(dataSourceKey)));
    }

    /**
     * 从分级缓存系统中批量获取缓存值
     * @see Cache#getAll(Collection)
     */
    @Override
    public Map<K, V> getAll(Collection<K> keys) {
        Map<K, V> valMap = new HashMap<>();
        if (CollectionUtils.isEmpty(keys)) {
            return valMap;
        }
        keys.forEach(key -> AssetMapUtil.putValueExceptNull(valMap, key, get(key)));
        return valMap;
    }

    /**
     * 从分层缓存中查询,再放入常驻缓存
     * <ul>
     *     <li>不直接从数据源查询,避免全Zone请求太多</li>
     *     <li>穿透保护值也会放入常驻缓存</li>
     * </ul>
     * @see LevelCacheManager#memCacheLoad(Collection)  
     */
    @Override
    public void memCacheLoad(Collection<K> keys) {
        keys = AssetDefaultUtil.defaultList(filterKey(keys));
        List<K> sucKeys = new ArrayList<>();
        keys.forEach(key -> {
            V val = getStartFromLru(key);
            if (val != null) {
                getMemCache().put(key, val);
                sucKeys.add(key);
            }
        });
        AssetLogUtil.info(logger, String.format("[%s]常驻缓存加载成功:%s", managerName, sucKeys));
    }

    /**
     * 由定时任务调用,从远端缓存查询,覆盖LRU缓存
     * <ul>
     *     <li>远端缓存查不到,会从数据源查询,并放入远端缓存</li>
     *     <li>只刷新LRU缓存中已存在的key,避免缓存污染</li>
     * </ul>
     * @see LevelCacheManager#lruCacheRefresh(Collection) 
     */
    @Override
    public void lruCacheRefresh(Collection<K> keys) {
        keys = AssetDefaultUtil.defaultList(filterKey(keys));
        List<K> sucKeys = new ArrayList<>();
        keys.forEach(key -> {
            if (!getLruCache().exists(key)) {
                return;
            }
            V val = getRemoteCache().get(key, dataSourceKey -> getDataSource().get(dataSourceKey));
            if (val != null) {
                getLruCache().put(key, val);
                sucKeys.add(key);
            }
        });
        AssetLogUtil.info(logger, String.format("[%s]常驻缓存加载成功:%s", managerName, sucKeys));
    }

    /**
     * 从数据源查询放入远端缓存
     * @see LevelCacheManager#remoteCacheLoad(Collection)
     */
    @Override
    public void remoteCacheLoad(Collection<K> keys) {
        keys = AssetDefaultUtil.defaultList(filterKey(keys));
        Map<K, V> valMap = AssetDefaultUtil.defaultMap(getDataSource().getAll(keys));
        getRemoteCache().putAll(valMap);
        AssetLogUtil.info(logger, String.format("[%s]远端缓存加载成功:%s", managerName, valMap.keySet()));
    }

    /**
     * 直接查询加载整个分级缓存
     * @see LevelCacheManager#levelCacheLoad(Collection)
     */
    @Override
    public void levelCacheLoad(Collection<K> keys) {
        keys = AssetDefaultUtil.defaultList(filterKey(keys));
        Map<K, V> valMap = AssetDefaultUtil.defaultMap(getAll(keys));
        AssetLogUtil.info(logger, String.format("[%s]分级缓存加载成功:%s", managerName, valMap.keySet()));
    }

    /**
     * 校验缓存键合法
     * @param key 缓存键
     * @return true=合法
     */
    protected abstract boolean checkKey(K key);

    /**
     * 过滤不合法的键
     * @param keys 键列表
     * @return 过滤后的键列表
     */
    protected List<K> filterKey(Collection<K> keys) {
        if (CollectionUtils.isEmpty(keys)) {
            return new ArrayList<>();
        }
        return keys.stream().filter(this::checkKey).collect(Collectors.toList());
    }

    /**
     * 获取常驻缓存
     * @return 常驻缓存
     */
    public Cache<K, V> getMemCache() {
        return memCache;
    }

    /**
     * 获取LRU缓存
     * @return LRU缓存
     */
    public Cache<K, V> getLruCache() {
        return lruCache;
    }

    /**
     * 获取远端缓存
     * @return 远端缓存
     */
    public Cache<K, V> getRemoteCache() {
        return remoteCache;
    }

    /**
     * 获取数据源
     * @return 数据源
     */
    public DataSource<K, V> getDataSource() {
        return dataSource;
    }

    /**
     * Getter method for property <tt>managerName</tt>.
     *
     * @return property value of managerName
     */
    public String getManagerName() {
        return managerName;
    }

}

实现案例

业务信息实体缓存管理器

/**
 * 权益业务信息配置缓存管理器
 * @author 
 * @version $Id: AssetBizInfoConfigCacheManager.java, v 0.1 2022/9/20 11:36  Exp $
 */
public class AssetBizInfoConfigCacheManager extends
                                            AbstractLevelCacheManager<AssetBizInfoConfigCacheKey, AssetBizInfoConfigCacheVal> {

    /**
     * TBase客户端
     */
    private TbaseCacheClient                       tbaseCacheClient;

    /**
     * 权益业务信息配置仓储
     */
    private AssetBizInfoConfigRepository           assetBizInfoConfigRepository;

    /**
     * 缓存穿透保护值
     */
    public static final AssetBizInfoConfigCacheVal GUARD_VALUE = AssetBizInfoConfigCacheVal
        .getImmutableInstance(-1L, new Date(0));

    /**
     * 构造函数
     */
    public AssetBizInfoConfigCacheManager() {
        super(LoggerFactory.getLogger(ASSET_BIZ_INFO_CACHE_DIGEST_LOGGER), "权益业务信息配置缓存管理器");
        buildMemCache();
        buildLruCache();
        buildRemoteCache();
        buildDataSource();
    }

    /**
     * @see AbstractLevelCacheManager#checkKey(Object)
     */
    @Override
    protected boolean checkKey(AssetBizInfoConfigCacheKey key) {
        return key != null;
    }

    /**
     * 初始化常驻缓存
     */
    private void buildMemCache() {
        this.memCache = new AbstractMemCache<AssetBizInfoConfigCacheKey, AssetBizInfoConfigCacheVal>(
            logger, "权益业务信息配置常驻缓存") {
            /**
             * @see AbstractMemCache#initMemCache() 
             */
            @Override
            protected ConcurrentMap<AssetBizInfoConfigCacheKey, AssetBizInfoConfigCacheVal> initMemCache() {
                return new ConcurrentLinkedHashMap.Builder<AssetBizInfoConfigCacheKey, AssetBizInfoConfigCacheVal>()
                    .maximumWeightedCapacity(VccCacheConfigDrmResource.getInstance()
                        .getAssetBizInfoConfigMemCacheMaxSize())
                    .build();
            }

            /**
             * @see com.alipay.assettoolkit.component.levelcache.AbstractCache#hit(Object, Object)  
             */
            @Override
            protected void hit(AssetBizInfoConfigCacheKey key, AssetBizInfoConfigCacheVal val) {
                assetBizInfoConfigHitCountTmp.hitInMemory();
            }
        };
    }

    /**
     * 初始化本地缓存
     */
    private void buildLruCache() {
        this.lruCache = new AbstractLruCache<AssetBizInfoConfigCacheKey, AssetBizInfoConfigCacheVal>(
            logger, "权益业务信息配置本地缓存") {
            /**
             * @see AbstractLruCache#initLruCache() 
             */
            @Override
            protected TimeoutCache<AssetBizInfoConfigCacheKey, AssetBizInfoConfigCacheVal> initLruCache() {
                return new TimeoutCache.Builder<AssetBizInfoConfigCacheKey, AssetBizInfoConfigCacheVal>()
                    .maximumWeightedCapacity(VccCacheConfigDrmResource.getInstance()
                        .getAssetBizInfoConfigLruCacheMaxSize())
                    .build();
            }

            /**
             * 5+10打散,LRU会有Trigger刷新,所以弱依赖过期时间,降低对tbase调用量
             * @see com.alipay.assettoolkit.component.levelcache.AbstractCache#expireTimeFor(Object) 
             */
            @Override
            public long expireTimeFor(AssetBizInfoConfigCacheVal val) {
                return TimeUnit.SECONDS
                    .toMillis(
                        VccCacheConfigDrmResource.getInstance()
                            .getAssetBizInfoConfigLruCacheExpireTime()
                              + RandomUtils.nextInt(10 * 60));
            }

            /**
             * @see com.alipay.assettoolkit.component.levelcache.AbstractCache#hit(Object, Object) 
             */
            @Override
            protected void hit(AssetBizInfoConfigCacheKey key, AssetBizInfoConfigCacheVal val) {
                assetBizInfoConfigHitCountTmp.hitLruCache();
            }
        };
    }

    /**
     * 初始化远端缓存
     */
    private void buildRemoteCache() {
        this.remoteCache = new AbstractRemoteCache<AssetBizInfoConfigCacheKey, AssetBizInfoConfigCacheVal>(
            logger, "权益业务信息配置远端缓存") {
            /**
             * @see com.alipay.assettoolkit.component.levelcache.AbstractCache#getPurely(Object) 
             */
            @Override
            public AssetBizInfoConfigCacheVal getPurely(AssetBizInfoConfigCacheKey key) {
                CasValue<Serializable> val = tbaseCacheClient.getts(buildCacheKey(key));
                return val == null ? null : (AssetBizInfoConfigCacheVal) val.getValue();
            }

            /**
             * @see AbstractRemoteCache#expireTimeFor(Object) 
             */
            @Override
            public long expireTimeFor(AssetBizInfoConfigCacheVal val) {
                // 10天+1小时打散。穿透保护值也按10天过期,业务场景上不会出现后面又能查到的情况
                return VccCacheConfigDrmResource.getInstance()
                    .getAssetBizInfoRemoteCacheExpireTime() + RandomUtils.nextInt(60 * 60);
            }

            /**
             * @see com.alipay.assettoolkit.component.levelcache.AbstractCache#hit(Object, Object)
             */
            @Override
            protected void hit(AssetBizInfoConfigCacheKey key, AssetBizInfoConfigCacheVal val) {
                assetBizInfoConfigHitCountTmp.hitTair();
            }

            /**
             * @see com.alipay.assettoolkit.component.levelcache.Cache#put(Object, Object, long)
             */
            @Override
            public void put(AssetBizInfoConfigCacheKey key, AssetBizInfoConfigCacheVal val,
                            long expireTime) {
                if (checkKey(key) && checkVal(val) && checkExpireTime(expireTime)) {
                    tbaseCacheClient.setts(buildCacheKey(key), val, val.getGmtModified().getTime(),
                        0, (int) expireTime);
                }
            }

            /**
             * 构建tbase缓存建
             * @param key 缓存键
             * @return tbase缓存键
             */
            private String buildCacheKey(AssetBizInfoConfigCacheKey key) {
                return PREFIX_ASSET_BIZ_INFO_CONFIG + key.getUniqueKey();
            }

            /**
             * @see com.alipay.assettoolkit.component.levelcache.Cache#remove(Object) 
             */
            @Override
            public void remove(AssetBizInfoConfigCacheKey key) {
                if (checkKey(key)) {
                    tbaseCacheClient.del(buildCacheKey(key));
                }
            }
        };
    }

    /**
     * 初始化数据源
     */
    private void buildDataSource() {
        this.dataSource = new AbstractDataSource<AssetBizInfoConfigCacheKey, AssetBizInfoConfigCacheVal>(
            logger, "权益业务信息配置数据源") {
            /**
             * @see DataSource#get(Object) 
             */
            @Override
            public AssetBizInfoConfigCacheVal getPurely(AssetBizInfoConfigCacheKey key) {
                AssetBizInfoConfigModel model = assetBizInfoConfigRepository.query(key.getInfoKey(),
                    key.getInfoType());
                if (model == null || model.noNeedWriteCache()) {
                    LogUtil.warn(logger,
                        String.format("[%s]该信息配置不存在或无需写入缓存:%s", getManagerName(), model));
                    return null;
                }
                return toCacheVal(model);
            }

            /**
             * @see AbstractDataSource#hit(Object, Object) 
             */
            @Override
            protected void hit(AssetBizInfoConfigCacheKey key, AssetBizInfoConfigCacheVal val) {
                assetBizInfoConfigHitCountTmp.hitDb();
            }

            /**
             * @see AbstractDataSource#miss(Object) 
             */
            @Override
            protected void miss(AssetBizInfoConfigCacheKey key) {
                LogUtil.warn(logger, String.format("[%s]缓存被穿透[%s][%s],使用保护值", getManagerName(),
                    key.getInfoKey(), key.getInfoType()));
            }

            /**
             * @see AbstractDataSource#getGuardValue() 
             */
            @Override
            protected AssetBizInfoConfigCacheVal getGuardValue() {
                return GUARD_VALUE;
            }

            /**
             * 转为CacheVal
             */
            private AssetBizInfoConfigCacheVal toCacheVal(AssetBizInfoConfigModel model) {
                AssetBizInfoConfigCacheVal val = new AssetBizInfoConfigCacheVal();
                val.setEntityCount(model.getEntityCount());
                val.setGmtModified(model.getGmtModified());
                return val;
            }
        };
    }

    /**
     * Setter method for property <tt>tbaseCacheClient</tt>.
     *
     * @param tbaseCacheClient value to be assigned to property tbaseCacheClient
     */
    public void setTbaseCacheClient(TbaseCacheClient tbaseCacheClient) {
        this.tbaseCacheClient = tbaseCacheClient;
    }

    /**
     * Setter method for property <tt>assetBizInfoConfigRepository</tt>.
     *
     * @param assetBizInfoConfigRepository value to be assigned to property assetBizInfoConfigRepository
     */
    public void setAssetBizInfoConfigRepository(AssetBizInfoConfigRepository assetBizInfoConfigRepository) {
        this.assetBizInfoConfigRepository = assetBizInfoConfigRepository;
    }

}

从实现案例可以看到,搭建一个缓存系统,只需要创建一个类,实现一些必要的功能,减少了代码量,提升了可维护性。

心得

   分级缓存组件定义了一套标准的缓存系统实现方案,尽可能在内聚代码时,提供较好的扩展性。设计时考虑到了缓存常见问题的解决方案,比如返回伪数据解决缓存穿透问题、加本地锁解决热点key问题,另外提供了配套的缓存刷新方案,以保证数据一致性。

   但是使用过程中发现,要使用这套分级缓存组件,必须要理解上层抽象类的逻辑,才能知道怎么做才是正确的。找到内聚和扩展的平衡点,是一件比较难的事。如果重新设计,可能会考虑内聚更少一点,把更多的动作交给实现者掌控,才能让实现者更放心。

   另外实现三级缓存虽然只需要写一个public类,但还是要写四个匿名类,这里是我不太满意的地方,未来想要优化下,比如使用方法级注解,只需要几个方法即可实现简单的分级缓存系统。

后续改进Action :

  1. 堆内缓存,数据量需要合理设置,否则将加重GC的负担。可以考虑将一级常驻缓存、一级LRU缓存放在堆外缓存中。
  2. 一级LRU缓存,可以使用带过期时间的LRU淘汰策略来缓存数据,避免 LUR 中长期存在未淘汰且未被使用的数据。
  3. 简化集成方的接入成本。

参考文献:

GuavaCache GitHub Wiki
GuavaCache中文文档
缓存常见问题与解决方案

;