Redis的通信协议
Redis是单进程单线程的,应用系统和Redis通过Redis协议(RESP)进行交互。
- 请求响应模式
Redis协议位于TCP层之上,即客户端和Redis实例保持双工的连接。

串行的请求响应模式(ping-pong)
串行化是最简单模式,客户端与服务器端建立长连接,连接通过心跳机制检测(ping-pong) ack应答,客户端发送请求,服务端响应,客户端收到响应后,再发起第二个请求,服务器端再响应。

telnet和redis-cli 发出的命令 都属于该种模式
特点:有问有答,耗时在网络传输命令,性能较低
双工的请求响应模式(pipeline)
批量请求,批量响应,请求响应交叉进行,不会混淆(TCP双工),pipeline。
pipeline的作用是将一批命令进行打包,然后发送给服务器,服务器执行完按顺序打包返回;
通过pipeline,一次pipeline(n条命令)=一次网络时间 + n次命令时间。

请求数据格式
Redis客户端与服务器交互采用序列化协议(RESP),请求以字符串数组的形式来表示要执行命令的参数。用命令特有(command-specific)数据类型作为回复。通信协议的特点
客户端和服务器通过 TCP 连接来进行数据交互, 服务器默认的端口号为 6379 。
客户端和服务器发送的命令或数据一律以 rn (CRLF)结尾。
在这个协议中, 所有发送至 Redis 服务器的参数都是二进制安全(binary safe)的。
简单,高效,易读。规范格式
1、间隔符号,在Linux下是rn,在Windows下是n;
2、简单字符串 Simple Strings, 以 "+"加号 开头;
3、错误 Errors, 以"-"减号 开头;
4、整数型 Integer, 以 ":" 冒号开头;
5、大字符串类型 Bulk Strings, 以 "$"美元符号开头,长度限制512M;
6、数组类型 Arrays,以 "*"星号开头。



命令处理流程

Server启动时监听socket
启动调用 initServer方法:
创建eventLoop(事件机制)
注册时间事件处理器
注册文件事件(socket)处理器
监听 socket 建立连接建立Client
redis-cli建立socket;
redis-server为每个连接(socket)创建一个 Client 对象;
创建文件事件监听socket;
指定事件处理函数。读取socket数据到输入缓冲区:从client中读取客户端的查询缓冲区内容。解析获取命令
将输入缓冲区中的数据解析成对应的命令;
判断是单条命令还是多条命令并调用相应的解析器解析。执行命令:解析成功后调用processCommand 方法执行命令,大致分为
调用 lookupCommand 方法获得对应的 redisCommand;
检测当前 Redis 是否可以执行该命令;
调用 call 方法真正执行命令;

协议响应格式状态回复:回复的第一个字节是“+”,如:"+OK";错误回复:回复的第一个字节是“ - ”,如:-ERR unknown command 'abc';整数回复:回复的第一个字节是“ :”,如:":6";批量回复:回复的第一个字节是“$”,如:"$6 foobar"多条批量回复:对于多条批量回复(数组),回复的第一个字节是“*”,如:"*3"
协议解析及处理:包括协议解析、调用命令、返回结果。协议解析
用户在Redis客户端键入命令后,Redis-cli会把命令转化为RESP协议格式,然后发送给服务器。服务器再对协议进行解析,具体步骤如下:
1. 解析命令请求参数数量:命令请求参数数量的协议格式为"*Nrn" ,其中N就是数量,首字符必须是“*”,使用"r"定位到行尾,之间的数就是参数数量了。如:set name:1 zhaoyun,在aof中看到的协议内容如下:

