CACHE RELOAD机制设计和实现(防止CACHE失效引发雪崩)
CACHE RELOAD机制设计和实现(防止CACHE失效引发雪崩)
故障分析:
当前逛正在做推广,流量突然暴增,QPS达到5000+,当首页部分缓存失效时,需要查询DB,但由于这部分业务逻辑很复杂导致这SQL包含多表join,groupby,orderby等,执行需要1S,产生的大量临时表,内存中都装不下,变成在磁盘上的临时表,但当时放临时表的磁盘分区容量只有20G,很快磁盘也爆了,结果显然网站打不开了。
总结为几点:
- SQL语句优化不足
- MYSQL
tmp_table_size
配置太小 - 磁盘分区不合理/ TMPDIR路径配置不合理
- 部门间沟通不足,大型推广前没事先打招呼。
临时解决措施:
由于当时持续大量用户访问,查询数据库一直挂住,导致缓存一直无法一套回去,首页那缓存一直处于未命中状态,恶性循环,雪崩了。
当时我们立马采取以下措施:
- 调整MYSQL
tmp_table_size
,关于tmp_table_size
请看下面详细描述。 - 修改MYSQL临时表保存路径(TMPDIR)到较大分区
- 简化业务逻辑,修改SQL,重新部署。
临时表使用内存(tmp_table_size):当我们进行一些特殊操作如需要使用临时表才能完成的join,Order By,Group By 等等
,MySQL 可能需要使用到临时表。当我们的临时表较小(小于 tmp_table_size 参数所设置的大小)的时候,MySQL 会将临时
表创建成内存临时表,只有当 tmp_table_size 所设置的大小无法装下整个临时表的时候,MySQL 才会将该表创建成 MyISAM
存储引擎的表存放在磁盘上。不过,当另一个系统参数 max_heap_table_size 的大小还小于 tmp_table_size 的时候,MySQL
将使用 max_heap_table_size 参数所设置大小作为最大的内存临时表大小,而忽略 tmp_table_size 所设置的值。
而且 tmp_table_size 参数从 MySQL 5.1.2 才开始有,之前一直使用 max_heap_table_size.
长期解决措施:终于到本文的重点Cache Reload机制设计和实现
在讲Cache Cache Reload机制设计和实现之前,先看看缓存更新方式:
- 是缓存超时,让缓存失效,重查。(被动更新)
- 是由后端通知更新,一量后端发生变化,通知前端更新。(主动更新)
前者适合实时性不高,但更新频繁的;后者适合实时性要求高,更新不太频繁的应用。
缓存重新加载机制设计:
根据逛当时业务需求,选择被动更新方式,但这种方式的弊端是当缓存失效那个点,刚好遇上高并发的话,就会发生上述的雪崩情况。
所以我在想这种使用率高的缓存,就不用设置超时或超时设置足够大,然后按业务需求时间间隔定期从DB重新加载/刷新缓存数据,这缓存就不会出现失效情况,也不出现雪崩现象。
下图是guang.com关于Cache Reload的一小部分架构:
主要2个步骤:
- 将有需要重新加载缓存的包装器保存到redis
Hash
。 - 部署在Daemon server上的
CacheReloadJob
,每分钟去redis拿需要reload的缓存的hashmap,判断是否到时间刷新缓存,如果到,通过反射调用相关方法重新重载数据和重置这个缓存。
Cache Reload机制实现:
必要时使用重新加载机制设置memcached:
/**
* <h2>set cache with reload mechanism </h2>
* <h3>Example:</h3>
* <p>
* MethodInvocationWrapper wrapper = new MethodInvocationWrapper();<br>
* wrapper.setMethodName("getProductList");<br>
* wrapper.setObjectName("productService");<br>
* wrapper.setArgs(new Object[] { null,0,1 });<br>
* wrapper.setParameterTypes(new Class[] { Product.class,int.class,int.class});
* </p>
*
* <h3>NOTE:</h3>
* Make sure the Args have been Serializable and the service has been marked the name, like "@Service("productService")"
*
* @param key
* @param expiredTime 过期时间,如果reloadable=true, 此时间建议为 24*60*60 一天.
* @param value
* @param reloadable 是否reload
* @param durationTime reload 时间间距,单位 ms
* @param wrapper
* @return
* @author Kenny Qi
*/
public boolean set(String key, int expiredTime, Object value,boolean reloadable, long durationTime, MethodInvocationWrapper wrapper) {
if(reloadable){
wrapper.setWriteTime(System.currentTimeMillis());
wrapper.setDuration(durationTime);
wrapper.setKey(key);
wrapper.setExpiredTime(expiredTime);
objectHashOperations.put(RedisKeyEnum.CACHE_RELOAD.getKey(), key, wrapper);
}
if(value==null) return false;
try {
return memcachedClient.set(key, expiredTime, value);
} catch (Exception e) {
logger.warn(e.getMessage(), e);
return false;
}
}
CacheReloadJob:
public class CacheReloadJob {
private static Logger logger = LoggerFactory.getLogger(CacheReloadJob.class);
@Autowired
MyXMemcachedClient myXMemcachedClient;
@Resource(name="objectHashOperations")
private HashOperations<String, String, MethodInvocationWrapper> objectHashOperations;
public void reloadCache(){
logger.info("Try to reload cache");
Map<String, MethodInvocationWrapper> map = objectHashOperations.entries(RedisKeyEnum.CACHE_RELOAD.getKey());
ThreadFactory tf = new NamedThreadFactory("CACHE_RELOAD_THREADPOOL");
ExecutorService threadPool = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors(), tf);
for (String key: map.keySet()) {
final MethodInvocationWrapper wrapper = map.get(key);
if(wrapper.getWriteTime()+wrapper.getDuration()>System.currentTimeMillis()){//刷新时间大于当前时间
threadPool.execute(new Runnable() {
@Override
public void run() {
refreshCache(wrapper);
}
});
}
}
logger.info("completed with reloaded cache");
}
private void refreshCache(MethodInvocationWrapper wrapper){
Object object = ReflectionUtils.invokeMethod(SpringContextHolder.getBean(wrapper.getObjectName()), wrapper.getMethodName(), wrapper.getParameterTypes(), wrapper.getArgs());
myXMemcachedClient.set(wrapper.getKey(), wrapper.getExpiredTime(), object);
wrapper.setWriteTime(System.currentTimeMillis());
objectHashOperations.put(RedisKeyEnum.CACHE_RELOAD.getKey(), wrapper.getKey(), wrapper);
}
}
Redis存储结构(更多Redis的应用场景,请看Redis在电商中的实际应用场景):
redis> HSET cache:reload:memcached <memcache_key> <MethodInvocationWrapper>
OK
redis> HGETALL cache:reload:memcached