Junhc

岂止于博客

缓存穿透、雪崩、击穿

缓存穿透

一般是黑客故意请求缓存中不存在的数据,导致所有的请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉。

解决方案:
有很多种方法可以有效地解决缓存穿透问题,最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的BitMap中,一个一定不存在的数据会被 这个BitMap拦截掉,从而避免了对底层存储系统的查询压力。另外也有一个更为简单粗暴的方法(我们采用的就是这种),如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。

缓存雪崩

缓存同一时间大面积失效,所以后面的请求都会落到数据库上,造成数据库短时间内承受大面积请求而崩掉。

解决方案:

  • 事前:尽量保证整个Redis集群的高可用性,发现机器宕机尽快补上、选择合适的内存淘汰策略。
  • 事中:本地缓存 + hystix限流&降级,避免MySQL崩掉。
  • 事后:利用Redis之就好机制保存的数据尽快恢复缓存。
缓存击穿

对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题,这个和缓存雪崩的区别在于这里针对某一key缓存,前者则是很多key。

缓存在某个时间点过期的时候,恰好在这个时间点对这个key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。

解决方案:

  • 使用互斥锁:在缓存失效的时候判断这个值是否为空,而不是立即读取DB,先set一个mutex key,当操作成功时,再进行读取DB的操作,并回写缓存,否则就重试整个get缓存方法。
  • “提前”使用互斥锁:在value内部设置一个超时值timeout1,timeout1比实际的timeout小。当从cache读取到timeout1发现它已经过期,马上延长timeout1并重新设置到cache,然后再从数据库加载数据并设置到cache中。
v = memcache.get(key);  
if (v == null) {  
    if (memcache.add(key_mutex, 3 * 60 * 1000) == true) {  
        value = db.get(key);  
        memcache.set(key, value);  
        memcache.delete(key_mutex);  
    } else {  
        sleep(50);  
        retry();  
    }  
} else {  
    if (v.timeout <= now()) {  
        if (memcache.add(key_mutex, 3 * 60 * 1000) == true) {  
            // extend the timeout for other threads  
            v.timeout += 3 * 60 * 1000;  
            memcache.set(key, v, KEY_TIMEOUT * 2);  

            // load the latest value from db  
            v = db.get(key);  
            v.timeout = KEY_TIMEOUT;  
            memcache.set(key, value, KEY_TIMEOUT * 2);  
            memcache.delete(key_mutex);  
        } else {  
            sleep(50);  
            retry();  
        }  
    }  
}
  • 永不过期
String get(final String key) {  
    V v = redis.get(key);  
    String value = v.getValue();  
    long timeout = v.getTimeout();  
    // 对服务器时间同步有要求
    if (v.timeout <= System.currentTimeMillis()) {  
        // 异步更新后台异常执行  
        threadPool.execute(new Runnable() {  
            public void run() {  
                String keyMutex = "mutex:" + key;  
                if (redis.setnx(keyMutex, "1")) {  
                    // 3 min timeout to avoid mutex holder crash  
                    redis.expire(keyMutex, 3 * 60);  
                    String dbValue = db.get(key);  
                    redis.set(key, dbValue);  
                    redis.delete(keyMutex);  
                }  
            }  
        });  
    }  
    return value;  
}
  • 资源保护:使用hystrix限流&降级