1、Redis为何高性能?
- 基于内存存储数据
- 高效的IO模型
- 高效的数据结构
2、Redis真的只是单线程吗?
Redis 是单线程,主要是指 Redis 的网络 IO 和键值对读写是由一个线程来完成的,这也是 Redis 对外提供键值存储服务的主要流程。但 Redis 的其他功能,比如持久化、异步删除、集群数据同步等,其实是由额外的线程执行的。
3、Redis为什么要用单线程?
采用多线程缺点
- 多线程开发复杂度高,可维护性差
- 共享资源的并发访问控制问题
采用多线程优点
- 有效利用cpu资源
因为Redis是基于内存存储数据,所以多线程的优点所提升的性能并不能抵消掉其开发程度的困难。并且通过测试,Redis会随着线程数的增多,吞吐率并不再会有明显提升。
4、Redis的单线程为什么这么快?
Linux 中的 IO 多路复用机制是指一个线程处理多个 IO 流,就是我们经常听到的 select/epoll 机制。简单来说,在 Redis 只运行单线程的情况下,该机制允许内核中,同时存在多个监听套接字和已连接套接字。内核会一直监听这些套接字上的连接请求或数据请求。一旦有请求到达,就会交给 Redis 线程处理,这就实现了一个 Redis 线程处理多个 IO 流的效果。
5、IO模型有哪些?
BIO(同步阻塞):我去上厕所,这个时候坑位都满了,我必须等待坑位释放了,我才能上吧?此时我啥都不干,站在厕所里盯着,过了一会有人出来了,我就赶紧蹲上去。
NIO(同步非阻塞):我去上厕所,这个时候坑位都满了,没关系,哥不急,我出去抽根烟,过会回来看看有没有空位,如果有我就蹲,如果没有我出去接着抽烟或者玩会手机。
多路复用(异步阻塞):我去上厕所,这个时候坑位都满了,没事我等着,等有了新的空位,让他通知我就行,通知了我,我就蹲上去。
AIO(异步非阻塞):我去上厕所,这个时候坑位都满了,没事,我一点也不急,我去厕所外面抽根烟再玩玩手机,等有新的坑位释放了,会有人通知我的,通知我了,我就可以进去蹲了。
- 同步就是我需要自己每隔一段时间,以轮训的方式去看看有没有空的坑位。
- 异步则是有人拉完茅坑会通知你,通知你后你再回去蹲。
- 阻塞就是在等待的过程中,你不去做其他任何事情,干等着。
- 非阻塞就是你再等待的过程中可以去做其他的事,比如抽烟、喝酒、烫头、玩手机。
6、Redis中的阻塞点和IO模型?
同步阻塞模式
- 监听客户端请求(bind/listen)
- 客户端建立连接(accept)
- 从 socket 中读取请求(recv)
- 解析客户端发送请求(parse)
- 根据请求类型读取键值数据(get)
- 最后给客户端返回结果,即向 socket 中写回数据(send)
异步阻塞模式
Redis 类似找厕所的人。内核会一直监听这些套接字上的连接请求或数据请求。一旦有请求到达,就会交给 Redis 线程处理,这就实现了一个 Redis 线程处理多个 IO 流的效果。
7、Redis中有哪些高效的数据结构?(以下源码分析都从redis5源码中截取)
string,list,hash,sort set,set都只是数据的保存形式。
底层的数据结构是:简单动态字符串,双向链表,压缩列表,哈希表,跳表,整数数组
(Redis是使用C写的,而C中根本不存在string,list,hash,set和sort set这些数据类型,那么C是如何将这些数据类型实现出来的呢?)
8、Redis中有哪些高效的数据结构?-哈希表
8.1、哈希表整体
Redis 使用了一个哈希表来保存所有键值对。
其实就是一个数组,数组的每个元素称为一个哈希桶。所以,一个哈希表是由多个哈希桶组成的,每个哈希桶中保存了键值对数据。
哈希桶中的元素保存的并不是值本身,而是指向具体值的指针。这也就是说,不管值是 String,还是集合类型,哈希桶中的元素都是指向它们的指针。
8.2、哈希表的rehash操作
Redis 默认使用了两个全局哈希表:哈希表 1 和哈希表 2。刚插入数据时,默认使用哈希表 1,此时的哈希表 2 并没有被分配空间。随着数据逐步增多,Redis 开始执行 rehash。
这个过程分为三步:
1、给哈希表 2 分配更大的空间,例如是当前哈希表 1 大小的两倍;
2、把哈希表 1 中的数据重新映射并拷贝到哈希表 2 中;
3、释放哈希表 1 的空间。
渐进式 rehash
1、每处理一个请求时,从哈希表 1 中的第一个索引位置开始,顺带着将这个索引位置上的所有 entries 拷贝到哈希表 2 中;
2、Redis本身还会有一个定时任务在执行rehash,如果没有键值对操作时,这个定时任务会周期性地(例如每100ms一次)搬移一些数据到新的哈希表中,这样可以缩短整个rehash的过程。
8.3、哈希表的扩容/缩容
每次插入键值对时,都会检查是否需要扩容。
哈希表的负载因子公式:
load_factor = ht[0].used / ht[0].size
负载因子 = 哈希表已保存节点数量 / 哈希表大小
1、当服务器没有在执行 BGSAVE 命令或者 BGREWRITEAOF 命令,并且哈希表的负载因子大于等于1。
2、服务器正在执行 BGSAVE 命令或者 BGREWRITEAOF 命令,并且哈希表的负载因子大于等于5。
根据 BGSAVE 命令或 BGREWRITEAOF 命令是否正在执行,服务器执行扩展操作所需的负载因子并不相同,这是因为在执行 BGSAVE 命令或 BGREWRITEAOF 命令的过程中,Redis 需要创建当前服务器进程的子进程,而大多数操作系统都采用写时复制 (copy-on-write)技术来优化子进程的使用效率,所以在子进程存在期间,服务器会提高执行扩展操作所需的负载因子,尽可能地避免在子进程存在期间进行哈希表扩展操作。这可以避免不必要的内存写入操作,最大限度节约内存。另一方面,当哈希表的负载因子小于0.1时,程序自动开始对哈希表执行收缩操作,缩容大小为第一个大于等于当前key数量的2的n次方。
9、Redis中有哪些高效的数据结构?-简单动态字符串(Simple Dynamic String,SDS)
9.1、简单动态字符串(Simple Dynamic String,SDS)整体
buf:字节数组,保存实际数据。为了表示字节数组的结束,Redis 会自动在数组最后加一个“\0”,这就会额外占用 1 个字节的开销。数组意味着使用者可以修改,它的底层实现有点类似于 Java 中的 ArrayList。
len:占 4 个字节,表示 buf 的已用长度。
alloc:也占个 4 字节,表示 buf 的实际分配长度,一般大于 len。
9.2、简单动态字符串(Simple Dynamic String,SDS)结构分析
优势?(和C语言的字符串比较)
- 更快速的获取字符串长度
Java的字符串有提供length方法,列表有提供size方法,可以直接获取大小。但是C却不一样,更偏向底层实现,所以没有直接的方法使用。这样就带来一个问题,如果想要获取某个数组的长度,就只能从头开始遍历,当遇到第一个’\0’则表示该数组结束。这样的速度太慢了,不能每次因为要获取长度就变量数组。所以设计了SDS数据结构,在原来的字符数组外面增加总长度,和已用长度,这样每次直接获取已用长度即可。复杂度为O(1)。 - 数据安全,不会截断
如果传统字符串保存图片,视频等二进制文件,中间可能出现’\0’,如果按照原来的逻辑,会造成数据丢失。所以可以用已用长度来表示是否字符数组已结束。 - 空间预分配策略与惰性空间释放策略
通过空间预分配策略与惰性空间释放策略,Redis可以减少连续执行字符串增长操作所需的内存重分配次数。如果追加字符串并且空闲空间不够,则需要扩容。如果目前的字符串小于1M,则直接扩容双倍,如果目前的字符串大于1M,则直接添加1M,数据长度最大为512M。如果截去字符串,并不会释放多余空间,而是留着备用。但SDS也提供了相应的API,让我们可以在有需要时,真正地释放SDS的未使用空间。
9.3、简单动态字符串(Simple Dynamic String,SDS)源码分析
这是在源码中定义的sds数据结构。在sdshdr5那个数据结构中有个注释。
1、sdshdr5没有用到,我们只是将数据结构标识在这里。
2、所以这里描述了五种sds字符串类型。
(在源码中有个flags的标志位,注释写的是3个有效字节,5个字节没用到。本人猜测是用作标识结束位的,但没有找到实际代码。希望有人能解答下。)
9.3.1、为什么需要定义五种sds字符串类型?
可以看到上面五种sds字符串结构的区别只有len和alloc的类型是有不同的。像uint8_t、uint16_t、uint32_t、uint64_t,每种类型存储的数值大小是有不同的。
redis为了更加合理使用内存,所以定义了五种类型。
9.3.2、何时使用五种sds字符串类型?
可以看到选取哪种sds的类型是由这个数据长度决定的。并且在这里有个宏定义,(LONG_MAX == LLONG_MAX),这是判断系统的long int 和long long int类型数据长度,区分系统是32位操作系统还是64位操作系统。(在32位操作系统中long int占4字节,64位操作系统中long int占8字节,long long int 在32位和64位编译系统中,都占8字节。)
10、Redis中有哪些高效的数据结构?-压缩列表(ZIPLIST)
10.1、压缩列表整体
zlbytes:记录整个压缩列表占用的内存字节数:在对压缩列表进行内存重分配,或者计算zlend的位置时使用
zltail:列表尾的偏移量,程序无须遍历整个压缩列表就可以确定表尾节点的地址
zllen:列表中的entry个数
zlend:标记列表结束
10.2、压缩列表结构分析
时间复杂度
如果我们要查找定位第一个元素和最后一个元素,可以通过表头三个字段的长度直接定位,复杂度是 O(1)。而查找其他元素时,就没有这么高效了,只能逐个查找,此时的复杂度就是 O(N) 了。
优势?(和其他数据结构比较)
内存利用率,数组和压缩列表都是非常紧凑的数据结构,它比链表占用的内存要更少。Redis是内存数据库,大量数据存到内存中,此时需要做尽可能的优化,提高内存的利用率。
10.3、压缩列表源码分析
这里描述的是压缩列表的entry。大概的意思是每一个entry都包含两部分信息。
1、(prevlen)前一个entry的长度,以便能从后到前遍历
2、(encoding)当前entry的编码类型,如果是字符串类型,则表示字符串长度。
10.3.1、encoding是怎么做区分的?
encoding的两种情况:
- 1字节、2字节或者5字节长,值的最高位为00、01或者10的是字节数组编码(字符串类型),这种编码表示节点的entry-data保存着字节数组,数组的长度由编码除去最高两位之后的其他位记录。
- 1字节长,值的最高位以11开头的是整数编码(数值类型),这种编码表示节点的entry-data保存着整数值,整数值的类型和长度由编码除去最高两位之后的其他位记录。
10.3.2、连锁更新?
由于压缩列表的’prevlen’属性可能是1字节或5字节,若在一个压缩列表中,有多个连续的、长度介于250字节到253字节之间的节点,则添加新节点或删除节点都有可能会引发多个节点的连续多次空间扩展,这种现象称之为“连锁更新”。
因为连锁更新在最坏情况下需要对压缩列表执行N次空间重分配操作, 而每次空间重分配的最坏复杂度为O(N), 所以连锁更新的最坏复杂度为O(N^2)。但“连锁更新”发生的概率是很低的,所以不必担心其会影响压缩列表的性能。
11、Redis中有哪些高效的数据结构?-跳表(SKIPLIST)
11.2、跳表整体
跳表是在链表的基础上,增加了多级索引,通过索引位置的几个跳转,实现数据的快速定位,
11.2、跳表结构分析
优势?
相比于单纯的链表, 跳表采用多级索引的方式提高查询效率,也是采用二分查找的思想来定义索引结构。
劣势?
采用多级索引的方式,也就是空间换时间,即是优势,也是劣势。增加了内存开销。
11.3、跳表源码分析
11.3.1、跳表整体结构
header:指向跳跃表的表头节点,通过这个指针程序定位表头节点的时间复杂度就为O(1)
tail:指向跳跃表的表尾节点,通过这个指针程序定位表尾节点的时间复杂度就为O(1)
level:记录目前跳跃表内,拥有节点的最大层级(表头节点的层数不计算在内)
length:记录跳跃表的长度,也即是,跳跃表目前包含节点的数量(表头节点不计算在内),通过这个属性,程序可以再O(1)的时间复杂度内返回跳跃表的长度。
11.3.2、跳表节点结构
这里有注释,zset的跳表特殊的实现方式。由上图可以看出zset又套了一层zskiplist。
11.3.3、跳表何时会增加索引?
由上图可知,跳表设计采用随机算法设计。每新增一个节点,这个方法返回该节点的最高级索引,并且在小于该最高级别的索引中也会有该节点索引。正如Redis作者所说,SkipList使用的是概率平衡而不是强制平衡。
11.3.4、跳表的索引概率如何计算?
跳表的最高级别索引是32。从注释中可知,定义这个级别应该满足2的64次方个对象使用。
跳表的随机比较值是0.25。也就是说新增一个对象有四分之一的机会,该节点可以在二级索引中存在,有十六分之一的机会在三级索引中存在,以此类推。
12、Redis中数据结构整体分析
debug object
查看一个key内部信息,比如refcount、encoding、serializedlength等,结果如下:
Value at:key的内存地址
refcount:引用次数
encoding:编码类型
serializedlength:序列化长度
lru_seconds_idle:空闲时间
(ps: redis为了节省内存会在初始化服务器时,创建一万个字符串对象,这些对象包含了0到9999的所有整数值,当服务器需要用到值为0到9999的字符串对象时,服务器就会使用这些共享对象,而不是创建新的对象。)
13、Redis中数据结构整体分析-String类型
set一个整数值类型,encoding的类型是int
set一个字符串类型,encoding的类型是embstr或者raw
13.1、String类型的encoding怎么区分的?
Redis的string类型三种存储方式
- 当保存的是 Long 类型整数时,RedisObject 中的指针就直接赋值为整数数据了,这样就不用额外的指针再指向整数了,节省了指针的空间开销。
- 当保存的是字符串数据,并且字符串小于等于 44 字节时,RedisObject 中的元数据、指针和 SDS 是一块连续的内存区域,这样就可以避免内存碎片。这种布局方式也被称为 embstr 编码方式。
- 当字符串大于 44 字节时,SDS 的数据量就开始变多了,Redis 就不再把 SDS 和 RedisObject 布局在一起了,而是会给 SDS 分配独立的空间,并用指针指向 SDS 结构。这种布局方式被称为 raw 编码模式。
14、Redis中数据结构整体分析-List类型
版本小于3.2.0列表类型的内部编码有两种:
- ziplist(压缩列表):当哈希类型元素个数小于hash-max-ziplist-entries配置(默认512个)同时所有值都小于hash-max-ziplist-value配置(默认64个字节)时,Redis会使用ziplist作为哈希的内部实现。
- linkedlist(链表):当列表类型无法满足ziplist的条件时,Redis会使用linkedlist作为列表的内部实现。
版本大于等于3.2.0列表类型的内部编码有一种:
- quickList:是由ziplist组成的双向链表,链表中的每一个节点都以压缩列表ziplist的结构保存着数据,而ziplist有多个entry节点,保存着数据。相当与一个quicklist节点保存的是一片数据,而不再是一个数据。
每个quicklistNode节点的ziplist字节大小不能超过8kb。
15、Redis中数据结构整体分析-其他类型
15.1、Redis中数据结构整体分析-哈希(hash)类型
- ziplist(压缩列表):当哈希类型元素个数小于hash-max-ziplist-entries配置(默认512个)同时所有值都小于hash-max-ziplist-value配置(默认64个字节)时,Redis会使用ziplist作为哈希的内部实现,ziplist使用更加紧凑的结构实现多个元素的连续存储,所以在节省内存方面比hashtable更加优秀。
- hashtable(哈希表):当哈希类型无法满足ziplist的条件时,Redis会使用hashtable作为哈希的内部实现。因为此时ziplist的读写效率会下降,而hashtable的读写时间复杂度为O(1)。
15.2、Redis中数据结构整体分析-集合(set)类型
- intset(整数集合):当集合中的元素都是整数且元素个数小于set-max-intset-entries配置(默认512个)时,Redis会选用intset来作为集合内部实现,从而减少内存的使用。
- hashtable(哈希表):当集合类型无法满足intset的条件时,Redis会使用hashtable作为集合的内部实现。
15.3、Redis中数据结构整体分析-有序集合(sort set)类型
- ziplist(压缩列表):当有序集合的元素个数小于zset-max-ziplist-entries配置(默认128个)同时每个元素的值小于zset-max-ziplist-value配置(默认64个字节)时,Redis会用ziplist来作为有序集合的内部实现,ziplist可以有效减少内存使用。
- skiplist(跳跃表):当ziplist条件不满足时,有序集合会使用skiplist作为内部实现,因为此时ziplist的读写效率会下降。