2. 循环解析请求参数:首字符必须是"$",使用"/r"定位到行尾,之间的数是参数的长度,从/n后到下一个"$"之间就是参数的值了,循环解析直到没有"$"。
协议执行
协议的执行包括命令的调用和返回结果,首先判断参数个数和取出的参数是否一致,RedisServer解析完命令后,会调用函数processCommand处理该命令请求quit校验:如果是“quit”命令,直接返回并关闭客户端;命令语法校验:执行lookupCommand,查找命令(set),如果不存在则返回:“unknowncommand”错误;参数数目校验:参数数目和解析出来的参数个数要匹配,如果不匹配则返回:“wrong number ofarguments”错误;
此外还有权限校验,最大内存校验,集群校验,持久化校验等等。
校验成功后,会调用call函数执行命令,并记录命令执行时间和调用次数。执行命令后返回结果的类型不同则协议格式也不同,分为5类:状态回复、错误回复、整数回复、批量回复、多条批量回复。
事件处理机制
Redis服务器是典型的事件驱动系统,主要分为文件事件和时间事件。
- 文件事件文件事件即Socket的读写事件,也就是IO事件,主要包括:客户端的连接、命令请求、数据回复、连接断开。
socket:套接字(socket)是一个抽象层,应用程序可以通过它发送或接收数据。Reactor:事件处理机制采用单线程的Reactor模式,属于I/O多路复用的一种常见模式。IO多路复用( I/O multiplexing )指的通过单个线程管理多个Socket。Reactor pattern(反应器设计模式)是一种为处理并发服务请求,并将请求提交到 一个或者多个服务处理程序的事件设计模式,该模式是基于事件驱动的。


Handle:I/O操作的基本文件句柄,在linux下就是fd(文件描述符);Synchronous Event Demultiplexer :同步事件分离器,阻塞等待Handles中的事件发生。(系统); Reactor: 事件分派器,负责事件的注册,删除以及对所有注册到事件分派器的事件进行监控, 当事件发生时会调用Event Handler接口来处理事件;Event Handler: 事件处理器接口,这里需要Concrete Event Handler来实现该接口; Concrete Event Handler:真实的事件处理器,通常都是绑定了一个handle,实现对可读事件 进行读取或对可写事件进行写入的操作。

处理流程
主程序向事件分派器(Reactor)注册要监听的事件;
Reactor调用OS提供的事件处理分离器,监听事件(wait);
当有事件产生时,Reactor将事件派给相应的处理器来处理 handle_event()。
4种IO多路复用模型与选择I/O多路复用就是通过一种机制,一个进程可以监视多个socket,一旦某个socket就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。
select,poll,epoll、kqueue都是IO多路复用的机制。select

select 函数监视的文件描述符分3类,分别是:writefds,readfds,exceptfds。调用后select函数会阻塞,直到有描述符就绪(有数据 可读、可写、或者有except),或者超时(timeout指定等待时间,如果立即返回设为null即可),函数返回。当select函数返回后,可以通过遍历fd列表,来找到就绪的描述符。优点:跨平台缺点:单个进程打开的文件描述是有一定限制的,它由FD_SETSIZE设置,默认值是1024,采用数组存储。另外在检查数组中是否有文件描述需要读写时,采用的是线性扫描的方法,即不管这些socket是不是活跃的,都轮询一遍,所以效率比较低。poll

poll使用一个 pollfd的指针实现,pollfd结构包含了要监视的event和发生的event,不再使用select“参数-值”传递的方式。优点:采用链表的形式存储,监听的描述符数量没有限制,可以超过select默认限制的1024大小缺点:在检查链表中是否有文件描述需要读写时,采用的是线性扫描的方法,即不管这些socket是不是活跃的,都轮询一遍,所以效率比较低。epoll
相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。

创建一个epoll的句柄。自从linux2.6.8之后,size参数是被忽略的。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。

epoll的事件注册函数不同于select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。
参数说明:
第一个参数是epoll_create()的返回值。
第二个参数表示动作:
EPOLL_CTL_ADD:注册新的fd到epfd中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:从epfd中删除一个fd;
第三个参数是需要监听的fd。
第四个参数是告诉内核需要监听什么事。

