项目背景
xx系统中对容量和耗时有较高要求,以支付优惠立减为例,每个用户咨询可用立减时,都会过一遍全量生效活动。目前日常活动数3000+,目标2w+;日常秒级咨询量1w+,大促22w+。所以如何支撑日常和大促的业务非常具有挑战性。
对此我们做了很多优化,其中缓存是整个优化的基石。我们对优惠模型、优惠活动预算和优惠周期预算等数据都做了缓存,但在日常开发中存在以下问题:
- 没有标准的缓存设计方案,需要翻其他系统的代码去理解怎么做;
- 开发成本高,搭建一套缓存要创建多个类,其中较多代码可复用;
- 容易踩坑,比如数据一致性问题、缓存击穿、热点key问题等。
本次缓存设计参考自yy系统,其代码经过了长时间的验证,所以我们希望输出一套缓存标准化方法,尽量满足当前已有业务场景,经过验证后能推广至其他系统,帮助大家夯实缓存系统,为业务发展保驾护航。
多级缓存整体设计
为什么需要多级缓存?
通过建设 可选的分级缓存结构、各层级缓存数据的不同scope、各层级不同的更新策略 的多级缓存,减少网络IO,极大的提高各个应用节点获取数据的速度。
适用场景
- 针对数据变更较少。如大促期间的优惠配置、商户支付规则配置等。
- 访问量非常大的。这个概念需要case by case去看,一般是某个接口会对某批数据高频查询使用的场景,数据QPS > 10w or 接口TPS > 10w
- 接受较短时间的数据同步延迟的场景。
- 不接受 既要 非常满足CAP能力,又要 保证数据吞吐量,还要 多级缓存结构的业务通用性,这是非常不合理的述求,异常情况兼容处理的 ROI 太低,建议由提出这个想法的人来做。分布式多级缓存的 Consistency 和 Availability,只能是尽量满足,如果业务能接受一些技术层面的规则,我们的架构就能在 Consistency 和 Availability 上做的更好。
分级缓存结构
参照已有的系统,有较为常用的三级缓存结构:
- 常驻缓存:存放静态数据或热点数据,一般没有超时时间也不会被剔除。为解决变更数据的一致性问题,需要数据推送更新一定成功,定时任务只校验数据一致性问题;
- LRU缓存:存放懒加载的热点数据,使用LRU淘汰机制和打散的过期时间维护缓存数据;
- 远端缓存:存放近期的全量数据,会设置较长的过期时间,尽量不被击穿,保护数据源;
- 数据源:存放全量数据,通常是数据库或者外部查询接口。
各级缓存形成一个数据正金字塔,流量访问倒金字塔,越上层存放着越经常访问的数据,承担着更多的流量。
说明:
- 为什么要常驻缓存和LRU缓存?
- 预读失效和缓存污染问题
- 如果只有一个LRU缓存,那么在预热时或批量读取数据时,导致真正的热点数据不在LRU中,导致缓存命中率降低。
- 两个缓存的设计,参考的是InnoDB和Linux缓存页读取的思路,更多的内容可以参考
- 预读失效和缓存污染问题
- 常驻缓存、LRU缓存的数据有什么差异?
a. 长期来看,常驻缓存和LRU缓存中的数据会呈现互补的关系。
缓存更新机制(Consistency保障)
多级缓存更新设计时,应考虑的问题
先抛出业务问题,再看解决的方案。
- DB 数据变更后,如何尽快的让业务使用到最新的数据?
- 需要主动更新的方式,尽快变更缓存中的数据
- 在1更新数据时,由于一些未知因素导致缓存数据更新失败,该如何处理?
- 为避免部分数据更新成功,部分数据更新失败,需要保证更新操作的原子性,既有更新的操作,有更新失败的回滚操作即可。(不建议对小概率的异常场景,进行过多的设计)
- 如何及时发现各级缓存的数据不一致问题和使用情况
- 主动增量变更数据时,保证操作的原子性
- 定时任务定时扫描
- 为什么不要延时双删?怎么解决在更新数据的同时,把历史数据加载到了缓存中,导致脏数据长时间在Cache中。延时双删方案
- 本业务方案中,不使用删除的方式,在完成DB的数据变更后,使用更新DB的数据更新到各级缓存,可以解决延时双删方案中,读取并使用老的数据更新缓存的问题。
- 同步更新DB和Cache 或 延时双删的策略,需按业务场景自行决策。
- 如何解决热点key的大批量请求影响系统的运行问题?
- 热点key一般都会被放在一级常驻缓存中,正常来讲,不影响单台node的运行
- 若疏忽上面的步骤,且一级LRU缓存中,没有对应的数据,那么将请求到远端缓存中,此时应当可以得到数据,也可以解决热点key问题对应用的影响
- 若上面两级缓存中都不存在,我们在远端缓存查询DB时,使用分布式排他锁,避免大量请求到DB端,也可以解决绝大部分场景下的热点key问题对应用的影响。
Consistency 由推拉结合的方式来保障,但不同层级的缓存操作流程不一样,整体架构图如下:
通过业务平台被动的数据变更流程
备注:
- 蓝色为业务管理系统,绿色为应用服务
多级缓存主动增量更新机制
备注:
- 缓存穿透问题的解决:
- 查询前校验key的合法性
- 设置守护值,设置一个较短的过期时间
- 在查询DB的前一级缓存中添加锁(分布式或单机锁),控制缓存击穿。只解决DB层的缓存击穿,其他缓存层级间不处理该问题
- 缓存雪崩问题的解决:
- 设置过期时间时,添加了随机值
定时器缓存核对任务
- 通过指定的策略(loop:30min),扫描缓存中的所有Key,并和DB的数据源进行对比;若数据有差异,则抛出告警,不更新缓存;
- 记录各个缓存层级基础信息,如命中率、内存使用情况、key情况等
缓存预热加载机制
- 注册应用启动事件,预热失败时,阻塞应用的启动,由人工来排查原因
- 通过缓存管理器中的LevelCacheManager#levelCacheLoad(Collection< K > keys),将不同的缓存进行刷新,流程:
getAll >> get >> memCache >> lruCache >> remoteCache >> dataSrouce >> putRemoteCache >> putLruCache- 参数keys的捞取策略,默认最近最新1k条数据,可以在管理器中指定方案
- 常驻缓存的内容是提前分析出来,通过缓存管理器主动加载数据
类结构设计
抽象分级缓存组件主要由三个部分组成:
- 缓存:定义了缓存的标准方法,实现类主要有常驻缓存、LRU缓存、远端缓存
- 数据源:定义了数据源的标准方法,目前仅查询方法;
- 分级缓存管理器:定义了分级缓存系统的标准方法,包括查询方法、缓存更新方法、各级缓存获取方法。
另外用单例模式分别实现了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 :
- 堆内缓存,数据量需要合理设置,否则将加重GC的负担。可以考虑将一级常驻缓存、一级LRU缓存放在堆外缓存中。
- 一级LRU缓存,可以使用带过期时间的LRU淘汰策略来缓存数据,避免 LUR 中长期存在未淘汰且未被使用的数据。
- 简化集成方的接入成本。
参考文献: