百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术教程 > 正文

一个 Redis 最好存多少 key ?| 运维进阶

mhr18 2024-12-10 14:35 19 浏览 0 评论

我们来看一段对话:

下面来解析:

一、存在哪?

1. 哈希表(hashtable)

要知道能存多少,首先要知道Redis的键值对存在哪?你可能已经猜到了哈希表,它是天然的键值对数据结构,各种语言都支持该数据结构,基本上是使用数组链表的形式实现的(JDK 8用的红黑树),下面是一个整体结构。

dictEntry存储的是实际键值对,它的的结构定义如下所示:

typedefstruct dictEntry {
void*key;
union{
void*val;
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next;
} dictEntry;

dictht是哈希表,它的定义如下:

typedefstruct dictht {

//dictEntry数组链表
    dictEntry **table;

//数组的长度
unsignedlong size;

//数组掩码,等于size-1
unsignedlong sizemask;

//键值个数
unsignedlong used;
} dictht;

2.键值"路由"

对于一个key,首先会通过哈希算法(见下面提示)计算它的哈希值,然后与sizemask做&计算(等价于与size做%计算),算出所在的数组下标(index),然后生成dictEntry插入其中。

index = dictHashKey(key) & sizemask = dictHashKey(key) % size

其中哈希算法在Redis版本中也有迭代:

提:提示:Redis4开始使用siphash算法代替murmur2算法。
   murmur2算法已经证实会受到HashDos的攻击(可以简单理解为破坏哈希的均匀性)。

bug detail:https://paper.seebug.org/180/

假如我向一个空的Redis执行了一条如下命令:

127.0.0.1:6379> set hello world
OK

首先Redis会使用siphash算法计算"hello"的哈希值,然后与sizemask做&计算。

siphashKey("hello") & 3= 984616787& 3= 3

计算出"hello"所对应的数组下标是3,如下所示:

注意kv里面不是直接存字符串,k是sds,v是redisObject。

3.冲突处理

那么问题来了,如果一个新的key算出的数组下标已经包含了其他dictEntry,那么该如何处理呢?这就要引出冲突处理问题,实际上这个问题的处理方式也很简单,因为每个数组下标对应的是一个链表,如果发生冲突,只需要把新的key对应dictEntry挂在链表的第一个位置即可(因为是新插入的,可能马上会用到),例如插入两条数据后:

4.扩缩容(rehash)

接下来问题又来了,单纯依赖链表做冲突处理,链表会越来越长,读写效率会差,所以需要对哈希表做扩容,对于Redis来说,一个哈希数据结构内置了两个哈希表(dictht),一个用于使用,另一个平时闲置,待时机成熟,扩容一倍,然后将数据迁移到另一个哈希表内,如下图所示:

typedefstruct dict {
    dictType *type;
void*privdata;
    dictht ht[2];
long rehashidx; /* rehashing not in progress if rehashidx == -1 */
unsignedlong iterators; /* number of iterators currently running */
} dict;

其中rehashidx用来标识当前dict是否正在做rehash,如果为-1表示没有做rehash。当used > 2^n时候,就需要扩容了(rehashidx代表ht[0]在rehash时候所在的索引值),如下图所示。为了保证Redis单线程的高效性,整个rehash是渐进式(一点点)完成的,但全部迁移完成后,将rehashidx置为-1,表示rehash结束了。

对于key的路由来说,它依然先从dict[0]去找,如果找到了,就顺便把它迁移到dict[1]。如果没找到就要从dict[1]去找。

二、能存多少呢?

相信通过前面的介绍,你已经对Redis的dict有了一定的认识,如果现在有人问你一个Redis能存储多少个键值对,相信这个问题不难回答了吧,因为我们已经了解了Redis的哈希表实现方式,知道size定义了数组的长度,由于它是unsigned long类型(4个字节),所以Redis理论上可以存储2^32个元素(大约40亿个)。下面一段是官方的一段FAQ(引自:https://redis.io/topics/faq)

Whatis the maximum number of keys a single Redis instance can hold?

Redis can handle up to 2^32 keys, and was tested in practice to
 handle at least 250 million keys per instance.

从官方的回答可以看到,Redis虽然可以承担2^32个(大约40亿个)键值对,但是它建议你最佳实践是存放2.5亿个键值对,没有给出具体的原因。

下面我们结合刚才介绍的内容大开脑洞想一下:键值个数的增加可能会引发什么问题呢?

三、可能引起的问题?

1.rehash问题

通过前面的介绍,相信你也简单了解了Redis的扩容是需要通过rehash来完成的,也了解了扩容的相关机制,但是一般来说很多人对于Redis中的这类机制只是浅尝辄止,没有深层次的考虑。现实是残酷的,下面我用一次我实际中遇到的问题来说明rehash可能带来的危险。

那是我在阿里云的时候,有个客户反馈他的Redis内存突然涨了2GB,如下图所示:Redis实例的内存在一分钟内突然从7.46GB涨到9.46GB,并持续了一段时间。

对于此类问题,我们已经有了一套检测方式:各类缓冲区检测、键值增长趋势、大键值、Lua引擎内存等等,但我们进行了一轮检测后,均未发现异常(注释:当时使用的Redis 3.0,内存统计信息不是很全)。经同事提醒后发现可能是rehash造成的,于是抱着尝试的心态做了试验。

如下面的代码所示,我向一个空的Redis写入简单的键值对,2^n是rehash的临界点,在临界点附近放慢键值的写入速度。

publicstaticvoid testRehash(int n) {
//rehash的临界点
int rehashThreshold = (int) Math.pow(2, n);
//临界点左右的偏移量,用于观察数据
int offset = 10;
for(int i = 0; i < rehashThreshold + offset; i++) {
        jedis.set(String.valueOf(i), String.valueOf(i));
//用于观察临界点内存的变化。
if(i > rehashThreshold - offset) {
TimeUnit.SECONDS.sleep(1);
}
}
}

Redis的官方客户端redis-cli提供了一个简单的选项--stat用于定时观察Redis相关状态的变化。

(1) 当n=15时,可以观察到:当keys超过32768(2^15)时,内存突然从3.45M涨到了3.70M。

redis-cli -h 127.0.0.1-p 6379--stat
keys       mem      clients blocked requests            connections
327673.45M201230010(+2)        13
327683.45M201230012(+2)        13
327693.70M201230014(+2)        13
327703.70M201230016(+2)        13
327713.70M201230018(+2)        13
327723.70M201230020(+2)        13

(2) 当n=20时,可以观察到:当keys超过1048576(2^20)时,内存突然从88.70M涨到了104.70M。

keys       mem      clients blocked requests            connections
104857488.70M202278689(+2)        16
104857588.70M202278691(+2)        16
104857688.70M202278693(+2)        16
1048577104.70M202278695(+2)        16
1048578104.70M202278697(+2)        16
1048579104.70M202278699(+2)        16

(3) 当n=26时,效果更加明显,当keys超过67108864(2^26)后,内存直接从5.50G增长到6.50G。

keys       mem      clients blocked requests            connections
671088625.50G202574533(+2)        23
671088635.50G202574535(+2)        23
671088645.50G202574537(+2)        23
671088656.50G202574539(+2)        23
671088666.50G202574541(+2)        23
671088676.50G202574543(+2)        23

再结合一下当时客户Redis的键值变化,基本可以确定是由rehash造成的内存异常增长。至此我们已经分析出内存突然增长的原因。(过期键值的dictht也有1G的消耗)

dict: 67108864* 16字节(dictEntry) = 1GB
expires: 67108864* 16字节(dictEntry) = 1GB

但是还有更糟糕的情况,如果内存暴增的一瞬间超过了Redis设置的最大内存,不仅会产生大量的数据逐,而且会造成客户端产生大量的超时。下面我们用一个例子来具体说明。

现在我们将Redis设置最大使用内存为6GB,并设置最大内存淘汰策略是allkeys-lru。

127.0.0.1:6379> config set maxmemory 6GB
OK
127.0.0.1:6379> config set maxmemory-policy allkeys-lru
OK
127.0.0.1:6379> config rewrite
OK

我们继续设置n=26运行上述程序,会发现在rehash临界点瞬间(67108864),redis-cli --stat会卡住,过一段时间后内存虽然也增长了,但是发现key大量丢失(rehash完成,但是rehash一瞬间内存的使用已经超过maxmemory,触发了maxmemory-policy),而且又长时间的卡顿,如果放在生产环境,如果对QPS要求很高的核心业务,很可能就是一次事故。

671088625.50G202710190(+2)        26
671088635.50G202710192(+2)        26
671088645.50G202710194(+2)        26
======================这里redis-cli --stat停顿======================
612025975.56G202710196(+2)        26
612025985.56G102710198(+2)        26
612025985.56G102710199(+1)        26
612025985.56G102710200(+1)        26

下表是每个n对应的键值个数和rehash瞬间的内存额外开销:

n

键值个数

rehash容量

20

1048576(百万)

16MB

26

67108864(千万)

1GB

27

134217728(亿)

2GB

28

268435456(亿)

4GB

2.小键值和分片过大问题

你可能经常听到一个建议,Redis的分片(maxmemory)不要设置过大,一般建议最大不要超过8G,这里面原因很多:

(1) bgsave和bgrewriteaof要在主线程执行fork操作,分片越大,执行时间越长,阻塞Redis可能性越大。

(2) 分片越大,RDB落盘、网络传输、加载时间都是会较长,对于CPU、网络、磁盘IO的开销也会越大。

这些内容在后面的持久化文章中都会有介绍,那我们来看一下在不同键值个数、value字节数大小下,内存的开销总和,所以键值个数自己可以掂量下

类型

8字节

50字节

100字节

500字节

1KB

10万

8.7MB

12MB

18MB

56MB

105MB

100万

78MB

116MB

169MB

550MB

1.01GB

500万

448M

667MB

980MB

3.1G

5.75GB

1000万

790MB

1.2GB

1.7GB

5.5GB

10GB

1亿

8GB

12GB

17GB

55GB

100GB

3.死键问题

Redis将所有键值对保存dict中,除此之外还有一个dict *expires用来保存过期键,用于标记设置了过期时间的键值,整个定义如下:

typedefstruct redisDb {
    dict *dict;    /* The keyspace for this DB */
    dict *expires; /* Timeout of keys with a timeout set */
......
} redisDb;

Redis有两种过期键值处理机制:

(1) 主动过期:访问key时,先去expires表访问,如果发现已经过期就删除掉。(2) 被动过期:通过某种机制定期抽样扫描expires表,如果发现过期就删除。

其中详细的机制这里就不多做介绍了,而这个定时任务也是在Redis的单线程中完成的。也就是说,如果执行过于频繁,会影响Redis的性能;但是如果执行不频繁的话,会使得大量该过期的数据没有过期,造成内存浪费。这种现象被称为“死键问题”。

如果键值数量过多,并设置了过期时间,很可能出现类似问题。从我的实际经验来看,一旦键值个数超过千万级别,就会有这样的情况发生,下图中的下面两个节点是经过scan后的Redis实例,可以发现内存量和键值个数急剧减少。

4.数据剔除问题

Redis有最大内存淘汰机制(maxmemory-policy),如果键值个数过多,那么可能逐出的就会更多,也就意味段时间内有大量的删除操作,甚至会造成Redis段时间不可用,如下图所示:

总结:

本文首先通过Redis到底能存储多少个键值对,引出Redis的Hash表实现方式(数组链表)、扩缩容等原理,最后通过一个开脑洞的思考探讨,分析了各种利弊,最终讨论Redis到底存储多少个键值对会比较好(最多千万级别)。

相关推荐

Java面试题合集200道!

1.Java中操作字符串都有哪些类?它们之间有什么区别?String、StringBuffer、StringBuilder.String和StringBufer、StringBuilder的区别...

JAVA分布式锁的原理,及多种分布式实现优劣对比分析

引题比如在同一个节点上,两个线程并发的操作A的账户,都是取钱,如果不加锁,A的账户可能会出现负数,正确的方式是对账户acount进行加锁,即使用synchronized关键字,对其进行加锁后,当有线程...

百度Linux C++后台开发面试题(个人整理)

1、C/C++程序的内存分区其实C和C++的内存分区还是有一定区别的,但此处不作区分:1)、栈区(stack)—由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中...

什么是云计算?看这篇就够了(建议收藏)

一、什么是云?云,又称云端,指无数的大型机房或者大型数据中心。二、为什么需要云?1)从用户的角度来讲:传统应用的需求日益复杂,比如需要支持更多的用户,需要更强的计算能力等,为满足这些日益增长的需求,企...

