一,事件
Redis是一个事件驱动程序(也就是,请求才会执行操作)那么主要产生的事件是什么呢?下面的两个。
一,文件事件
服务器和客户端连接交互产生的事件,Redis基于Readcot模式开发了自己的网络事件处理器,使用IO使用多路复用,同时监听多个套接字,也就是连接,绑定事件处理器,当事件到达的时候,调用对应的事件处理器。
1.文件事件处理器的构成:四个部分:套接字,IO多路复用程序,文件事件派发,事件处理器(可以理解为socket,selector,dispatcher,handler)
![](https://i-blog.csdnimg.cn/blog_migrate/0978c814b00e739d9546ca4e920d46e4.png)
我们可以确定的一点,在多路复用中,对连接事件的读取,肯定是单线程的,然后,多个事件并发到达,它不是使用多线程来处理的,而是通过一个队列存放,一个handler线程来处理所有的事件
问题:单线程如何保证高并发?
答案:1.cpu不说redis的瓶颈,内存大小or网络宽带才是瓶颈。内存速度也很快。
2.不需要锁的消耗,不需要线程切换的消耗。
3.单线程绷不住了,使用集群的方式解决,使用多线程也也解决不了问题。(多进程)
4.多路复用技术,事件不会发生等待阻塞。
5.Hash结构,存储读取速度也比较快。
参考链接:https://www.jianshu.com/p/0bf46dd0aaf5
2.事件的类型:请求连接,关闭,可读,可写事件,同一个socket产生两个事件的时候,优先考虑可读事件。
3.文件事件的处理器:
- 连接应答处理器:acceptTcphandler函数,在redis初始化的时候,要把这个处理器绑定到accept事件中。
- 命令请求处理器:readQueryFromClient,包装read,负责套接字中读入客户端发送的命令,
- 命令回复处理器:sendReplyToClient,包装是write,服务器向客户端发送命令回复。
(注意这的read和write不是针对数据库的,而是针对服务端套接字的,c发送过来s就去读,s发送给c就是写)
![](https://i-blog.csdnimg.cn/blog_migrate/2db243c3d5431864b36070e55f0e2740.png)
二,时间事件
分类:
1.定时事件:在指定的事件执行,比如30秒之后。
2.周期性事件:隔一段事件执行一次,比如30秒执行一次(两种没什么好区分的,根据业务玩就是了)
时间事件的三个属性组成:
id:服务器为时间事件创建的全局唯一ID,递增。
when:unix时间戳。记录了事件到达的时间。(定时持久化)
timeProc:时间事件处理器,一个函数(和上面的文件处理器一个道理,知不道文件处理器是绑定事件,时间处理器是绑定时间)
实现:服务器将所有的时间都保存在一个无序链表中(看来就是一个结构体指针),时间执行器遍历观察它的时间是否已经到达,然后执行(这个和键的过期删除极其相似),这个周期时间和定时时间就看函数的返回值,定时的返回值收到就会进行删除这个事件。这里的无序不是值ID乱序,而是指when时间的乱序的,所以执行的时候要进行遍历。
三,事件的调度与执行
因为redis是单线程的,而且文件事件和时间事件是同时存在的,所以需要调度配合,做好处理。
整体思路:计算时间到达的时间事件,然后保存起来,在规定的阻塞事件中,等待文件事件的产生,然后执行在这个范围内的文件事件,处理完毕再处理时间事件(那么这里你就会发现,时间事件并不是准时执行的,还需要等待调度)
![](https://i-blog.csdnimg.cn/blog_migrate/0978c814b00e739d9546ca4e920d46e4.png)
这里有一个问题,文件事件的处理不是派发的吗,为什么又变成了轮询了,这里是放在队列里面,进行等待。这里就有阻塞了。
二,客户端
Redis是典型的一对多服务器,一个服务器可以和多个客户端相连,多个socket这个可以理解。 服务器使用一个链表来保存客户端,clients:redisClient->redisClient。一个节点就是一个客户但。
1.属性
-
套接字描述符:int fd;根据客户端的不同值不同,伪客户端fd=-1,普通客户端>-1,相当于id吧。
-
名字:默认的客户端没有名字,但是可以设置名字
-
标志:记录客户端的角色,int flags;比如主从复制里面,有主服务器的客户端,从服务客户端。还有很多标识,比如正在阻塞等等。
-
输入缓冲区:用于保存客户端发送的命令,sds querybuf;输入缓冲区会根据输入的内容动态缩小或者扩大,最大大小不能超过1G,否者会关闭这个客户端。
-
命令和命令参数:(我们根据socket通信知道,接收端的socket在用户那里有一份,在服务器接通了之后,也会有一份)当服务器收到命令之后,会进行解析,然后把解析及结果:命令参数和命令参数的个数存入客户端的argv属性中和argc属性中,
-
命令的实现函数:服务之根据argv[0]的值,在命令表中查找对应的命令实现函数(也就是处理函数),这个命令表就是一个字典,键就是字符串命令,值是redisCommand结构,它存放了对应的处理函数。找到了这个结构,redisClient里面有一个属性就会保存这个结构的指针,方便调用。
-
输出缓冲区:每个客户端有两个缓冲区,一个是大小固定的,另外一个是可变的,固定的保存比较小的回复,反知,可变的保存比较大的回复。固定的实现是一个char,这里没有使用sds,可变的是用链表实现的。
-
身份验证:int authenticated 值为0,标识未通过验证,命令会被拒绝,值为1就正常。
-
时间:记录连接时间,最后一次交互时间,
2.客户端的创建和关闭
创建普通客户端:客户端连接到服务器的时候,服务器监听到这个连接事件,就会调用连接事件处理器,为客户端创建相应的客户端状态,并计入到服务器状态结构clients链表的尾部。
关闭客户端:关闭的原因有多种,进程退出,被杀死,客户端发送了非法命令,客户端主动关闭,时间到达关闭,缓冲区溢出,特别要注意缓冲区的大小是否装得下内容。
三,服务端
1.命令请求的执行过程
发送命令请求:用户输入一个请求,户端将请求转换为协议,然后发送给服务器;demo:输入 set key value 客户端转换为:
读取命令请求:服务器收到命令,保存到客户端状态 的输入缓冲区中,对输入缓冲区的命令进行分析,提取出命令请求中包含的命令参数,以及参数个数u,保存到客户端状态的argv,argc中。调用命令执行器。
命令执行器:
-
查找命令实现:根据agrv[0]找到命令的实现函数,将指针转给 客户端状态的cmd属性。
-
执行预备操作:检查之前的操作是否出错(cmd是否为空,参数个数是否完整,身份验证是否Ok,订阅功能如果开启是否正常,服务器是否在载入数据,是否阻塞,是否执行事务)如果是集群版本的,预备操作更加的复杂。
-
调用命令的实现函数:调用函数,产生相应的回复,把回复保存到客户端状态的输出缓冲区中,
-
执行后续工作:如果开启了慢查询,添加日志。如果命令耗费时间长,则需要更新计数器。如果开启了AOF,则需要切入aof缓冲区。
将命令回复发送给客户端:回复以及到了输出缓冲区,关联回复处理器,将客户端套接字改为可写状态,执行回复处理器。
客户端接收并打印命令回复:客户端收到回复,进行协议转换,展示给用户
2.serverCron函数
更新服务器时间缓冲:避免多次调用系统时间函数,保存了系统时间,有秒级别和毫秒级别的时间戳。
更新LRU时钟:也是时间缓冲的一种
更新服务器每秒执行命令次数:抽样计算的方式,估算并记录服务器最近一秒钟处理的命令请求数量
更新服务器内存峰值记录:记录最大的内存使用大小。
处理SIGTERM信号
管理客户端资源
管理数据库资源
执行被延迟的bg rewrite aof
检查持久化操作的运行状态
将AOF缓冲区的内容写入AOF文件
关闭异步客户端
增加crionloops计数器的值
3.初始化服务器
初始化服务器状态结构:创建redisServer结构,作为服务器的实例,设置默认值,初始化initServerConfig函数()完成,包含了运行id,配置文件路径,端口号,持久化方式,初始化LRU时钟,创建命令表。
载入配置选项
初始化服务器数据结构
还原数据库状态
执行事件循环:这里就正常工作了。