redis缓存穿透、击穿、雪崩等

[TOC]

什么是缓存穿透

正常情况下,查询的数据都存在,如果请求一个不存在的数据,也就是缓存和数据库都查不到这个数据,每次都会去数据库查询,这种查询不存在数据的现象我们称为缓存穿透

穿透带来的问题

如果每次都拿一个不存在的id去查询数据库,可能会导致你的数据库压力增大

解决办法

  1. 缓存空值
    之所以发生穿透,是因为缓存中没有存储这些数据的key,从而每次都查询数据库
    我们可以为这些key在缓存中设置对应的值为null,后面查询这个key的时候就不用查询数据库了
    当然为了健壮性,我们要对这些key设置过期时间,以防止真的有数据
  2. BloomFilter
    BloomFilter 类似于一个hbase set 用来判断某个元素(key)是否存在于某个集合中
    我们把有数据的key都放到BloomFilter中,每次查询的时候都先去BloomFilter判断,如果没有就直接返回null
    注意BloomFilter没有删除操作,对于删除的key,查询就会经过BloomFilter然后查询缓存再查询数据库,所以BloomFilter可以结合缓存空值用,对于删除的key,可以在缓存中缓存null

缓存击穿

什么是缓存击穿

在高并发的情况下,大量的请求同时查询同一个key时,此时这个key正好失效了,就会导致同一时间,这些请求都会去查询数据库,这样的现象我们称为缓存击穿

击穿带来的问题

会造成某一时刻数据库请求量过大

解决办法

1.使用互斥锁(mutex key)

业界比较常用的做法,是使用mutex。简单地来说,就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db,而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一个mutex key,当操作返回成功时,再进行load db的操作并回设缓存;否则,就重试整个get缓存的方法。

SETNX,是「SET if Not eXists」的缩写,也就是只有不存在的时候才设置,可以利用它来实现锁的效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public String get(key) {
String value = redis.get(key);
if (value == null) {
//代表缓存值过期
//设置3min的超时,防止del操作失败的时候,下次缓存过期一直不能load db
if (redis.setnx(key_mutex, 1, 3 * 60) == 1) {
//代表设置成功
value = db.get(key);
redis.set(key, value, expire_secs);
redis.del(key_mutex);
} else {
//这个时候代表同时候的其他线程已经load db并回设到缓存了,这时候重试获取缓存值即可
sleep(50);
get(key);
//重试
}
} else {
return value;
}
}

2.”永远不过期”**

这里的“永远不过期”包含两层意思:

(1) 从redis上看,确实没有设置过期时间,这就保证了,不会出现热点key过期问题,也就是“物理”不过期。

(2) 从功能上看,如果不过期,那不就成静态的了吗?所以我们把过期时间存在key对应的value里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建,也就是“逻辑”过期

从实战看,这种方法对于性能非常友好,唯一不足的就是构建缓存时候,其余线程(非构建缓存的线程)可能访问的是老数据,但是对于一般的互联网功能来说这个还是可以忍受。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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;
}

缓存雪崩

什么是缓存雪崩

当某一时刻发生大规模的缓存失效的情况,比如你的缓存服务宕机了

解决办法

  1. 如果Redis是集群部署,将热点数据均匀分布在不同的Redis库中也能避免全部失效
  2. ehcache本地缓存 + Hystrix限流&降级
    ehcache 本地缓存的目的也是考虑在 Redis Cluster 完全不可用的时候,ehcache 本地缓存还能够支撑一阵
    使用 Hystrix进行限流 & 降级 ,比如一秒来了5000个请求,我们可以设置假设只能有一秒 2000个请求能通过这个组件,那么其他剩余的 3000 请求就会走限流逻辑
  3. 在批量往Redis存数据的时候,把每个Key的失效时间都加个随机值就好了
  4. 热点数据永不过期,参考上文