redis模型(1):单线程模型

1、文件事件处理器

Redis基于Reactor模式开发了自己的网络事件处理器,被称为文件事件处理器,由套接字、I/O多路复用程序、文件事件分派器(dispatcher),事件处理器四部分组成。

 I/O多路复用程序会同时监听多个套接字,当被监听的套接字准备好执行accept、read、write、close等操作时,与操作相对应的文件事件就会产生,I/O多路复用程序会将所有产生事件的套接字都压入一个队列,然后以有序地每次仅一个套接字的方式传送给文件事件分派器,文件事件分派器接收到套接字后会根据套接字产生的事件类型调用对应的事件处理器。

  • 注意1:只有当上一个套接字产生的事件被所关联的事件处理器执行完毕,I/O多路复用程序才会继续向文件事件分派器传送下一个套接字,所以对每个命令的执行时间是有要求的,如果某个命令执行过长,会造成其他命令的阻塞。所以慎用O(n)命令,Redis是面向快速执行场景的数据库。
  • 注意2:命令的并发性。Redis是单线程处理命令,命令会被逐个被执行,假如有3个客户端命令同时执行,执行顺序是不确定的,但能确定不会有两条命令被同时执行,所以两条incr命令无论怎么执行最终结果都是2。

2、事件

服务器需要处理两类事件:

2.1、文件事件

Redis服务器对套接字的操作,当一个套接字准备执行连接、读、写、关闭等操作时就会产生一个文件事件。文件事件分为AE_READABLE和AE_WRITABLE两类。

  • 当套接字变得可读时(客户端对套接字执行write操作,或者执行close操作),或者有新的可应答(acceptable)套接字出现时(客户端对服务器的监听套接字执行connect操作),套接字产生AE_READABLE事件。
  • 当套接字变得可写时(客户端对套接字执行read操作),套接字产生AE_WRITABLE事件。
  • 当一个套接字同时产生了这两种事件,文件事件分派器会优先处理AE_READABLE事件。

2.2、时间事件:

Redis服务器中一些需要在给定时间点执行的操作。

服务器将所有时间事件放在一个无序链表中,每当时间事件执行器运行时,它就会遍历整个链表,查找所有已经到达的时间事件,并调用相应的事件处理器。

Redis服务器一般情况下只执行serverCron函数一个时间事件,通过redis.c/serverCron函数定期对自身的资源和状态进行检查和调整,主要工作包括:

  • 更新服务器的各类统计信息,如时间、内存占用、数据库占用情况等。
  • 清理数据库中的过期键值对。
  • 关闭和清理连接失效的客户端。
  • 尝试进行AOF或RDB持久化操作。
  • 如果服务器是主服务器,那么对从服务器进行定期同步。
  • 如果处于集群模式,对集群进行定期执行同步和连接测试。

Redis2.6版本,服务器默认serverCron每秒运行10次,平均每隔100毫秒运行一次。Redis2.8版本之后,通过hz调整每秒执行次数。

3、文件事件的处理器

3.1、连接应答处理器:

当Redis服务器进行初始化的时候,程序会将这个连接应答处理器和服务器监听套接字的AE_READABLE事件关联起来,当有客户端用sys/socket.h/connect函数连接服务器监听套接字的时候,套接字就会产生AE_READABLE事件,引发连接应答处理器执行,并执行相应的套接字应答操作。

3.2、命令请求处理器:

当一个客户端通过连接应答处理器成功连接到服务器之后,服务器会将客户端套接字的AE_READABLE事件和命令请求处理器关联起来,当客户端向服务器发送命令请求的时候,套接字就会产生AE_READABLE事件,引发命令请求处理器执行,并执行相应的套接字读入操作;

在客户端连接服务器的整个过程中,服务器都会一直为客户端套接字的AE_READABLE事件关联命令请求处理器。

3.3、命令回复处理器:

当服务器有命令回复需要传送给客户端的时候,服务器会将客户端套接字的AE_WRITABLE事件和命令回复处理器关联起来,当客户端准备好接收服务器传回的命令回复时,就会产生AE_WRITABLE事件,引发命令回复处理器执行,并执行相应的套接字写入操作。

