Redis的穿透、击穿和雪崩

前言

不论是我们在使用Redis还是准备面试,都逃不掉一块我们必须要考虑到的内容,也是使用Redis不精细的话必定会遇到的问题,就是缓存穿透、击穿和雪崩,这三个问题严重情况下,会使服务无法继续正常使用。从名字上来看,好像雪崩最严重,眼前突然浮现出电影《攀登者》里面的画面=。=

缓存穿透

缓存穿透是指访问一个DB和Redis中必定不存在的key,如果不对这类请求进行过滤拦截的话,请求每次都会穿过Redis直接打到DB上,并且我们一般是缓存中没数据的时候去DB中取,取出来之后再放到缓存中,但这类请求所需要的数据在DB中也不存在,所以即使请求打到DB上,最终缓存中还是没有数据,在这类请求特别多的情况下,DB很快就会被拖垮,引起服务异常。

解决方案

  1. 布隆过滤器:我们可以在做事务型处理之后,将需要缓存的key放到布隆过滤器中,但是由于布隆过滤器只能保证可能存在,所以在使用过程中还是会有穿透的可能性存在,但概率极小,所以不用过多担心
  2. 短期null:此方案是在DB中查询不到数据的时候,就往Redis中设置一个短时间内就会过期的null值,比如30秒,1分钟等,不过时间还是要根据自己的业务性质来定。为什么要给不存在的key在缓存中设置一个null值?其实不一定是null,只要团队约定一个特殊字符即可,因为我们到数据库里取不出来数据,缓存里取个null(或者nil),也就代表了这个key不存在与数据库中

缓存雪崩

大量的key在同一时刻同时失效,这些key并不一定是设置了相同的时间,也可能是凑巧时间累计在一起了,恰巧大量的针对这些失效的key的请求在同一时间大量的打了进来,这时缓存全部未命中,所有的请求都透传到DB上,引起DB压力瞬间扩大数倍,极易导致DB因负载过高而崩溃,危害极大。

解决方案

  1. 加锁:对访问的key进行加锁,同时只放一个请求透传到DB,从一定程度上缓解了DB的压力,这也是缓存击穿的一种解决方案
  2. 队列:所有的请求全部塞入到队列中,依次打到DB上,这种方式能解决DB的压力,但是会给请求处理效率带来一些延迟
  3. 随机过期时间:给key设置过期时间时,在原过期时间的基础上加一个随机时间,比如3000毫秒以内的随机数,这样过期时间重复或者累计重复的可能性降低了很多,不太容易引起大量的key同时失效,并且成本较低。

缓存击穿

缓存雪崩是说的大量的key,缓存击穿说的是某一个热点key,也就是在某些时间点会被超高的并发访问。key在某个时间点过期的时候,恰好在对这个Key有大量的并发请求过来,缓存中无法命中则会把请求全部打到DB端,如此大量的请求可能会瞬间把DB压爆。

解决方案

  1. 临时加锁:对key加上互斥锁,若缓存中命中不了的时候,先给这个key设置一个锁(SETNX),锁的过期时间要非常简短,只有加锁成功的线程才透传到DB,加载完数据后set到缓存中,并释放锁

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    function get(key):
    var v = redis.get(key)
    if v eq null:
    if setnx(key_lock, val, timeout):
    var v = db.get(key)
    redis.set(key, v, timeout)
    release(key_lock)
    return v
    else:
    sleep(30)
    return get(key)
    else:
    return v;
  2. 同步锁:对于热点key,set缓存的时候同时set一个针对这个key的监视key,监视key的过期时间一定要小于被监视的key,每次获取缓存数据的时候都获取一下这个监视key,并判断监视key是否过期了,如果过期了,则重置一下key的过期时间,并重新设置这个监视key

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    function get(key):
    var v = redis.get(key)
    if v eq null:
    if setnx(key_lock, val, timeout):
    var v = db.get(key)
    redis.set(key, v, timeout)
    redis.set(key_monitor, now() + timeout - time, timeout - time)
    release(key_lock)
    return v
    else:
    sleep(30)
    return get(key)
    else:
    var vm = redis.get(key_monitor)
    if now() - vm lt 10:
    redis.expire(key ,timeout)
    reids.expire(key_monitor, now() + timeout - time)
    else:
    return v

    总结

    若是想更好的使用Redis,那么穿透、击穿、雪崩必定是要慎重考虑的东西,解决方案有多种,应根据自己的实际业务做更优选择