Redis基本类型底层原理

Redis为什么是单线程呢

1. 从redis的性能上考虑,单线程避免上下文频繁切换问题,效率高

2. 从redis的内部结构设计原理进行考虑,redis是基于Reactor模式开发了自己的网络事件处理器;这个处理器被称为文件事件处理器.而这个文件事件处理器是单线程的,

所以才叫redis的单线程模型


redis的单线程模型构造部分 最为核心的就是文件事件处理器

而文件事件处理器结构包含5个部分,其实真正包含为4个部分(不包含socket队列,加上主要方便后面理解):多个socket,io多路复用程序,socket队列,文件事件分派器,以及事件处理器.而事件处理器又分为3个部分为:

连接应答处理器,命令请求处理器,命令回复处理器


Redis单线程模型的大致工作流程及原理:

1. 首先在redis启动初始化的时候,redis会先将事件处理器中的连接应答处理器和AE_READABLE事件关联起来;

2. 如果客户端向redis发起连接,会产生AE_READABLE事件,产生该事件后会被IO多路复用程序监听到,然后IO多路复用程序会把监听到的socket信息放入到队列中,

事件分配器每次从队列中取出一个socket,事件分配器把socket给对应的事件处理器.

由于连接应答处理器和AE_READABLE事件在redis初始化的时候已经关联起来,所以由连接应答处理器来处理跟客户端建立连接,

然后通过serversocket创建一个与客户端一对一对应的socket,同时将这个socker01的AE_READABLE事件和命令请求处理器关联起来.

20190615185708852.png

3. 当客户端向redis发生请求时(读,写操作),首先就会在对应的socket如socket01上会产生AE_READABLE事件(步骤),产生该事件后会被IO多路复用程序监听到(步骤b),然后IO多路复用程序会把监听到的socket信息放入队列中(步骤c),

事件分配器每次从队列中取出一个socket(步骤D),然后事件分派器吧socket给对应的事件处理器(步骤E).由于命令处理器和socket01的socket01上读取相关的数据,

如何执行相应的读写处理.操作执行完之后,redis就会将准备好相应的响应数据(如你在redis客户端输入set a 123回车时会看到响应OK),并将socket01的AE_WRITABLE事件和命令回复处理器关联起来.

20190615185708852.png


4. 当客户端会查询redis是否完成相应的操作,就会在socket01上产生一个AE_WRITABLE事件,会由对应的命令回复处理器来处理,就是将准备好的相应数据写入socket01(由于socket连接是双向的),

返回给客户端,如写操作客户端会显示OK

20190615193213901.png

5. 如果命令回复处理器执行完成后,就会删除这个socket01的AE_WRITABLE事件和命令回复处理器的关联.

6. 这样客户端就和redis进行了一次通信.由于连接应答处理器执行一次就够了,如果客户端再次进行操作就会由命令请求处理器来处理,反复执行

在redis中,并没有直接使用这些数据结构来实现键值对数据库,而是基于这些数据结构创建了一个对象系统,这些对象系统也就是前面说的五大数据类型,每种数据类型都至少使用到了一种数据结构.

通过这五种不同类型的对象,redis可以在执行命令之前,根据对象的类型判断一个对象是否可以执行给定的命令,而且可以针对不同的场景,为对象设置多种不同的数据结构,从而优化对象在不同场景下的使用效率.

redis使用前面说的五大数据类型来表示键和值,每次在redis数据库中创建一个减值对时,至少会创建俩个对象,一个是键对象,一个是值对象,

而redis中的每个对象都是由redisObject结构来表示:

1. type属性: 对象的type属性记录了对象的类型,这个类型就是前面讲的五大数据类型:

五大数据类型.png

注意:在redis中,键总是一个字符串对象,而值可以是字符串,列表,集合等对象,所以我们通常说的键为字符串键,表示的是这个键对应的值为字符串对象,

我们说的一个键为集合键时,表示的是这个键对应的值为集合对象.

2. encoding 属性和*prt指针

对象的prt指针指向对象底层的数据结构,而数据结构由encoding属性来决定.

prt指针.png

每种类型的对象都至少使用了俩种不同的编码:

编码.png

