07-Redis 穿透,击穿,雪崩,倾斜,淘汰,删除

image-20200815230439632

1. 缓存穿透

  • 概念 查不到,缓存层+持久层都压力增大

当用户去查询数据的时候,发现 redis 内存数据库中没有,于是向持久层数据库查询,发现也没有,于是查询失败,当用户过多时,缓存都没有查到,于是都去查持久层数据库,这会给持久层数据库造成很大的压力,此时相当于出现了缓存穿透。

img

  • 解决方案 × 2
  1. 布隆过滤器(★):是一种数据结构,将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被这个 bitmap 拦截掉,从而避免了对底层存储系统的查询压力。

img

5TB的硬盘上放满了数据,请写一个算法将这些数据进行排重。如果这些数据是一些32bit大小的数据该如何解决?如果是64bit的呢?
对于空间的利用到达了一种极致,那就是Bitmap和布隆过滤器(Bloom Filter)。

  • Bitmap: 典型的就是哈希表
    缺点是,Bitmap对于每个元素只能记录1bit信息,如果还想完成额外的功能,恐怕只能靠牺牲更多的空间、时间来完成了。
  • 布隆过滤器
    就是引入了k(k>1)k(k>1)个相互独立的哈希函数,保证在给定的空间、误判率下,完成元素判重的过程。
    它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。
    Bloom-Filter算法的核心思想就是利用多个不同的Hash函数来解决“冲突”。
    Hash存在一个冲突(碰撞)的问题,用同一个Hash得到的两个URL的值有可能相同。为了减少冲突,可以多引入几个Hash,如果通过其中的一个Hash值得出某元素不在集合中,那么该元素肯定不在集合中。只有在所有的Hash函数告诉该元素在集合中时,才能确定该元素存在于集合中。这便是Bloom-Filter的基本思想。
    Bloom-Filter一般用于在大数据量的集合中判定某元素是否存在。
  • 布隆过滤器基本使用

  布隆过滤器有二个基本指令,bf.add 添加元素,bf.exists 查询元素是否存在,如果想要一次添加多个,就需要用到 bf.madd 指令。同样如果需要一次查询多个元素是否存在,就需要用到 bf.mexists 指令。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
127.0.0.1:6379> bf.add codehole user1
(integer) 1
127.0.0.1:6379> bf.add codehole user2
(integer) 1
127.0.0.1:6379> bf.add codehole user3
(integer) 1
127.0.0.1:6379> bf.exists codehole user1
(integer) 1
127.0.0.1:6379> bf.exists codehole user2
(integer) 1
127.0.0.1:6379> bf.exists codehole user3
(integer) 1
127.0.0.1:6379> bf.exists codehole user4
(integer) 0
127.0.0.1:6379> bf.madd codehole user4 user5 user6
1) (integer) 1
2) (integer) 1
3) (integer) 1
127.0.0.1:6379> bf.mexists codehole user4 user5 user6 user7
1) (integer) 1
2) (integer) 1
3) (integer) 1
4) (integer) 0
  1. 缓存空对象:当存储层查不到时,即使返回的空对象也将其缓存起来,同时设置一个过期时间,之后再访问这个数据将会从缓存中获取,保护后端数据。

img

但会有两个问题:

  1. 如果空值被缓存起来,就意味着需要更多的空间存储更多的键,会有很多空值的键。

  2. 即使对空值设置了过期时间,还是会存在缓存层和存储层会有一段时间窗口不一致,这对于需要保持一致性的业务会有影响。

2. 缓存击穿

  • 概念 访问量大,缓存过期瞬间并发穿过缓存层,直接访问持久层

指对某一个 key 的频繁访问,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就会直接请求数据库,就像在一个屏障上凿开了一个洞,例如微博由于某个热搜导致宕机。

其实就是:当某个 key 在过期的瞬间,有大量的请求并发访问,这类数据一段是热点数据,由于缓存过期,会同时访问数据库来查询最新数据,并回写缓存,导致数据库瞬间压力过大。

img

  • 解决方案 × 2
  1. 设置热点数据永不过期:从缓存层面上来说,不设置过期时间,就不会出现热点 key 过期后产生的问题。

  2. 添加互斥锁(★):使用分布式锁,保证对每个 key 同时只有一个线程去查询后端服务,其他线程没有获得分布式锁的权限,因此只需要等待即可,这种方式将高并发的压力转移到了分布式锁上,对分布式锁也是一种极大的考验。

3. 缓存雪崩

  • 概念 缓存过期,大量并发导致缓存层+持久层宕机

指在某一个时间段,缓存集中过期失效或 Redis 宕机导致的,例如双十一抢购热门商品,这些商品都会放在缓存中,假设缓存时间为一个小时,一个小时之后,这些商品的缓存都过期了,访问压力瞬间都来到了数据库上,此时数据库会产生周期性的压力波峰,所有的请求都会到达存储层,存储层的调用量暴增,造成存储层挂掉的情况。

img

img

其实比较致命的缓存雪崩,是缓存服务器某个节点宕机或断网,因为自然形成的缓存雪崩,一定是在某个时间段集中创建缓存,此时的数据库还是可以顶住压力的,而缓存服务节点的宕机,对数据库服务器造成的压力是不可预知的,有可能瞬间就把服务器压垮。

  • 解决方案 × 3
  1. 配置 Redis 高可用:其实就是搭建集群环境,有更多的备用机。

    Redis 搭建集群参考:Redis 搭建集群步骤

  2. 限流降级:在缓存失效后,通过加锁或者队列来控制读服务器以及写缓存的线程数量,比如对某个 key 只允许一个线程查询数据和写缓存,其他线程等待。

    降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。
    以参考日志级别设置预案:
    (1)一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级;
    (2)警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警;
    (3)错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级;
    (4)严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级。服务降级的目的,是为了防止Redis服务故障,导致数据库跟着一起发生雪崩问题。因此,对于不重要的缓存数据,可以采取服务降级策略,例如一个比较常见的做法就是,Redis出现问题,不去数据库查询,而是直接返回默认值给用户。

  3. 数据预热:在项目正式部署之前,把可能用的数据预先访问一边,这样可以把一些数据加载到缓存中,在即将发生大并发访问之前手动触发加载缓存中不同的key,设置不同的过期时间,让缓存失效的时间尽量均衡。

    解决思路:
    1、直接写个缓存刷新页面,上线时手工操作下;
    2、数据量不大,可以在项目启动的时候自动进行加载;
    3、定时刷新缓存。

4. 缓存倾斜

  • 概念 某个缓存服务器压力过大而宕机

指某一台 redis 服务器压力过大而导致该服务器宕机。

img

  • 解决方案
  1. 配置 Redis 高可用:其实就是搭建集群环境,有更多的备用机。

    Redis 搭建集群参考:Redis 搭建集群步骤

5. 淘汰机制

在 Redis 内存已经满的时候,添加了一个新的数据,执行淘汰机制。(redis.conf 中配置)

  1. volatile-lru:在内存不足时,Redis 会再设置过了生存时间的key中干掉一个最近最少使用的key。

  2. allkeys-lru:在内存不足时,Redis 会再全部的key中干掉一个最近最少使用的key。

  3. volatile-lfu:在内存不足时,Redis 会再设置过了生存时间的key中干掉一个最近最少频次使用的key。

  4. allkeys-lfu:在内存不足时,Redis 会再全部的key中干掉一个最近最少频次使用的key。

  5. volatile-random:在内存不足时,Redis 会再设置过了生存时间的key中随机干掉一个。

  6. allkeys-random:在内存不足时,Redis 会再全部的key中随机干掉一个。

  7. volatile-ttl:在内存不足时,Redis 会再设置过了生存时间的key中干掉一个剩余生存时间最少的key。

  8. noeviction:(默认)在内存不足时,直接报错。

淘汰方案

  • 指定淘汰机制的方式:maxmemory-policy 具体策略
  • 设置Redis的最大内存:maxmemory 字节大小

redis 内存数据集大小上升到一定大小的时候,就会进行数据淘汰策略。

5.1 如何配置

通过配置 redis.conf 中的 maxmemory 这个值来开启内存淘汰功能

1
# maxmemory

值得注意的是,maxmemory 为 0 的时候表示对 Redis 的内存使用没有限制。

根据应用场景,选择淘汰策略:

1
# maxmemory-policy noeviction

5.2 内存淘汰的过程

首先,客户端发起了需要申请更多内存的命令(如set)。

然后,Redis检查内存使用情况,如果已使用的内存大于maxmemory则开始根据用户配置的不同淘汰策略来淘汰内存(key),从而换取一定的内存。

最后,如果上面都没问题,则这个命令执行成功。

5.3 动态改配置命令

此外,redis 支持动态改配置,无需重启。

设置最大内存

1
config set maxmemory 100000

设置淘汰策略

1
config set maxmemory-policy noeviction

5.4 如何选择淘汰策略

allkeys-lru:如果应用对缓存的访问符合幂律分布,也就是存在相对热点数据,或者不太清楚应用的缓存访问分布状况,可以选择allkeys-lru策略。

allkeys-random:如果应用对于缓存key的访问概率相等,则可以使用这个策略。

volatile-ttl:这种策略使得可以向Redis提示哪些key更适合被eviction。

另外,volatile-lru策略和volatile-random策略适合将一个Redis实例既应用于缓存和又应用于持久化存储的时候,然而也可以通过使用两个Redis实例来达到相同的效果,值得一提的是将key设置过期时间实际上会消耗更多的内存,因此建议使用allkeys-lru策略从而更有效率的使用内存。

6. 生存时间到了删除?

key的生存时间到了,Redis 会立即删除吗?答:不会立即删除

  1. 定期删除:Redis每隔一段时间就去会去查看Redis设置了过期时间的key,会再100ms的间隔中默认查看3个key。

  2. 惰性删除:如果当你去查询一个已经过了生存时间的key时,Redis会先查看当前key的生存时间,是否已经到了,直接删除当前key,并且给用户返回一个空值。

总结:

定期删除:默认100ms查看3个过期的key,定期删除
惰性删除:查询时,redis检查是否过期,过期则删除key,返回空值

  • 采用定期删除+惰性删除就没其他问题了么?

不是的,如果定期删除没删除key。然后你也没即时去请求key,也就是说惰性删除也没生效。这样,redis的内存会越来越高。那么就应该采用内存淘汰机制。在redis.conf中有一行配置

1
maxmemory-policy volatile-lru

07-Redis 穿透,击穿,雪崩,倾斜,淘汰,删除
https://janycode.github.io/2017/06/20/05_数据库/04_Redis/07-Redis 穿透,击穿,雪崩,倾斜,淘汰,删除/
作者
Jerry(姜源)
发布于
2017年6月20日
许可协议