Redis大Key问题的解决方案
作者:organwalk
什么是大Key问题
Redis中某些键(key)所对应的值(value)特别大,或者集合类数据结构(如hash、set、zset、list)中存储的元素数量过多,这就是大Key问题。它分为以下三种类型:
单个字符串类型(String)Key的Value特别大
具体大小标准依据业务场景不同而有所变化,一般认为在普通业务场景下,如果单个String类型的value大于1MB,或者在高并发低延迟场景中大于10KB,就可能被视为大Key。
集合数据类型(如Hash、Set、ZSet、List)中的元素数量过多或总体数据量过大
例如,一个Hash类型Key的成员数量虽只有1000个,但这些成员的Value总大小达到100MB,或者一个ZSet类型的Key成员数量达到10000个,也会被看作是大Key问题。
单个Key的内存占用过高
比如阿里云Redis定义中,一个String类型的Key其值达到5MB,或一个ZSet类型的Key成员数量达到10000个,都被视为大Key。
负面影响
读取成本高
大Key由于体积大,读取时会消耗更多的时间,增加延迟,尤其是在网络传输中,大Key会占用更多的带宽,影响系统的响应速度和网络资源的有效利用。
写操作易阻塞
写入大Key时,由于Redis采用单线程模型处理请求,操作大Key会阻塞其他命令的执行,导致整个Redis服务响应变慢,甚至无法正常响应其他请求。
慢查询与主从同步异常
大Key的读写操作时间长,可能触发Redis的慢查询日志记录,频繁的慢查询会加重服务器负担。同时,在主从复制场景下,大Key的同步也会比小Key慢,可能影响数据的一致性和实时性。
占用更多存储空间,导致逐出与OOM
大Key占据大量内存空间,容易触发Redis的内存淘汰策略,造成重要数据被意外移除(逐出)。在极端情况下,可能会导致Redis实例因内存耗尽而崩溃(OOM)。
集群架构下的内存资源不均衡
在Redis集群中,若某个分片上有大Key,该分片的内存使用率将远高于其他分片,打破集群间内存使用的均衡状态,影响集群的整体性能和稳定性。
影响高并发与低延迟要求的场景
在对时延敏感的应用中,大Key的存在会显著增加请求处理时间,降低系统处理高并发请求的能力。
产生原因
业务设计不合理
最常见的原因是在没有合理拆分的情况下,直接将大量数据(如大的JSON对象或二进制文件数据)存储在一个键中。这种做法忽视了Redis作为内存数据库的特性,没有充分利用其高效处理小数据块的优势。
未能处理Value动态增长问题
随着时间推移,如果持续向某个键的Value中添加数据,而又没有相应的定期删除机制、合理的过期策略或大小限制,Value的大小最终会增长到难以管理的程度。例如,不断累积的微博粉丝列表、热门评论或直播弹幕等场景很容易形成大Key。
程序Bug
有时候,软件开发中的错误可能导致某些键的生命周期超出预期,或者其包含的元素数量异常增长。例如,如果负责消费LIST类型键的业务代码发生故障,可能会导致该Key的成员只增不减,进而形成大Key。
找出大Key
使用redis-cli的--bigkeys
参数
这是最直接的方法。通过Redis命令行工具redis-cli,使用--bigkeys
参数来扫描Redis实例中的所有Key。它会遍历整个键空间并返回每个数据类型中最大的Key的信息。执行命令如下:
redis-cli --bigkeys
此命令会输出每个数据类型中最大的Key及其相关信息,以及一些整体统计信息,如不同类型Key的数量、平均长度等。
利用Redis RDB Tools
一种更深入和定制化的分析方法是使用Redis RDB Tools这个开源工具。首先,导出Redis的RDB文件,然后使用该工具分析此文件,找出大Key。例如,输出占用内存超过128字节的前5个Keys到CSV文件:
rdb -c memory dump.rdb --bytes 128 --largest 5 -f memory.csv
这样做可以更精确地控制分析条件,比如按大小过滤Key,或者按数量筛选最大的几个Key。
可观测性分析
通过监控工具,跟踪Redis的性能指标,如延迟、吞吐量和错误率,以及分析慢查询日志,也是发现大Key的一种间接方法。如果存在执行时间过长的操作,这可能是大Key导致的。部分云服务提供商还可能提供了直接查看Top Key统计的功能,便于发现内存占用高的Key。
解决方案
那么通过以上手段找出大Key后,就需要分析两个问题🤔:
- 确定大Key是否是业务必要的(如果不是,就让它棍😠),能不能通过业务逻辑的优化来处理这个问题
- 检查程序Bug,看看是不是代码写的有问题🙂,导致Key的大小不正常
如果以上问题都检查无误,也没有解决大Key问题,可以考虑下面的优化策略:
😋 避免大Key问题
解决问题的最好办法就是解决提出问题的人
在业务设计的初期就应该避免生成大Key,仅仅缓存必要的数据字段。
✂ 数据拆分
就像视频分片缓冲一样,可以考虑把大Key分片成小Key进行存储。比如直接存储中国的所有省市区的一些详细信息(招商、气象)肯定会产生大Key(“China”),但如果分片存储,定义一个namespace,然后省、市、区分别去存,就形成了小key,具体说明就是:
定义namespace:可以将“China”作为一个命名空间。
省、市、区分别存储:
- 省级信息可以存储为
China:province:省份ID
,值为该省的省级详细信息。 - 市级信息存储为
China:city:城市ID
,值为该市的市级详细信息。 - 区县级信息存储为
China:district:区域ID
,值为该区的区县级详细信息。
- 省级信息可以存储为
这种是靠定量可以解决的。如果是不定量的需求,即Value会增长的,那么就可以考虑依据第一个分片Value有几片,再按照这个num往下分。
不过,分片可能会造成部分写的问题。比如,一个不恰当但好理解的例子。用户提交订单时,需要同时写入这三个分片:
Order:Info:{OrderID}
-> 订单基本信息Order:Items:{OrderID}
-> 订单商品列表Order:Payment:{OrderID}
-> 订单支付信息
假设在执行写操作时,发生以下情况:
Order:Info:{OrderID}
写入成功Order:Items:{OrderID}
写入成功Order:Payment:{OrderID}
写入失败
这就是部分写问题。那么在设计阶段,可以为每一个Value加入一个版本号,以进行一致性检查,如果有一个版本号没对上,就立马回源,重新读取,然后重新加载并再次尝试写入。
↪️ 换个方向
你有没有考虑过这个数据就不应该用String存储?
比如在网络爬虫中,开发人员可能会用一个很长的String来记录哪些URL已经被访问过,每个字符对应一个URL的hash值,但这样会浪费内存空间。如果用Bitmap来记录访问过的URL,每一位表示一个URL的hash值,这样可以节省大量内存,并且能快速判断URL是否已经访问过。
又或者是用户活跃度统计。如果要记录一个大型网站每天数百万用户的登录活跃情况,对于每一天,可以用一个整数的32位(或64位)比特位来表示当天所有用户是否登录过,其中每位代表一个用户ID是否活跃。如果使用String来存储,即使每个用户活跃状态只需一位表示,也会因为String的编码方式(如UTF-8)而占用更多的空间,每个字符至少占用8位。
🧹 合理清理
只需要设计个方案合理清理就能避免大Key的累积。
- 在低峰期删除。可以考虑在业务流量低的时候定时清理缓存。但很明显,这个方案不太合理。假如一整天业务流量都很高,这时候已经产生大Key了呢?
- 分批定时定量删除。定量删除是为了防止阻塞。
- 异步删除。与del命令不同的是,unlink命令会异步地删除指定的键以及与之相关联的值。 即,它会将要删除的键添加到一个待删除的列表中,并立即返回,不会阻塞客户端。 Redis服务器会在后台异步地删除待删除列表中的键。
到此这篇关于Redis大Key问题的解决方案的文章就介绍到这了,更多相关redis大key内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!