可以通过OBJECT ENCODING key命令查看值对象的编码:


下面在另外说一下五大数据类型实现原理:

字符串对象:

字符串是redis最基本的数据类型,不仅所有的key都是字符串类型,其他几种数据类型构成的元素也是字符串.注意字符串的长度不能超过512M

1. 编码

字符串对象的编码可以是int,raw或者embstr

a. int编码:保存的是可以用long类型表示的整数值.

b. raw编码:保存长度大于44字节的字符串

c. embstr编码:保存长度小于44字节的字符串

字符串.png

由上可以看出,int编码是用来保存整数值,raw编码是用来保存长字符串,而embstr是用来保存段字符串.其实embstr编码是专门用来保存短字符串的一种优化编码,raw和embstr的区别

编码 (2).png

aHR0cHM6Ly9pbWFnZXMyMDE4LmNuYmxvZ3MuY29tL2Jsb2cvMTEyMDE2NS8yMDE4MDUvMTEyMDE2NS0yMDE4MDUzMDA3NTYzMjYyNC02OTY5ODEwMTMucG5n.png

embstr与raw都使用redisObject和sds保存数据,区别在于,embstr的使用只分配一次内存空间(因此redisObject和sds是连续的),

而raw需要分配俩次内存空间(分别为redisObject和sds分配空间).因此与raw相比,embstr的好处在于创建时少分配一次空间,删除时少释放一次空间,以及对象的所有数据连在一起,寻找方便.

而embstr的坏处也很明显,如果字符串的长度增加需要重新分配内存时,整个redisObject和sds都需要重新分配空间,因此redis中的embstr实现为只读.

ps:Redis中对于浮点数类型也是作为字符串保存的,在需要的时候再将其转换成浮点数类型。

2. 编码的转换

当 int 编码保存的值不再是整数,或大小超过了long的范围时,自动转化为raw。

对于 embstr 编码,由于 Redis 没有对其编写任何的修改程序(embstr 是只读的),在对embstr对象进行修改时,都会先转化为raw再进行修改,因此,只要是修改embstr对象,修改后的对象一定是raw的,无论是否达到了44个字节。


列表对象:

list 列表,它是简单的字符串列表,按照插入顺序排序,你可以添加一个元素到列表的头部(左边)或者尾部(右边),它的底层实际上是个链表结构。

1. 编码

列表对象的编码可以是 ziplist(压缩列表) 和 linkedlist(双端链表)

比如我们执行以下命令,创建一个 key = ‘numbers’,value = ‘1 three 5’ 的三个值的列表。

rpush numbers 1 "three" 5

ziplist 编码表示如下:

aHR0cHM6Ly9pbWFnZXMyMDE4LmNuYmxvZ3MuY29tL2Jsb2cvMTEyMDE2NS8yMDE4MDUvMTEyMDE2NS0yMDE4MDUzMDExMDExMzg1OS0xOTk5NzA0OTI1LnBuZw.png

linkedlist表示如下:

aHR0cHM6Ly9pbWFnZXMyMDE4LmNuYmxvZ3MuY29tL2Jsb2cvMTEyMDE2NS8yMDE4MDUvMTEyMDE2NS0yMDE4MDUzMDExMDEzNTk2MS0xODE3NzczOTY3LnBuZw.png

2. 编码转换

当同时满足下面两个条件时,使用ziplist(压缩列表)编码:

  1、列表保存元素个数小于512个

  2、每个元素长度小于64字节

  不能满足这两个条件的时候使用 linkedlist 编码。

  上面两个条件可以在redis.conf 配置文件中的 list-max-ziplist-value选项和 list-max-ziplist-entries 选项进行配置。


哈希对象:

哈希对象的键是一个字符串类型,值是一个键值对集合。

1. 编码

哈希对象的编码可以是 ziplist 或者 hashtable。

当使用ziplist,也就是压缩列表作为底层实现时,新增的键值对是保存到压缩列表的表尾。比如执行以下命令:

hset profile name "Tom"

hset profile age 25

hset profile career "Programmer"

如果使用ziplist,profile 存储如下:

1.png

当使用 hashtable 编码时,上面命令存储如下:

2.png