写PHP框架需要具备那些知识?

如果没用过框架,讨论各个框架的内容都没有可讨论性,想自己写个框架涉及到的内容很多,个人觉得自己写一个框架对自己的逻辑思维,开发架构以及这门语言都有质的提升。可以参照其他框架的源代码,仅仅是看他们的思路...

不允许还有Java程序员不了解BlockingQueue阻塞队列的实现原理

我们平时开发中好像很少使用到BlockingQueue(阻塞队列),比如我们想要存储一组数据的时候会使用ArrayList,想要存储键值对数据会使用HashMap,在什么场景下需要用到Blocking...

Java性能优化指南—缓存那些事

由于笔者自身水平有限,如果有不对或者任何建议欢迎批评和指正本文预计阅读时间10分钟,分为前言、填坑两部分,主要包含缓存的基本使用到高级应用场景的介绍一、前言在处理高并发请求时,缓存几乎是无往不利的利器...

卓象科技:Nosql的介绍以及和关系型数据库的区别

Nosql介绍NoSQL(NotOnlySQL),泛指非关系型数据库。Nosql的全称是NotOnlySql,这个概念很早就有人提出,在09年的时候比较火。Nosql指的是非关系型数...

腾讯一面凉经(一面竟然就问了2小时,什么情况?)