当命令发送完毕后,服务器会解除命令回复处理器与客户端套接字的AE_WRITABLE事件之间的关联。

4、事件调度

事件的调度和执行由ae.c/aeProcessEvents函数负责,逻辑如下:

先计算最近的时间事件距离到达还有多少毫秒remaind_ms,根据这个值阻塞并等待文件事件产生,阻塞期间会不断处理出现的文件事件。当时间事件最终到达时,服务器才会开始处理达到的时间事件。

  • 注意:对文件事件和时间事件的处理都是同步、有序、原子地执行的,服务器不会中途中断事件处理,也不会对事件进行抢占,因此,不管是文件事件的处理器,还是时间事件的处理器,它们都会尽可能减少程序的阻塞时间,并在有需要时主动让出执行权。

因为时间事件在文件事件之后执行,并且事件之间不会出现抢占,所以时间事件的实际处理时间通常会比设定的到达时间晚一些。

如图,在时间事件到达前(100ms),服务器已经等待并处理了两次文件事件,又因为处理事件的过程中不会出现抢占,所以实际处理时间事件的时间比预计慢了30毫秒。

5、客户端与redis通信的一次流程

(1)假设一个Redis服务器正在运作,那么这个服务器的监听套接字的 AE_READABLE 事件应该正处于监听状态之下, 而该事件所对应的处理器为连接应答处理器。
(2)如果这时有一个Redis客户端向服务器发起连接,那么监听套接字将产生 AE_READABLE事件,触发连接应答处理器执行。处理器会对客户端的连接请求进行应答,然后创建客户端套接字,以及客户端状态,并将客户端套接字的 AE_READABLE事件与命令请求处理器进行关联,使得客户端可以向主服务器发送命令请求。
(3)之后,假设客户端向主服务器发送一个命令请求,那么客户端套接字将产生 AE_READABLE 事件,引发命令请求处理器执行,处理器读取客户端的命令内容,然后传给相关程序去执行。
(4)执行命令将产生相应的命令回复, 为了将这些命令回复传送回客户端, 服务器会将客户端套接字的 AE_WRITABLE 事件与命令回复处理器进行关联。当客户端尝试读取命令回复的时候, 客户端套接字将产生 AE_WRITABLE 事件, 触发命令回复处理器执行, 当命令回复处理器将命令回复全部写入到套接字之后, 服务器就会解除客户端套接字的 AE_WRITABLE 事件与命令回复处理器之间的关联。

6、执行命令请求过程

对于每个与服务器进行连接的客户端,服务器都为这些客户端建立了相应的redis.h/redisClient结构(客户端状态)。

Redis服务器状态结构的clients属性是一个链表,这个链表保存了所有与服务器连接的客户端的状态结构。对客户端执行批量操作,或者查找某个指定的客户端,都可以通过遍历clients链表来完成。

//客户端状态的关键属性:
typedef struct redisClient {
    //……
    sds querybuf;
    robj **argv;
    int argc;
    struct redisCommand *cmd;
    char buf[REDIS_REPLY_CHUNK_BYTES];
    int bufpos;
    list *reply;
    //……
} redisClient;

一个命令请求从发送到获得回复的过程:

(1)当客户端使用connect函数连接到服务器时,服务器就会调用连接事件处理器,为客户端创建相应的客户端状态,并将这个新的客户端状态添加到服务器状态结构clients链表的末尾。
(2)当用户在客户端中键入一个命令请求时,客户端会将这个命令请求转换成协议格式,然后通过连接到服务器的套接字,将协议格式的命令请求发送给服务器。

 

 

(3)当客户端与服务器之间的连接套接字因为客户端的写入而变得可读时,服务器将调用命令请求处理器来执行以下操作:

  • 读取套接字中协议格式的命令请求,并将其保存到客户端状态的输入缓冲区querybuf
  • 对输入缓冲区中的命令请求进行分析,提取出命令请求中包含的命令参数,以及命令参数的个数,然后分别将参数和参数个数保存到客户端状态的argvargc属性。
  • 调用命令执行器,执行客户端指定的命令。

(4)命令执行器要做的第一件事就是根据客户端状态的argv[0]参数,在命令表中查找参数所指定的命令,并将找到的命令保存到客户端状态的cmd属性中。

(5)到目前为止,服务器已经将执行命令所需的命令实现函数(cmd)、参数(argv)、参数个数(argc)都收集齐了,但在真正执行命令前,程序还需要进行一些预备操作,从而确保命令可以正确、顺利地被执行:检查客户端状态的cmd指针是否执行NULL、检查命令请求所给定的参数个数是否正确、检查客户端是否已经通过了身份验证...

(6)被调用的命令实现函数会执行指定的操作,并产生相应的命令回复,这些回复会被保存在客户端状态的输出缓冲区中(buf属性和reply属性),之后实现函数还会为客户端的套接字关联命令回复处理器,这个处理器负责将命令回复返回给客户端。

(7)在执行完实现函数之后,服务器还需要执行一些后续工作:

  • 如果服务器开启了慢查询日志功能,那么慢查询日志模块会检查是否需要为刚刚执行完的命令请求添加一条新的慢查询日志。
  • 根据刚刚执行命令所耗费的时长,更新被执行命令的redisCommand结构的milliseconds属性,并将命令的redisCommand结构的calls计数器的值加1。
  • 如果服务器开启AOF持久化功能,那么AOF持久化模块会将刚刚执行的命令请求写入到AOF缓冲区。
  • 如果有其他服务器正在复制当前这个服务器,那么服务器会将刚刚执行的命令传播给所有从服务器。

当以上操作都执行完之后,服务器对当前命令的执行到此告一段落,之后服务器就可以继续从文件事件处理器中取出并处理下一个命令请求了。

(8)当客户端套接字变为可写状态时,服务器就会执行命令回复处理器,将保存在客户端输出缓冲区中的命令回复发送给客户端。当命令回复发送完毕之后,回复处理器会清空客户端状态的输出缓冲区,为处理下一个命令请求做好准备。

(9)当客户端接收到协议格式的命令回复之后,它会将这些回复转换成人们可以识别的可读的格式。

 

 

补充:

  • querybuf最大不能超过1GB。
  • argv属性是一个数组,其中argv[0]是要执行的命令,而之后的其他项则是传给命令的参数。argc属性负责记录argv数组的长度。
  • 命令表是一个字典,字典的键是一个个命令名字,比如"set"、"get"、"del"等等;而字典的值则是一个个redisCommand结构体,每个redisCommand结构体记录了一个Redis命令的实现信息,包括命令的实现函数、命令的标志、命令应该给定的参数个数、命令的总执行次数和总消耗时长等统计信息。命令表使用的时大小写无关的查找算法。
  • buf是一个大小为REDIS_REPLY_CHUNK_BYTES字节的字节数组,而bufpos属性则记录了buf数组目前已使用的字节数量。REDIS_REPLY_CHUNK_BYTES常量目前的默认值为16*1024,也即是说,buf数组的默认大小为16KB。当buf数组的空间已经用完,或者回复因为太大而无法装进buf数组里面,服务器就会开始使用可变大小缓冲区reply,可变大小缓冲区是链表结构。
  • 服务器处理完客户端的命令请求后,命令回复只是暂时缓存在client结构体的buf缓冲区,待客户端的可写事件发生时,才会真正往客户端发送命令回复。

7、Redis 通信协议 RESP

Redis 的作者认为数据库系统的瓶颈一般不在于网络流量,而是数据库自身内部逻辑处理上。所以即使 Redis 使用了浪费流量的文本协议,依然可以取得极高的访问性能。Redis 将所有数据都放在内存,用一个单线程对外提供服务,单个节点在跑满一个 CPU核心的情况下可以达到 10w/s 的超高 QPS。

Redis 协议将传输的结构数据分为 5 种最小单元类型,单元结束时统一加上回车换行符号 \r\n。

(1)单行字符串以 + 符号开头。
(2)多行字符串以 $ 符号开头,后跟字符串长度。
(3)整数值以 : 符号开头,后跟整数的字符串形式。
(4)错误消息以 - 符号开头。
(5)数组以 * 号开头,后跟数组的长度。

 

客户端向服务器发送的指令只有一种格式,多行字符串数组。比如一个简单的 set 指令set author codehole会被序列化成下面的字符串。

*3\r\n$3\r\nset\r\n$6\r\nauthor\r\n$8\r\ncodehole\r\n

//以更容易阅读的格式
*3
$3
set
$6
author
$8
codehole

服务器向客户端回复的响应要支持多种数据结构,所以消息响应在结构上要复杂不少。不过再复杂的响应消息也是以上5种基本类型的组合。

127.0.0.1:6379> scan 0
1) "0"
2) 1) "info"
   2) "books"
   3) "author"

//转换为协议
*2
$1
0
*3
$4
info
$5
books
$6
author

 8、初始化服务器

(1)初始化服务器状态
      初始化服务器的第一步就是创建一个struct redisServer类型的实例变量server作为服务器的状态,并为结构中的各个属性设置默认值。由redis.c/initServerConfig函数完成。

(2)载入服务器配置
      服务器在用initServerConfig函数初始化完server变量之后,就会开始载入用户给定的配置参数和配置文件,并根据用户设定的配置,对server变量相关属性的值进行修改。

  • 如果用户为这些属性的相应选项指定了新的值,那么服务器就使用用户指定的值来更新相应的属性。
  • 如果用户没有为属性的相应选项设置新值,那么服务器就沿用为initServerConfig函数为属性设置的默认值。

(3)初始化服务器数据结构
      调用initServer函数初始化数据结构。打开服务器的监听端口,并为监听套接字关联连接应答事件处理器,等待服务器正式运行时接受客户端的连接。为serverCron函数创建时间事件,等待服务器正式运行时执行serverCron函数。如果AOF持久化功能已经打开,那么打开现有的AOF文件,如果AOF文件不存在,那么创建并打开一个新的AOF文件,为AOF写入做好准备
(4)还原数据库状态
      在完成了对服务器状态server变量的初始化之后,服务器需要载入RDB文件或AOF文件,并根据文件记录的内容来还原服务器的数据库状态。

  • 如果服务器启用了AOF持久化功能,那么服务器使用AOF文件来还原数据库状态
  • 相反地,如果服务器没有启用AOF持久化功能,那么服务器使用RDB文件来还原数据库状态

      当服务器完成数据库状态还原工作之后,服务器将在日志中打印出载入文件并还原数据库状态所耗费的时长:

[5244] 21 Nov 22:43:49.084 * DB loaded from disk: 0.068 seconds

(5)执行事件循环

      在初始化最后一步,服务器将打印出日志:

[5244] 21 Nov 22:43:49.084 * The server is now ready to accept connections on port 6379

      并开始执行服务器的事件循环(loop)。至此,服务器的初始化工作圆满完成,服务器现在开始可以接受客户端的连接请求,并处理客户端发来的命令请求了。

 

补充:
当服务器接收到一条命令请求时,需要从命令表中查找命令,而redisCommandTable命令表是一个数组,意味着查询命令的时间复杂度为O(N),效率低下。因此Redis在服务器初始化时,会将redisCommandTable转换为一个字典存储在redisServer对象的commands字段,key为命令名称,value为命令redisCommand对象。

9、单线程还能支持高并发的原因

为什么Redis使用单线程模型会达到每秒万级别的处理能力呢?

第一,纯内存访问,Redis将所有数据放在内存中,内存的响应时长大约为100纳秒,这是Redis达到每秒万级别访问的重要基础。
第二,非阻塞I/O,Redis使用epoll作为I/O多路复用技术的实现,再加上Redis自身的事件处理模型将epoll中的连接、读写、关闭都转换为事件,不在网络I/O上浪费过多的时间。
第三,单线程避免了线程切换和竞态产生的消耗。

猜你喜欢

转载自www.cnblogs.com/zjxiang/p/11736360.html