编辑
2023-06-02
💌中间键
00
请注意,本文编写于 748 天前,最后修改于 225 天前,其中某些信息可能已经过时。

目录

前言
redis数据热点引起原因?
真实项目中,热点数据问题如何准确定位的呢?
如何解决热点数据问题
redis支持丰富的数据类型,这些数据类型存储的大Value如何解决,线上有遇到这种情况吗?

前言

正常情况下,Redis集群中数据都是均匀分配到每个节点,请求也会均匀的分布到每个分片上,但在一些特殊场景中,比如外部爬虫、攻击、热点商品等,最典型的就是明星在微博上宣布离婚,吃瓜群众纷纷涌入路演,导致微博评论功能崩溃,这种短时间内某些key访问量过于大,对于这种相同的key会请求到同一台数据分片上,导致该分片负责较高成为瓶颈问题,导致雪崩等一些问题。

redis数据热点引起原因?

案例回答:

关于热点数据问题我有话要说,这个问题我早在刚刚学习使用 Redis 时就从已经意识到了,所以在使用时会刻意避免,坚决不会给自己挖坑,热点数据最大的问题会造成 Reids 集群负载不均衡(也就是数据倾斜)导致的故障,这些问题对于 Redis 集群都是致命打击。

先说说造成 Reids 集群负载不均衡故障的主要原因:

  1. 高访问量的 Key,也就是热 key,根据过去的维护经验一个 key 访问的 QPS 超过 1000 就要高度关注了,比如热门商品,热门话题等。
  2. 大 Value,有些 key 访问 QPS 虽然不高,但是由于 value 很大,造成网卡负载较大,网卡流量被打满,单台机器可能出现千兆 / 秒,IO 故障。
  3. 热点 Key + 大 Value 同时存在,服务器杀手。

那么热点 key 或大 Value 会造成哪些故障呢:

  1. 数据倾斜问题:大 Value 会导致集群不同节点数据分布不均匀,造成数据倾斜问题,大量读写比例非常高的请求都会落到同一个 redis server 上,该 redis 的负载就会严重升高,容易打挂。
  2. QPS 倾斜:分片上的 QPS 不均。
  3. 大 Value 会导致 Redis 服务器缓冲区不足,造成 get 超时。
  4. 由于 Value 过大,导致机房网卡流量不足。
  5. Redis 缓存失效导致数据库层被击穿的连锁反应。

reds缓存穿透

理解重在穿透吧,也就是访问透过redis直接经过mysql,通常是一个不存在的key,在数据库查询为null。每次请求落在数据库、并且高并发。数据库扛不住会挂掉。

解决方案:

  1. 可以将查到的null设为该key的缓存对象
  2. 可以根据明显错误的key在逻辑层就就行验证。
  3. 可以分析用户行为,是否为故意请求或者爬虫、攻击者。针对用户访问做限制。
  4. 用布隆过滤器(超大型hashmap)先过滤。

redis缓存雪崩

理解雪崩,就是某东西蜂拥而至的意思,像雪崩一样。在这里,就是redis缓存集体大规模集体失效,在高并发情况下突然使得key大规模访问mysql,使得数据库崩掉。可以想象下国家人口老年化。以后那天人集中在70-80岁,就没人干活了。国家劳动力就造成压力。

解决方案:

  1. 将key的过期时间后面加上一个随机数,让key均匀的失效。
  2. 考虑用队列或者锁让程序执行在压力范围之内,当然这种方案可能会影响并发量。
  3. 热点数据可以考虑不失效

redis缓存击穿

缓存击穿,是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,好像蛮力击穿一样。

缓存和击穿的不同

  1. 穿透的意思是想法绕过redis去使得数据库崩掉。
  2. 而击穿你可以理解为正面刚击穿,这种通常为大量并发对一个key进行大规模的读写操作。这个key在缓存失效期间大量请求数据库,对数据库造成太大压力使得数据库崩掉。就比如在秒杀场景下10000块钱的mac和100块的mac这个100块的那个订单肯定会被抢到爆,不断的请求(当然具体秒杀有自己处理方式这里只是举个例子)。
  3. 所以缓存击穿就是针对某个常用key大量请求导致数据库崩溃。

解决方案:

  1. 可以使用互斥锁避免大量请求同时落到db。
  2. 布隆过滤器,判断某个容器是否在集合中
  3. 可以将缓存设置永不过期(适合部分情况)
  4. 做好熔断、降级,防止系统崩溃。

真实项目中,热点数据问题如何准确定位的呢?

案例回答:

