• 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

  • 相关阅读:
    【游学】Our trip in Baidu developer conference~
    【招新】Bit Workshop ,requiring new ~Welcome ~
    【web】Modify some style of Diandian blog
    【随谈】The little words is essence~
    【电脑】Enable administrator account in win 7
    【web】What's the hell of diandian ?Why can't recognize the videos ?
    【游学】Fortunately ,photographed with the COO of dolphin browser ,Mr.Wang,and the general mangager of Demo coffee Mr.Yan
    【电脑】Modify the default system guide in win 7
    sql中while遍历更新字段数据
    mysql报ERROR [42000]
  • 原文地址:https://www.cnblogs.com/zpcoding/p/12461961.html
Copyright © 2020-2023  润新知