这次一面感觉是在打心理战,哥们自己的心里防线基本是被击溃,面到怀疑人生的程度,所以过程感觉不是太好,很多题哥们自己也感觉没答好,要么答得“缺胳膊少腿”,要么就是“画蛇添足”。先是聊项目,从项目的架构设...

我凭借这份pdf,最终拿到了阿里,腾讯,京东等八家大厂offer

怎样才能拿到大厂的offer,没有掌握绝对的技术,那么就要不断的学习我是如何笑对金九银十,拿到阿里,腾讯等八家大厂的offer的呢,今天分享我的秘密武器,美团大神整理的Java核心知识点,面试时面试官...

高并发 异步解耦利器:RocketMQ究竟强在哪里?

本文带大家从以下几个方面详细了解RocketMQ:RocketMQ如何保证消息存储的可靠性?RocketMQ如何保证消息队列服务的高可用?如何构建一个高可用的RocketMQ双主双从最小集群?Rock...

阿里最新Java架构师成长笔记开源

下面先给大家上一个总的目录大纲,基础的东西就不进行过多的赘述,我们将会从JVM说起,同时由于每篇的内容过多,我们也只说重点,太过基础的内容谁都会,我就不多敲字浪费大家的时间了!JVM多线程与高并发Sp...

程序员失业2个月找不到工作,狂刷了5遍这份pdf终获字节跳动offer

写在前面1月初失业,找了近2个多月的工作了,还没找到心仪的工作,感觉心好慌,不知道该怎么办了?找不到工作的时候压力很大,有人说自信会很受打击,还有人说会很绝望,是人生的低谷……尽管很多时候我们自己知道...

Spring AI 模块架构与功能解析

SpringAI是Spring生态系统中的一个新兴模块,专注于简化人工智能和机器学习技术在Spring应用程序中的集成。本文将详细介绍SpringAI的核心组件、功能模块及其之间的关...

Nginx从入门到精通,超详细整理,含项目实战案例|运维必学

Nginx是免费的、开源的、高性能的HTTP和反向代理服务器、邮件代理服务器、以及TCP/UDP代理服务器。因为它的稳定性、丰富的模块库、灵活的配置和低系统资源的消耗而闻名。Nginx可以做静态HT...

取消回复欢迎 发表评论: