0%

redis缓存穿透和大key热key问题

1. 缓存问题

1.1 缓存穿透(都没有数据,用布隆过滤器)

缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求。由于缓存不命中,并且出于容错考虑,如果从数据库查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,失去了缓存的意义。

如发起为id为“-1”的数据或id为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大。

解放方案

  • 接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;

  • 从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击

  • 布隆过滤器。bloomfilter就类似于一个hash set,用于快速判某个元素是否存在于集合中,其典型的应用场景就是快速判断一个key是否存在于某容器,不存在就直接返回。布隆过滤器的关键就在于hash算法和容器大小。

    布隆判断有可能是错的(别人的 hash 碰撞)。不过告诉我不存在,就一定不存在。 先去布隆过滤器查询,再去 redis。

布隆过滤器

布隆过滤器说某个元素存在,小概率会误判。布隆过滤器说某个元素不在,那么这个元素一定不在。

img

由位数组 和一系列随机哈希函数 两部分组成的数据结构。

位数组中的每个元素都只占用 1 bit ,并且每个元素只能是 0 或者 1。这样申请一个 100w 个元素的位数组只占用 1000000Bit / 8 = 125000 Byte = 125000/1024 kb ≈ 122kb 的空间。

当一个元素加入布隆过滤器中的时候

  1. 使用布隆过滤器中的哈希函数对元素值进行计算,得到哈希值(有几个哈希函数得到几个哈希值)。
  2. 根据得到的哈希值,在位数组中把对应下标的值置为 1。

当我们需要判断一个元素是否存在于布隆过滤器的时候:

  1. 对给定元素再次进行相同的哈希计算;
  2. 得到值之后判断位数组中的每个元素是否都为 1,如果值都为 1,那么说明这个值在布隆过滤器中,如果存在一个值不为 1,说明该元素不在布隆过滤器中。

1.2 缓存击穿(数据库有数据)

缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。

解决方案

  • 设置热点数据永远不过期。

  • 接口限流与熔断,降级。重要的接口一定要做好限流策略,防止用户恶意刷接口,同时要降级准备,当接口中的某些 服务 不可用时候,进行熔断,失败快速返回机制。

  • 加互斥锁

1.3 缓存雪崩

缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。

但是也有Redis 挂掉了,请求全部走数据库。对缓存数据设置相同的过期时间,导致某段时间内缓存失效,请求全部走数据库。

解决方案

  • 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。

  • 实现 Redis 的高可用 (主从架构 + Sentinel 或者 Redis Cluster),尽量避免 Redis 挂掉这种情况发生。

  • 万一 Redis 真的挂了,我们可以设置本地缓存 + 限流,尽量避免我们的数据库被干掉 (起码能保证我们的服务还是能正常工作的)

2. key的问题

2.1 bigkey

通常以Key的大小和Key中成员的数量来综合判定,例如:

  1. Key本身的数据量过大:一个String类型的Key,它的值为5 MB。
  2. Key中的成员数过多:一个ZSET类型的Key,它的成员数量为10000个。
  3. Key中成员的数据量过大:一个Hash类型的Key,它的成员数量虽然只有1000个但这些成员的Value(值)总大小为100 MB。

查找大key

1
redis-cli -h 127.0.0.1 -p6379 -a "password" -- bigkeys

解决方案

  1. 对大Key进行拆分

    例如将含有数万成员的一个HASH Key拆分为多个HASH Key,并确保每个Key的成员数量在合理范围。在Redis集群架构中,拆分大Key能对数据分片间的内存平衡起到显著作用。

  2. 对大Key进行清理

    将不适用Redis能力的数据存至其它存储,并在Redis中删除此类数据。Redis 4.0及之后版本:您可以通过 UNLINK 命令安全地删除大Key甚至特大Key,该命令能够以非阻塞的方式,逐步地清理传入的Key。

  3. 监控Redis的内存水位

    可以通过监控系统设置合理的Redis内存报警阈值进行提醒,例如Redis内存使用率超过70%、Redis的内存在1小时内增长率超过20%等。

  4. 对过期数据进行定期清理

    堆积大量过期数据会造成大Key的产生,例如在HASH数据类型中以增量的形式不断写入大量数据而忽略了数据的时效性。可以通过定时任务的方式对失效数据进行清理。

2.2 hotkey

通常以其接收到的Key被请求频率来判定,例如:

  1. QPS集中在特定的Key:Redis实例的总QPS(每秒查询率)为10,000,而其中一个Key的每秒访问量达到了7,000。

  2. 带宽使用率集中在特定的Key:对一个拥有上千个成员且总大小为1 MB的HASH Key每秒发送大量的 HGETALL 操作请求。

  3. CPU使用时间占比集中在特定的Key:对一个拥有数万个成员的Key(ZSET类型)每秒发送大量的 ZRANGE 操作请求。

解决方案

  1. 在Redis集群架构中对热Key进行复制

    例如将热Key foo复制出3个内容完全一样的Key并名为foo2、foo3、foo4,将这三个Key迁移到其他数据分片来解决单个数据分片的热Key压力。(由原来更新一个Key演变为需要更新多个Key)

  2. 使用读写分离架构

    如果热Key的产生来自于读请求,您可以将实例改造成读写分离架构来降低每个数据分片的读请求压力,甚至可以不断地增加从节点。但是读写分离架构在增加业务代码复杂度的同时,也会增加Redis集群架构复杂度。

  3. 内存预热

    是指在系统启动或重启后,主动将热点数据加载到内存中。这样当用户访问这些热点数据时,可以直接从内存中获取,避免对 Redis 造成压力。

2.3 分区不均匀

可能原因:大Key、Hash Tags。

  • 大key。对key进行拆分。
  • HashTag 机制使用{}大括号,指定key只计算大括号内字符串的哈希,从而将不同key的健值对插入到同一个哈希槽。

    检查下业务代码,有没有引入HashTag,将太多的key路由到了一个实例。

3. 头脑风暴

  • 缓存穿透:布隆过滤器。缓存击穿:限流。缓存雪崩:随机过期时间。

  • 大key要去做分割,热key要去分片或内存预热,分区不均匀是大key和hashtag机制。

4. 参考资料

给作者打赏,可以加首页微信,咨询作者相关问题!