Redis 缓存雪崩、击穿、穿透

251 阅读8分钟

缓存雪崩

当大量的缓存在同一时间过期(失效)或redis宕机时,用户的请求无法在redis中处理,全部请求直接访问数据库,导致数据库压力骤增,严重的会造成数据库宕机,导致一系列系统崩溃问题。

雪崩原因
  • 大量数据同一时间过期
  • Redis宕机
策略
  1. 大量数据同一时间过期

    • 均匀的设置过期时间:给数据的过期时间加上一个随机数,避免大部分的数据过期时间都重叠导致大部分数据在同一时间过期
    • 互斥锁:当客户端请求时,发现访问的数据不在redis中,就加一个互斥锁,保证同一时间只有一个请求来构建缓存,构建完成释放锁。未能获取到互斥锁的请求,要么等待锁被释放然后读取缓存,要么直接返回给客户端空值。
      • 这里使用互斥锁是有代价的,最好设置一个锁的过期时间,避免其他因素导致阻塞,锁一直无法释释放影响整个系统的运行
    • 后台更新缓存:业务主线程不再负责更新缓存也不设置过期时间,这样缓存永久有效,更新操作由后台线程定时更新。
      • 在设置缓存的时候不设置过期时间的情况下并不是说缓存永久有效,当内存达到一定阈值的之后,redis会启用内存淘汰策略,就会导致一些缓存会失效。
      • 解决办法:
      • 第一种:后台线程不仅负责定时更新缓存,还需要频繁的检验缓存是否有效,如果失效就有可能是被淘汰了,然后需要将数据写入缓存中
      • 第二种:如果在处理客户端请求的时候发现缓存失效,可以通过消息队列通知后台线程更新缓存。后台线程收到消息通知做一系列处理。
  2. redis宕机

    • 服务熔断或请求限流机制
      • reids宕机导致雪崩的问题,可以使用服务熔断机制,暂停业务应用对缓存的请求访问,直接返回错误,不访问mysql,直到缓存恢复。当处理客户端的请求发现redis已经宕机,这个时候直接返回错误信息给客户端,不访问数据库。这样可以防止mysql过载。
      • 启用限流机制,只将少部分的请求发送到数据库进行处理,过多的请求直接拒绝访问。当处理客户端请求的时候,启用限流机制,是有少部分请求可以请求到数据,这样mysql就不会有过载的现象。
    • 构建Reids高可用的缓存集群
      • 可以通过主从节点的方式构建高可用的缓存集群,保证可靠性。

缓存击穿

缓存中的热点数据过期,导致大量请求直接涌入数据库,大量并发请求到数据,导致数据库过载。

策略
  1. 互斥锁方案:保证同一时间只有一个业务线程更新缓存,未能获取到锁的请求,要么等到锁释放后重新读取缓存,要么返回空值。
    • 当客户端请求到服务端的时候,发现缓存中的数据已经过期,这个时候使用互斥锁,阻塞其他线程的查询操作,然后由获得锁的线程去查询数据然后更新缓存即可。但是要注意锁的释放,而且还需要注意锁的时间限制,灵活操作,不影响业务。也可以就是获取不到锁的时候不阻塞等待,而是直接返回给客户端空值。
  2. 不给热点数据设置过期时间,由后台异步更新缓存信息,或者热数据准备过期前,提前通知后台线程更新缓存以及重新设置过期时间。
    • 不设置过期时间,这个就比较好理解,处理客户端请求的时候不需要进行缓存方面的处理,而是直接由后台线程定时刷新。
    • 如果设置了过期时间,只是多设置了过期时间,然后在写一个守护写程去维护当前这个缓存数据,可以到达一定阈值之后更新缓存然后进行续租。

缓存穿透

当用户访问数据时,数据既不在缓存中,也不在数据库中。导致在处理客户端请求时,缓存数据缺失,再去访问数据库时,数据库中也没有数据,这个时候没有办法构建缓存数据。如果这个时候有大量并发请求,数据库就有可能负载。

原因
  • 业务误操作,缓存和数据中的数据都被误删除了,所以导致缓存和数据中的数据都没了
  • 被黑客恶意攻击,故意大量访问某些读取不存在的业务数据