等待内核返回的可读写事件,最多返回maxevents个事件。优点:epoll 没有最大并发连接的限制,上限是最大可以打开文件的数目,举个例子,在1GB内存的机器上大约是10万左 右效率提升, epoll 最大的优点就在于它只管你“活跃”的连接 ,而跟连接总数无关,因此在实际的网络环境 中, epoll 的效率就会远远高于 select 和 poll 。epoll使用了共享内存,不用做内存拷贝kqueue
kqueue 是 unix 下的一个IO多路复用库。最初是2000年Jonathan Lemon在FreeBSD系统上开发的一个高性能的事件通知接口。注册一批socket描述符到 kqueue 以后,当其中的描述符状态发生变化时,kqueue 将一次性通知应用程序哪些描述符可读、可写或出错了。优点:能处理大量数据,性能较高

文件事件分派器
在redis中,对于文件事件的处理采用了Reactor模型。采用的是epoll的实现方式。

Redis在主循环中统一处理文件事件和时间事件,信号事件则由专门的handler来处理。

事件处理器
连接处理函数 acceptTCPHandler:当客户端向 Redis 建立 socket时,aeEventLoop 会调用 acceptTcpHandler 处理函数,服务器会为每个链接创建一个 Client 对象,并创建相应文件事件来监听socket的可读事件,并指定事件处理函数。

请求处理函数 readQueryFromClient:当客户端通过 socket 发送来数据后,Redis 会调用 readQueryFromClient 方法,readQueryFromClient方法会调用 read 方法从 socket 中读取数据到输入缓冲区中,然后判断其大小是否大于系统设置的client_max_querybuf_len,如果大于,则向 Redis返回错误信息,并关闭 client。

命令回复处理器 sendReplyToClient:sendReplyToClient函数是Redis的命令回复处理器,这个处理器负责将服务器执行命令后得到的命令回复通过套接字返回给客户端。
1、将outbuf内容写入到套接字描述符并传输到客户端;
2、aeDeleteFileEvent 用于删除 文件写事件
时间事件
时间事件分为定时事件与周期事件。

一个时间事件主要由以下三个属性组成:
id(全局唯一id);
when (毫秒时间戳,记录了时间事件的到达时间);
timeProc(时间事件处理器,当时间到达时,Redis就会调用相应的处理器来处理事件);
serverCron
时间事件的最主要的应用是在redis服务器需要对自身的资源与配置进行定期的调整,从而确保服务器的长久运行,这些操作由redis.c中的serverCron函数实现。该时间事件主要进行以下操作:
1)更新redis服务器各类统计信息,包括时间、内存占用、数据库占用等情况;
2)清理数据库中的过期键值对;
3)关闭和清理连接失败的客户端;
4)尝试进行aof和rdb持久化操作;
5)如果服务器是主服务器,会定期将数据向从服务器做同步操作;
6)如果处于集群模式,对集群定期进行同步与连接测试操作;
redis服务器开启后,就会周期性执行此函数,直到redis服务器关闭为止。默认每秒执行10次,平均100毫秒执行一次,可以在redis配置文件的hz选项,调整该函数每秒执行的次数。server.hz:serverCron在一秒内执行的次数 , 在redis/conf中可以配置。run_with_period:定时任务执行都是在10毫秒的基础上定时处理自己的任务(run_with_period(ms)),即调用run_with_period(ms)[ms是指多长时间执行一次,单位是毫秒]来确定自己是否需要执行。返回1表示执行。
假如有一些任务需要每500ms执行一次,就可以在serverCron中用run_with_period(500)把每500ms需要执行一次的工作控制起来。定时事件:让一段程序在指定的时间之后执行一次。aeTimeProc(时间处理器)的返回值是AE_NOMORE该事件在达到后删除,之后不会再重复。周期性事件:让一段程序每隔指定时间就执行一次。
aeTimeProc(时间处理器)的返回值不是AE_NOMORE,当一个时间事件到达后,服务器会根据时间处理器的返回值,对时间事件的 when 属性进行更新,让这个事件在一段时间后再次达到。serverCron就是一个典型的周期性事件。aeEventLoop
aeEventLoop 是整个事件驱动的核心,Redis自己的事件处理机制。它管理着文件事件表和时间事件列表,不断地循环处理着就绪的文件事件和到期的时间事件。


