EdsCache通用缓存框架——(2.4)场景应用举例: 缓解缓存雪崩问题

达芬奇密码2018-06-22 16:08
问题描述
当数据加载比较耗时、并发较高时,会形成缓存雪崩,对DB造成较大的压力。
比如几万人同时开始一场考试作答,且考试缓存数据没有提前预热或已经超时,
试卷数据需要从N张表中进行查询,组装出完整的试卷数据,数据加载比较耗时,瞬间高并发没有命中缓存,全部穿透到DB,较长时间的加载加上DB性能的影响、接口超时重试等,形成缓存雪崩。

解决思路
主要解决思路是减少穿透到DB的线程数。
当一个JVM进程内,同时存在多个线程,对于同一个缓存key,没有命中缓存,需要从DB中加载数据时,
选其中一个线程作为Master,其余线程均为Slave,
只有Master会真正的执行数据加载,穿透到DB,并最终回写缓存;Slave则仅等待Master取回来的数据。

Master选择:
当线程没有命中,进入加载流程时,往ConcurrentHashMap loadingMap中,能putIfAbsent成功即成为Master,失败则为Slave。
loadingMap中记录着,某个缓存key,正在进行数据加载中。

Slave如何等待Master加载数据:
Slave通过wait等待Master的信号,Master完成数据加载后,把数据放入LoadingWrapper中,notifyAll通过所有Slave去LoadingWrapper中取。
其中还需要包括一些必要的超时检测、异常机制,确保Master异常等,不会引起Slave一直等待,保证业务功能不受影响。

具体实现流程:

EdsCache使用样例
按照其他样例正常使用缓存框架即可,该功能是默认开启的。
如何关闭该功能:
如果业务需求希望每次缓存没有命中时,都穿透到DB获取数据,可以选择关闭该功能。
方法一,单个接口关闭:
通过在注解上指定dataLoadSlaveTimeOut=0即可关闭。
@EdsCache(key = constants.USER_CACHE_PREFIX, expire = constants.EXPIRE_SECOND, dataLoadSlaveTimeOut=0)
public UserDTO getUser(@EdsCacheKey Long id){
    return userDao.getUser(id);
}
方法二,全局关闭:
可以通过配置文件,enableDataLoadWait设置为false,全局关闭该功能。
eds:
  cache: 
    namespace: cache_demo4
    enableDataLoadWait: false

分布式锁
如果需要分布式服务之间也避免重复数据加载,可以使用“分布式锁”功能。
当每个进程内的Master要执行数据加载时,会尝试抢分布式锁,
抢锁成功,才能进行真正的数据加载,并回写缓存;
抢锁失败,进入等待,定时(间隔很小)轮询分布式缓存,尝试从缓存中获取数据。
结合一定的超时检测、占锁超时处理等机制,具体实现流程如下:


分布式锁使用样例
@EdsCache(key = constants.USER_CACHE_PREFIX, expire = constants.EXPIRE_SECOND, lockExpire=3)
public UserDTO getUser(@EdsCacheKey Long id){
    return userDao.getUser(id);
}
通过设置lockExpire大于0,开启分布式锁功能,进一步缓解缓存雪崩问题。

本文来自网易实践者社区,经作者陈婷授权发布。  

相关阅读:EdsCache通用缓存框架——(1)总览导航