序章
Redis应用的很广,本文不介绍Redis的优劣,尽可能精简的讲述一下Redis的设计与实现,主要参考《redis的设计与实现》一书以及redis5.X源码,本文以Redis的流程为主,底层数据结构、编码、对象等是作为前置知识,不是本文的重点,但是必须知道,所以开设第一章简略讲解下。
一、Redis底层实现与Redis对象
底层数据结构
SDS
Redis底层并不是直接使用C语言的字符数组,而是包装了一个SDS对象来描述字符串呢,可以减少字符串变更时的重新内存分配,预分配内从,同时内部又可以使用C语言自带的字符串处理函数,可以理解成Java里面的包装类。
链表
字典
字典是redis中最重要的数据结构,是kv结构的基础,所有的数据都是存储在字典中。
字典是由字典结构、哈希表、哈希节点组成
这里的哈希表和Java中的HashMap类似,是数组+链表构成,采用链表法解决Hash冲突。哈希节点类似于HashMap中的Node,既是数组的节点,也是链表的节点。
而字典结构则是在字典结构外面包装了一层,对外暴露,那么为什么不直接对外暴露哈希表呢,其中一个原因就是扩容,字典结构内保存了2张哈希表,通过渐进式Rehash来做扩容,简单来说就是两张表,一个时间只用一张,要扩容了就给另一张分配空间,把正在使用的搬过去,然后用另一张。
跳跃表
压缩列表
五种Redis对象
八股文之五种redis对象,但是实际上redis底层统一通过redisObject结构保存的,根据类型区分是五种之中的哪一种,而且存在一个编码的概念,大概意思就是同样是string类型,底层实现并不一定是sds,也可能是int。 同样是Hash对象,底层不一定是字典,也可能是压缩列表。
对象
redis统一由redisObject结构保存。
编码
二、Redis主流程
好了,前面的都不是我想写的,前置的这些知识如果听都没听过,只知道五种对象的话,那看接下来的文章会有些吃力。好好学习不要偷懒。
Reactor模式
所谓readctor模式,又称作反应堆模式,是一种基于基于事件驱动的设计模式,IO多路复用就是基于这个模式的实现,在redis、netty中都有体现。下图就是reactor的一个基本体现,在面对多个客户端的请求时,会有一个或者多个reactor接收事件,进行分派不同的handler进行处理。
Reactor模式实现
上图并不能完全代表reactor模式,在reactor模式下,主要可以分成几个角色,把client当成客户,recotor当成接待员,接待员可能并不清楚某些事情应该有哪个部门负责,也可能知道,所以也有可能存在二级接待员,到了具体的工作部门之后可能只有一个业务员负责,也可能有多业务员处理,就会延伸出单reactor单线程,单reactor多线程,主从reactor等多种复合形式的实现。
上图就是单reactor多线程模式,一般线程池就是这种模式。
netty则是主从reactor模式,netty服务端的初始化会设置两个eventLoopGroup,其中一个group中只有一个eventLoop负责连接应答,作为main reactor并把任务交给一个负责连接应答的handler即acceptor,另一个eventLoopGroup中会有多个eventLoop,每个eventLoop都有一个线程并且有一个多路复用器用来处理多个请求,这里每个eventLoop都是一个sub reactor,并且根据不同的请求,分派给不同的handler处理。netty的设计与实现我会另开一篇进行讲解。
而redis则是单reactor单线程,因为redis主流程是单线程运行的,所以他的设计思路是只有一个多路复用器,可以是select,可以是kqueue,也可以是epoll,作为reactor负责对外接收请求,包括连接请求和读写请求,redis是一个典型的命令模式,所谓的一条命令就是一行字符串,根据提前约定好的规则解析成命令名称和参数列表,根据命令的名称找到命令表中对应的命令以及命令对应的函数进行处理。
这里可以理解成在redis中所有人都是一个大部门,多路复用器作为接待员接收到请求之后,首先看情况交给了负责连接的业务员,或者读写的业务员,负责连接的业务员知道该怎么处理就自己处理了,但是读写的请求类型比较多,这个业务员根据工作分配,把事情交给其他业务员(具体命令)处理。
多路复用
多路复用是一种IO模型,在一个线程中处理多个IO请求,其实现需要依赖操作系统提供的api,如select、kqueue、epoll等。程序无论是发生于不同主机建立socket的网络IO还是与本地的外部设备如磁盘的IO,都会在操作系统中以文件的形式体现出来,建一个socket在文件系统里会有文件体现,往控制台上输出一段话会有文件体现,linux中一切都是文件。所以读和写的行为都会存在文件,而在程序中会用一个文件描述符作为索引去代表这个文件,而多路复用则是在内核中创建一个多路复用器,如epoll会在内核中创建一个一个文件代表epoll文件系统,将不同socket的套接字文件描述符与这个epoll文件系统做关联(放在一颗红黑树上),并向内核注册回调函数,交给内核来监视指定文件的状态变化,待对应事件发生则触发cpu中断,将对应的文件描述符放入待就绪列表中,返回给程序。
IO多路复用的本质就是将原本需要监视多个文件描述符的行为,转变为只监视对应多路复用器的文件描述符,将具体的监视每个IO事件的任务交给内核去处理。这里的内容会比较多,需要自行学习本文不做过多赘述,推荐《鸟哥的Linux私房菜》。下图描述一下epoll的实现。
事件循环
好了,前面的几章都不是本文的重点,现在开始是redis的主流程梳理。先看一段伪代码
main() {
// 初始化各项配置
init();
while(true) {
// 文件事件
fileEvent();
// 时间事件
timeEvnet();
}
// 退出
exit();
}
上面伪代码就是基本redis的主流程,而其中重中之重就是while循环,这个while循环被称作事件循环,只要while不退出,程序就会不断交替的执行文件事件和时间事件。这里的两种事件就是redis的主要功能做。
文件事件
前文说到,一切皆文件,即一切读写请求和连接请求都会体现在文件上,这里指的就是具体的文件可读、可写、可连接的事情发生了,这就是文件事件。redis里的文件事件应用了IO多路复用模型,如epoll,监视epoll对应的文件描述符,等待客户端的连接或者读写就绪,并执行对应的函数。
连接事件
当有客户端请求连接,则会在redis中创建客户端实例,并向多路复用器注册读事件
读事件
当客户端向服务端发送命令,接收后放入客户端实例的读取缓冲区,之后服务端获取之后会对命令文本进行解析,从命令表中查找指定的命令处理函数去执行。
如get name zxz会被解析成 命令get 参数["name","zxz"]
命令表
执行完命令之后会将返回结果放入服务端中客户端实例内的
输出缓冲区。
写事件
写事件事件主要用于服务端向客户端发送返回(实际上大部分返回并不会走多路复用器),或者redis实例之间的通信。
时间事件
redis服务端保存了很多全局数据,用于记录服务端状态,包括持久化涉及到的一些时间、命令执行的次数、内存的使用等等...