0%

Redis缓存穿透-击穿-雪崩-热点key-竞争key问题

既往不恋,当下不杂,未来不迎,人生需学会放下。 —— 曾国潘

一、缓存穿透

  1. 概念

        缓存穿透是指查询一个数据库一定不存在的数据,从而导致未缓存相应的数据,最终每次请求都会访问数据库。

  1. 解决方案
    • ①用一个bitmap和n个hash函数做布隆过滤器过滤没有在缓存的键,即布隆过滤器
    • ②持久层查询不到就缓存空结果,有效时间设置较短从而保证有数据时及时更新
      • 具体是通过某个特定值key去查询数据,如果数据库中不存在则缓存肯定不存在。
      • 此时在缓存中设置此特定值key,将其对应的value设置为一个默认的值(如“NULL”),并设置一个失效时间。
      • 在当前缓存失效之前,所有通过此key的访问都被缓存挡住了。
      • 如果此key对应的数据存在时,在缓存失效之后,通过此key再去访问数据库从而设置新的value。
    • ③接口添加校验,比如用户鉴权,参数合法校验(如id<0的直接拦截等)。

二、缓存击穿

  1. 概念
          缓存击穿是指当缓存中某个热点数据过期了,在该热点数据重新载入缓存之前,有大量的查询请求穿过缓存直接查询数据库,进而导致数据库压力骤增,造成大量请求阻塞,甚至直接导致数据库挂掉。

  2. 解决方案

    • ①热点数据不设置过期时间
    • ②对请求加锁,保证只有一个请求获取到锁,并将查询的结果放入缓存

三、缓存雪崩

  1. 概念

        缓存雪崩由同一时间热点缓存大面积失效,(例如:设置缓存时采用了相同的过期时间,在同一时刻出现大面积的缓存过期),所有原本应该访问缓存的请求都去查询数据库了,而对数据库CPU和内存造成巨大压力,严重的会造成数据库宕机,从而形成一系列连锁反应,造成整个系统崩溃。

  1. 解决方案

    • ①将系统中key的缓存失效时间均匀地错开,防止统一时间点有大量的key对应的缓存失效
      • 根据业务特性设置热点数据永不过期,有更新操作的时候更新下缓存,如电商首页
    • ②优化缓存使用方式,当通过key去查询数据时,首先查询缓存,如果此时缓存中查询不到,就通过分布式锁进行加锁,取得锁的进程查DB并设置缓存,然后解锁;其他进程如果发现有锁就等待,然后等解锁后返回缓存数据或者再次查询DB
    • ③通过队列实现串行化访问

四、热点key

  1. 概念

        热点key是指一个key非常热点(明星八卦新闻或商品大促销),大并发集中对这一个节点进行访问,当key失效的时候有大量线程来构建缓存,导致负载增加进而系统崩溃。

  1. 产生原因

    • ①用户消费的数据远大于生产的数据,如热卖商品、热点新闻等
    • ②请求分片集中,超过单 Server 的性能极限
  2. 解决方案

    • ①客户端热点key缓存:将热点key对应value并缓存在客户端本地,并且设置一个失效时间。每次读请求首先检查key是否存在于本地缓存中,如果存在则直接返回,如果不存在再去访问分布式缓存的机器。
    • ②将热点key分散为多个子key,然后存储到缓存集群的不同机器上,这些子key对应的value都和热点key是一样的。当通过热点key去查询数据时,通过某种hash算法随机选择一个子key,然后再去访问缓存机器,将热点分散到了多个子key上。
    • ③设置永不过期,包含两层意思:
      • redis确实没有设置过期时间,保证不会出现热点key过期问题,也就是“物理”不过期。
      • 从功能上把过期时间存在key对应的value里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建,也就是“逻辑”过期,为每个 value 设置一个逻辑过期时间,当发现超过逻辑过期时间后,会使用单独的线程去构建缓存。
    • ④使用互斥锁setnx:在缓存失效的时候(判断拿出来的值为空),不是立即去load db,而是先使用setnx去set一个mutex key,当操作成功时再进行load db的操作并设置缓存,否则就重试整个get缓存的方法。
    • ⑤异步构建缓存:缓存失效的时候另起一个线程重新构建
    • ⑥使用布隆过滤器:

五、竞争key

  1. 概念
          Redis是单线程的NoSQL数据库,本身并没有锁的概念,由于其单线程特性,多个客户端连接并不存在竞争关系,但是当并发量大时因各种原因导致执行顺序不一致从而会出现错误的结果。并发竞争key问题简单理解就是同时有多个客户端去set同一个key。

实例场景一:假设当前库存值为20,现在有2个连接都要减5,最终结果库存值应该是10才对,但存在下面这种情况:

时刻 客户端1 客户端2 说明
T1 127.0.0.1:6379> get stock 127.0.0.1:6379> get stock T1时刻两个客户端读取库存均为20
T2 127.0.0.1:6379> set stock 15 - T2时刻客户端1更新库存为15
T3 - 127.0.0.1:6379> set stock 15 T3时刻客户端2更新库存为15

实例场景二:比如有3个请求有序的修改某个key,按正常顺序的话数据版本应该是1->2->3,最后应该是3。但如果第二个请求由于网络原因延迟了,数据版本就变为了1->3->2,最后值变为2,不符合预期。

时刻 客户端1 客户端2 说明
T1 127.0.0.1:6379> set stock {num:20,timestamp:111} - T1时刻写入库存{num:20,timestamp:111}
T2 127.0.0.1:6379> get stock 127.0.0.1:6379> get stock T2时刻两个客户端读取库存{num:20,timestamp:111}
T3 127.0.0.1:6379> set stock {num:15,timestamp:1558077007} - T2时刻,假设满足条件客户端1更新库存为15,时间戳为1558077007
T4 - T4时刻,客户端2的时间戳仍为111,早于最新的1558077007,则放弃更新
  1. 解决方案

    • 基于watch命令(乐观锁),适用于单机实例,分片集群无法实现。
    • 基于时间戳,即set key value时将当前时间戳写入,并发set时先判断时间戳,适用于有序需求场景。
    • 基于分布式锁,每次写操作都要去申请锁,拿到锁以后才能进行操作。
    • 基于消息队列,将操作入队列实现串行化处理。
  2. 参考