初始化
Redis 服务端在其初始化函数 initServer 中,会创建事件管理器aeEventLoop 对象。
函数aeCreateEventLoop 将创建一个事件管理器,主要是初始化 aeEventLoop 的各个属性值,比如events 、fired 、timeEventHead 和apidata
首先创建aeEventLoop 对象;
初始化注册的文件事件表、就绪文件事件表。events 指针指向注册的文件事件表、fired 指针指向就绪文件事件表。表的内容在后面添加具体事件时进行初变更;
初始化时间事件列表,设置timeEventHead 和timeEventNextId 属性;
调用aeApiCreate 函数创建epoll 实例,并初始化 apidata。stop:停止标志,1表示停止,初始化为0。
文件事件: events, fired, apidata。
aeFileEvent 结构体为已经注册并需要监听的事件的结构体。

aeFiredEvent:已就绪的文件事件

void *apidata:在ae创建的时候,会被赋值为aeApiState结构

这个结构体是为了epoll所准备的数据结构。redis可以选择不同的io多路复用方法。因此 apidata 是个void类型,根据不同的io多路复用库来选择不同的实现。ae.c里面使用如下的方式来决定系统使用的机制:

时间事件: timeEventHead, beforesleep, aftersleep
aeTimeEvent结构体为时间事件,Redis 将所有时间事件都放在一个无序链表中,每次 Redis 会遍历整个链表,查找所有已经到达的时间事件,并且调用相应的事件处理器

beforesleep 对象是一个回调函数,在 redis-server 初始化时已经设置好了。功能:
检测集群状态;
随机释放已过期的键;
在数据同步复制阶段取消客户端的阻塞;
处理输入数据,并且同步副本信息;
处理非阻塞的客户端请求;
AOF持久化存储策略,类似于mysql的bin log;
使用挂起的输出缓冲区处理写入;
aftersleep对象是一个回调函数,在IO多路复用与IO事件处理之间被调用aeMain:aeMain 函数其实就是一个封装的 while 循环,循环中的代码会一直运行直到 eventLoop 的 stop 被设置为1(true)。它会不停尝试调用 aeProcessEvents 对可能存在的多种事件进行处理,而aeProcessEvents 就是实际用于处理事件的函数。

aemain函数中,首先调用Beforesleep。这个方法在Redis每次进入sleep/wait去等待监听的端口发生I/O事件之前被调用。当有事件发生时,调用aeProcessEvent进行处理。aeProcessEvent
首先计算距离当前时间最近的时间事件,以此计算一个超时时间;
然后调用 aeApiPoll 函数去等待底层的I/O多路复用事件就绪;
aeApiPoll 函数返回之后,会处理所有已经产生文件事件和已经达到的时间事件。


aeSearchNearestTimer:计算最早时间事件的执行时间,获取文件时间可执行时间。aeProcessEvents 都会先 计算最近的时间事件发生所需要等待的时间 ,然后调用aeApiPoll 方法在这段时间中等待事件的发生,在这段时间中如果发生了文件事件,就会优先处理文件事件,否则就会一直等待,直到最近的时间事件需要触发。堵塞等待文件事件产生:aeApiPoll 用到了epoll,select,kqueue和evport四种实现方式。处理文件事件:rfileProc (处理读事件)和 wfileProc(处理写事件) 就是在文件事件被创建时传入的函数指针处理时间事件:processTimeEvents取得当前时间,循环时间事件链表,如果当前时间>=预订执行时间,则执行时间处理函数。