Redis 缓存穿透、缓存击穿、缓存雪崩 等经典问题解读

由于基本看完了 《Redis 设计与实现》中的单机部分内容,所以就可以看一些面试常常会问到的相关问题,带着问题去学习,这样效率会更高。

缓存穿透

简介

缓存穿透(缓存击穿) 表示恶意用户请求很多不存在的数据,由于数据库中都没有,缓存中肯定也没有,导致这些请求短时间内直接落在了数据库上,导致数据库异常。

解决方案

1:缓存空值

    之所以发生穿透,就是因为缓存中没有存储这些空数据的 key。从而导致每次查询都到数据库去了。那么我们就可以为这些 key 对于的值设置为 null 丢到缓存里面去。后面再查询这个 key  的请求的时候,直接返回 null。这样就不用到数据库中去走一圈了,但是别忘了设置过期时间。

2:布隆过滤器

    BloomFilter 类似于一个 hash set , 用来判断某个元素 (Key) 是否存在于某个集合中,这种方案可以加在第一种方案中,在缓存之前在加一层 BloomFilter, 在查询的时候先去 BloomFilter 去查询 Key 是否存在,如果不存在就直接返回,存在再走 查缓存--->查 DB 的流程。

方案选择

特点:Key 比较多,请求重复率低:

    针对一些恶意攻击,攻击带过来的大量 Key 是不存在的,那么我们采用第一种方案就会缓存大量不存在Key 的数据。所以采用第二种方案;

特点:空数据的 Key 有限,重复率比较高:

    可采用第一种方案;

缓存击穿

简介

在高并发的系统中,大量的请求同时查询一个 Key 时,此时这个 Key 正好失效了,就会导致大量的请求都打到数据库上面去。这种现象我们成为缓存击穿。这将导致某一时刻数据库请求量过大,压力剧增。

解决方案

    上面的现象是多个线程同时去查询数据库的这条数据,那么我们可以在第一个查询数据的请求上使用一个互斥锁来锁住它。其它线程走到这一步拿不到锁就等着,等第一个线程查询到了数据,然后做缓存。后面的进程进来发现已经有缓存了,就直接走缓存。

func get(Key string) string {
    value := redis.get(Key)
    if(value == null) {  // 如果缓存没命中
        // 设置 3min 超时,防止 del 失败,导致后续无法从 DB 中 load 数据
        if(redis.setnx(key_mutex, 1, 3*60) == 1) {  // 如果不存在则设置,单线程操作, 可以充当互斥锁
            value = db.get(Key)    // 从 DB 中取出对于数据
            redis.set(key, value, expire_secs)  // 缓存下来
            redis.del(key_mutex)  // 删除
        } else {  // 其它线程进入 sleep
            sleep(50)
            get(Key)
        }
    } else {
        return value
    }
}

缓存雪崩

简介

缓存雪崩的情况是说,当某一时刻发生大规模的缓存失效的情况,比如你的缓存服务宕机了,大量键过期(失效),接下来的一大波请求瞬间都落在了数据库中导致链接异常。

解决方案

1:加锁

  与缓存击穿解决方式一样,采用加锁的方式来解决;

2:建立备份缓存

    缓存A和缓存B,  A设置超时时间, B不设置超时时间,先从 A读缓存,A没有读B,并且更新 A缓存和B缓存;

3:散开缓存失效时间

    我们可以在原因的失效时间基础上增加一个随机值,比如 1-5 分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效事件。

4:使用 Hystrix 进行限流 & 降级,比如一秒来了 5000 个请求,我们可以设置假设只能有一秒 2000 个请求能通过这个组件,那么其它剩余的 3000 个请求就会走限流逻辑。然后去调用我们自己

      开发的降级组件(降级),比如设置的一些默认值之类的。以此来保证我们的 DB 不会被大量的请求打死。

双写一致性问题

简介

在引入缓存系统的项目中,当我们需要对旧数据进行更新操作时,常常会发生缓存中的数据和数据库中的数据不一致的问题,我们通常采取的策略有以下几种:

1: 先更新数据库,再更新缓存

2: 先更新缓存,再更新数据库

3: 先删除缓存,再更新数据库

解决方案

方案一:这种方案,在大多数场景种不合适,主要原因有:

    资源浪费:我们引入缓存主要是对热点数据进行缓存,这时候如果很多用户对于冷数据进行更新,那么我们就没必要去更新缓存,这会导致缓存资源的大量浪费

    脏数据:请求 A 更新了数据库;请求 B 更新了数据库;请求 B 更新了缓存;请求 A 更新了缓存,这种情况会出现 A 数据覆盖 B 数据的情况,就会产生脏数据

方案二:这种策略比较多平台在使用,如:Facebook, 但这种策略也存在一些问题,如:

    脏数据:造成脏数据的原因主要是由并发引起

方案三:这种策略也有比较多平台在使用,和方案二相同,也会产生脏数据

注:可引入消息系统来避免脏数据(未研究过消息系统,暂时不做分析) 

并发竞争问题

简介

Redis 的并发竞争问题,主要是发生在并发写操作,比如现在想把 price 的值进行 +10 操作,两个连接同时对 price 进行写操作,最终结果应该是 30 才正确:

T1: 连接1 将 price 读出, 目标设置的数据为 10 + 10 = 20

T2: 连接2 也将数据读出,也是为 10, 目标设置为 20

T3: 连接1 将 price 设置为 20

T4: 连接2 也将 price 设置为 20,则最终结果是一个错误的 20

解决方案

方案一:可以采用独占锁的方式,类似于操作系统 mutex 机制,不过成本较高

方案二:可以采用乐观锁的方式,成本低,非阻塞,性能高;Redis 提供了 watch 命令,它本质上就是一个乐观锁,实现伪代码:

// redis 伪代码
watch price
value = redis.get(price)
value = value + 10
multi
set(price, value)
exec

注:上述操作只有一个能成功,其它都会失败,如果期望有多个成功,则可以把命令入队,然后用一个消费者线程从队头依次取出请求,并做相应操作。

参考资料:

https://juejin.im/post/5c9a67ac6fb9a070cb24bf34

原文地址:https://www.cnblogs.com/zpcoding/p/12461961.html