缓存雪崩(cache avalanche)主要指当某一时刻发生大规模不同 key 的缓存失效时,会有大量的请求进来直接打到 db 上,进而导致数据库压力增大,若在高并发的情况下,可能瞬间就会导致 database 宕机。
原因
Redis 缓存雪崩主要有如下两个原因:
- Redis 缓存服务器是正常的,但是大量热点数据同时过期,导致大量请求需要查询数据库并写到缓存;
- Redis 缓存服务器故障宕机,缓存系统异常。
解决方法
redis key 同一时间大面积失效
避免大量热点缓存 key 同时失效,在为 redis key 设置有效期时,添加一个随机值(可以是 1-5 分钟),以使缓存失效时间均匀分布。这样,每个缓存的过期时间的重复率就会降低,进而避免 redis key 集体失效事件。
redis.set(key, value, fix_timeout + random);
针对大面积 key 同时过期,还有另一种方法,那就是热点数据不设置过期时间或设置比较长的失效时间,每个数据记录都有对应的缓存是否过期标记,如果已经过期,会触发另一个线程在后台更新实际 key 的缓存。
实际操作中,缓存数据其过期时间是缓存标记的几倍,如两倍。例如,标记缓存时间为 30 分钟,数据缓存设置为 60 分钟。这样,当缓存标记键过期时,实际缓存可以将旧数据返回给调用者,直到另一个线程完成后台更新后才会返回新缓存。
public Object mockGetDataFromCache(long itemId) {
int cacheTime = 30;
String cacheKey = "p" + itemId;
// cache flag
String cacheFlag = cacheKey + "_f";
String flag = redis.get(cacheFlag);
// get cache value
String cacheValue = redis.get(cacheKey);
if (flag != null) {
// Not expired, return directly
return cacheValue;
} else {
redis.setex(cacheFlag, cacheTime, "1");
if (cacheValue != null) {
ThreadPool.execute((arg) -> {
// This is generally SQL query data
String newData = getFromDB();
// The time is set to twice the cache time for dirty reading
redis.setex(cacheKey, cacheTime * 2, newData);
});
return cacheValue;
}
String newData = getFromDB();
// The time is set to twice the cache time for dirty reading
redis.setex(cacheKey, cacheTime * 2, newData);
return newData;
}
}
如果对系统的性能及吞吐量没有较高的要求,也可以通过队列锁定来减轻数据库的压力。
Redis 宕机
redis 挂了,可以考虑提前实现 redis 的集群,使用集群缓存,保证缓存服务的高可用。
redis 集群方式有很多种,如 twemproxy 代理方式、codis 架构、redis cluster、Redis Sentinel(哨兵模式)等等,更详细的信息参考 redis 集群方式全面详解。
此外,要开启 Redis 持久化机制,尽快恢复缓存集群,一旦重启,就能从磁盘上自动加载数据恢复内存中的数据。
其他
除了远程缓存如 redis、mongo 之类,也可以考虑结合本地缓存一起搭配使用,可以形成多层次的缓存机制,有效保证系统的稳定性,本地缓存有 guava 的 cache、ehcache 框架等。