策略
  1. 非法请求限制

    • 当碰到大量恶意请求不存在的业务数据时,也是会发生缓存穿透的,可以在客户端请求的时,服务端判断请求参数的合法性。如果判断出是恶意请求,可以直接返回错误。
  2. 缓存控制或默认值

    • 可以针对查询的数据,在缓存中设置一个空值或默认值,这样后续请就会读取到这个空值或默认值,然后响应给客户端即可,不会在进行数据库的访问
  3. 使用布隆过滤器

    • 在数据写入数据库的时,使用布隆过滤器做标记。当当客户端请求的时,查询缓存失效之后,可以先查询布隆过滤器判断数据是否存在mysql数据库中,如果不存在就不需要在查询数据库,减轻数据库压力。
布隆过滤器
  • 底层为一个位图数组,初始值都是0的位图数组,多个哈希函数。

  • 标记过程

    • 使用N个哈希函数,分别对数据做哈希计算,得到N个哈希值
    • 将计算出来的N个哈希值对位图的长度取模,得到每个哈希值在位图的对应位
    • 将每个哈希值在位图数组的对应因为设置成为1
  • 插入流程

# 假设当前位图的长度为10
# 初始状态:[0 0 0 0 0 0 0 0 0 0]
# 插入一个值:hyggebest
# 经过哈希函数计算然后取模,得到在位图的位置
# hash1("hyggebest") % 10  --> 2
# hash2("hyggebest") % 10  --> 6
# hash3("hyggebest") % 10  --> 9
# 位图数组为:[0 0 1 0 0 0 1 0 0 1]
  • 查询流程
    1. 使用多个哈希函数对查询元素进行计算,获取到位图的位置索引
    2. 查询位图中对应刚刚计算出来的位图所谓位置
      • 如果 所有位置的bit位全部都是1,则数据可能存在
      • 如果 任意位置的bit位为0 则该元素一定不存在
# 查询 hyggebest
# 计算:
# hash1("hyggebest") % 10  --> 2
# hash2("hyggebest") % 10  --> 6
# hash3("hyggebest") % 10  --> 9

# 如果发现bit位的2、6、9都是1,说明这个数据有可能存在
# 但是如果bit位的2、6、9有一个为0,说明数据一定不存在

误判(假阳、假阴)

  • 假阳
    • 布隆过滤器可能会错误的认为某个元素在集合中,但是实际并不在。这是因为多个不同的元素可能会映射到相同的位图位置,导致误判
  • 假阴
    • 布隆过滤器是不会发生假阴这种情况的,即使布隆过滤器说元素不在集合中, 它一定不在集合中。布隆过滤器不会漏判,只会误判。
# 假设当前位图的长度为10
# 初始状态:[0 0 0 0 0 0 0 0 0 0]
# 插入一个值:hyggebest
# 经过哈希函数计算然后取模,得到在位图的位置
# hash1("hyggebest") % 10  --> 2
# hash2("hyggebest") % 10  --> 6
# hash3("hyggebest") % 10  --> 9
# 位图数组为:[0 0 1 0 0 0 1 0 0 1]

# 插入第二个值:madhatter
# hash1("madhatter") % 10  --> 1
# hash2("madhatter") % 10  --> 7
# hash3("madhatter") % 10  --> 2
# 位图数组为:[0 1 1 0 0 0 1 1 0 1]

# 最终的位图数据为:[0 1 1 0 0 0 1 1 0 1]

# 此时,查询一个元素为:caps
# hash1("caps") % 10  --> 1
# hash2("caps") % 10  --> 2
# hash3("caps") % 10  --> 6

# 这个时候进行找到对应位图的位置发现位于bit位的1、2、6的值都是1,就会判断查询的元素caps可能存在
# 但其实并没有插入caps这个元素
  • 因为位图的长度就这么长,在进行哈希计算的时候是有可能将[不同]的元素映射到[相同]的bit位上,就会出现误判、假阳的情况。
  • 位图越小,哈希碰撞的可能性就越大,误判、假阳的可能性就越大。