这个问题的解决办法比较宽泛,要具体看不同业务场景,比如公司组织促销活动,那参加促销的商品肯定是有办法提前统计的,这种场景就可以通过预估法。对于突发事件,不确定因素,Redis 会自己监控热点数据。大概归纳下:

  1. 提前获知法:根据业务,人肉统计 or 系统统计可能会成为热点的数据,如,促销活动商品,热门话题,节假日话题,纪念日活动等。
  2. redis客户端收集法,调用端通过计数的方式统计 key 的请求次数,但是无法预知 key 的个数,代码侵入性强。
  3. redis集群代理层统计。像 Twemproxy,codis 这些基于代理的 Redis 分布式架构,统一的入口,可以在 Proxy 层做收集上报,但是缺点很明显,并非所有的 Redis 集群架构都有 proxy。

image.png

  1. redis服务端收集。监控 Redis 单个分片的 QPS,发现 QPS 倾斜到一定程度的节点进行 monitor,获取热点 key, Redis 提供了 monitor 命令,可以统计出一段时间内的某 Redis 节点上的所有命令,分析热点 key,在高并发条件下,会存在内存暴涨和 Redis 性能的隐患,所以此种方法适合在短时间内使用;同样只能统计一个 Redis 节点的热点 key,对于集群需要汇总统计,业务角度讲稍微麻烦一点。

image.png 5. 修改redis源码。我发现 Redis4.0 为我们带来了许多新特性,其中便包括基于 LFU 的热点 key 发现机制,有了这个新特性,我们就可以在此基础上实现热点 key 的统计,这个只是我的个人思路。

如何解决热点数据问题

案例回答:

关于如何治理热点数据问题,解决这个问题主要从两个方面考虑,第一是数据分片,让压力均摊到集群的多个分片上,防止单个机器打挂,第二是迁移隔离

概括总结:

  1. key拆分。如果当前 key 的类型是一个二级数据结构,例如哈希类型。如果该哈希元素个数较多,可以考虑将当前 hash 进行拆分,这样该热点 key 可以拆分为若干个新的 key 分布到不同 Redis 节点上,从而减轻压力
  2. 迁移热点key。以 Redis Cluster 为例,可以将热点 key 所在的 slot 单独迁移到一个新的 Redis 节点上,这样这个热点 key 即使 QPS 很高,也不会影响到整个集群的其他业务,还可以定制化开发,热点 key 自动迁移到独立节点上,这种方案也较多副本。
  3. 热点key限流。对于读命令我们可以通过迁移热点 key 然后添加从节点来解决,对于写命令我们可以通过单独针对这个热点 key 来限流。
  4. 增加本地缓存。对于数据一致性不是那么高的业务,可以将热点 key 缓存到业务机器的本地缓存中,因为是业务端的本地内存中,省去了一次远程的 IO 调用。但是当数据更新时,可能会造成业务和 Redis 数据不一致。

redis支持丰富的数据类型,这些数据类型存储的大Value如何解决,线上有遇到这种情况吗?

案例回答:

相比热点 key 大概念,大 Value 的概念比好好理解,由于 Redis 是单线程运行的,如果一次操作的 value 很大会对整个 redis 的响应时间造成负面影响,因为 Redis 是 Key - Value 结构数据库,大 value 就是单个 value 占用内存较大,对 Redis 集群造成最直接的影响就是数据倾斜。

先说说多大的 Value 算大,根据公司基础架构给出的经验值可做以下划分:

  • 大:string 类型 value > 10K,set、list、hash、zset 等集合数据类型中的元素个数 > 1000。
  • 超大: string 类型 value > 100K,set、list、hash、zset 等集合数据类型中的元素个数 > 10000。

由于 Redis 是单线程运行的,如果一次操作的 value 很大会对整个 redis 的响应时间造成负面影响,所以,业务上能拆则拆,下面举几个典型的分拆方案:

  1. 一个较大的 key-value 拆分成几个 key-value ,将操作压力平摊到多个 redis 实例中,降低对单个 redis 的 IO 影响
  2. 将分拆后的几个 key-value 存储在一个 hash 中,每个 field 代表一个具体的属性,使用 hget,hmget 来获取部分的 value,使用 hset,hmset 来更新部分属性。
  3. hash、set、zset、list 中存储过多的元素

类似于场景一中的第一个做法,可以将这些元素分拆。 以 hash 为例,原先的正常存取流程是:

hget(hashKey,field) hset(hashKey,field,value)

现在,固定一个桶的数量,比如 10000,每次存取的时候,先在本地计算 field 的 hash 值,模除 10000,确定该 field 落在哪个 key 上,核心思想就是将 value 打散,每次只 get 你需要的。

newHashKey = hashKey +(hash(field)%10000) hset(newHashKey,field,value) hget(newHashKey,field)

本文作者:Eric

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!