hashtable 编码的哈希表对象底层使用字典数据结构,哈希对象中的每个键值对都使用一个字典键值对。

在前面介绍压缩列表时,我们介绍过压缩列表是Redis为了节省内存而开发的,是由一系列特殊编码的连续内存块组成的顺序型数据结构,相对于字典数据结构,压缩列表用于元素个数少、元素长度小的场景。其优势在于集中存储,节省空间。

2. 编码转换

和上面列表对象使用 ziplist 编码一样,当同时满足下面两个条件时,使用ziplist(压缩列表)编码:

  1、列表保存元素个数小于512个

  2、每个元素长度小于64字节

  不能满足这两个条件的时候使用 hashtable 编码。第一个条件可以通过配置文件中的 set-max-intset-entries 进行修改



集合对象:

集合对象 set 是 string 类型(整数也会转换成string类型进行存储)的无序集合。注意集合和列表的区别:集合中的元素是无序的,因此不能通过索引来操作元素;集合中的元素不能有重复

1. 编码

集合对象的编码可以是 intset 或者 hashtable。

intset 编码的集合对象使用整数集合作为底层实现,集合对象包含的所有元素都被保存在整数集合中。

hashtable 编码的集合对象使用 字典作为底层实现,字典的每个键都是一个字符串对象,这里的每个字符串对象就是一个集合中的元素,而字典的值则全部设置为 null。这里可以类比Java集合中HashSet 集合的实现,HashSet 集合是由 HashMap 来实现的,集合中的元素就是 HashMap 的key,而 HashMap 的值都设为 null。

sadd numbers 1 3 5

1.png

SADD Dfruits "apple" "banana" "cherry"

2.png

2. 编码转换

当集合同时满足以下两个条件时,使用 intset 编码:

1、集合对象中所有元素都是整数

2、集合对象所有元素数量不超过512

不能满足这两个条件的就使用 hashtable 编码。第二个条件可以通过配置文件的 set-max-intset-entries 进行配置。


有序集合对象:

和上面的集合对象相比,有序集合对象是有序的。与列表使用索引下标作为排序依据不同,有序集合为每个元素设置一个分数(score)作为排序依据.

1. 编码

有序集合的编码可以是 ziplist 或者 skiplist。

ziplist 编码的有序集合对象使用压缩列表作为底层实现,每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员,第二个节点保存元素的分值。并且压缩列表内的集合元素按分值从小到大的顺序进行排列,小的放置在靠近表头的位置,大的放置在靠近表尾的位置.

3.png

skiplist 编码的有序集合对象使用 zet 结构作为底层实现,一个 zset 结构同时包含一个字典和一个跳跃表:

字典的键保存元素的值,字典的值则保存元素的分值;跳跃表节点的 object 属性保存元素的成员,跳跃表节点的 score 属性保存元素的分值。

这两种数据结构会通过指针来共享相同元素的成员和分值,所以不会产生重复成员和分值,造成内存的浪费。

说明:其实有序集合单独使用字典或跳跃表其中一种数据结构都可以实现,但是这里使用两种数据结构组合起来,原因是假如我们单独使用 字典,虽然能以 O(1) 的时间复杂度查找成员的分值,但是因为字典是以无序的方式来保存集合元素,所以每次进行范围操作的时候都要进行排序;假如我们单独使用跳跃表来实现,虽然能执行范围操作,但是查找操作有 O(1)的复杂度变为了O(logN)。因此Redis使用了两种数据结构来共同实现有序集合.


2. 编码转换

当有序集合对象同时满足以下两个条件时,对象使用 ziplist 编码:

1、保存的元素数量小于128;

2、保存的所有元素长度都小于64字节。

不能满足上面两个条件的使用 skiplist 编码。以上两个条件也可以通过Redis配置文件zset-max-ziplist-entries 选项和 zset-max-ziplist-value 进行修改。


有所不足的后期阿胜会慢慢优化

近期在准备换工作所以持续断更好长时间了,万分抱歉

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

Powered By Z-BlogPHP 1.5.2 Zero

WX:xcs345525801 QQ:345525801 Tel:19521445850 Email:xcssh868@163.com

Copyright © 2020 许承胜个人博客 版权所有 备案号:皖ICP备18014705号-1