当前位置: 代码网 > it编程>数据库>Redis > Redis网络I/O模型的使用及说明

Redis网络I/O模型的使用及说明

2025年12月24日 Redis 我要评论
声明:这里的i/o表示redis从网卡读来自客户端的数据(包括指令、数据)一、阻塞i/o阻塞i/o读取数据过程:应用程序(服务器)接收到客户端的三次握手,通过accept()建立连接,得到一个sock

声明:这里的i/o表示redis从网卡读来自客户端的数据(包括指令、数据)

一、阻塞i/o

阻塞i/o读取数据过程:

  • 应用程序(服务器)接收到客户端的三次握手,通过accept()建立连接,得到一个socket文件描述符,用于与客户端通信。
  • 应用程序进程调用read()函数,触发系统调用,进程从用户态切换到内核态,自动执行内核中的系统调用处理函数 sys_read()。
  • 内核态的该进程检查客户端的数据是否已经在内核空间的缓存中(第一次进入内核态肯定不在),如果数据在缓存中,直接拷贝到用户空间,然后切换回用户态。
  • 如果未命中说明客户端发送的数据尚未到达,进程进入阻塞队列让出cpu,持续等待客户端数据。
  • 客户端发起http请求,并携带数据。
  • 客户端发送的网络数据包到达服务器网卡,网卡通过dma将数据包写入内核缓冲区,然后触发硬件中断,抢占cpu,执行中断处理程序
  • 中断处理程序将数据进行网络协议栈的处理(如ip、tcp处理),最终将数据放入对应socket的接收缓冲区,cpu会返回到被中断的地方继续执行,唤醒等待在该socket上的进程,进入就绪态
  • 当该进程被调度器选中再次运行时,它从之前阻塞的地方继续执行,此时数据已经在内核缓存中,于是将数据从内核缓存拷贝到socket的用户空间
  • 系统调用返回,进程从内核态切换回用户态,并继续执行用户态代码。
  • 处理完成后,进程重新进入内核态阻塞。

对于单线程i/o模型,服务器只有一个线程用来监听所有客户端,线程每次调用recvfrom系统调用只能监听一个客户端的数据,如果该客户端没有数据会一直阻塞直到该客户端的数据到达内核缓冲区,无法处理其他客户端早已写入内核缓冲区的数据,性能差。

二、非阻塞i/o

非阻塞i/o相较于阻塞i/o优势在于不需要阻塞在某一个客户端,而是可以轮询所有客户端的状态,当某一客户端的数据可用会调用recvfrom读数据到用户空间,并处理该数据。

非阻塞i/o读取过程:和阻塞i/o的主要区别在步骤4和8

  • 应用程序(服务器)接收到客户端的三次握手,通过accept()建立连接,得到一个socket文件描述符,用于与客户端通信。
  • 应用程序进程调用read()函数,触发系统调用,进程从用户态切换到内核态,自动执行内核中的系统调用处理函数 sys_read()。
  • 内核态的该进程检查客户端的数据是否已经在内核空间的缓存中,如果数据在缓存中,直接拷贝到用户空间,然后切换回用户态。
  • 如果未命中说明客户端发送的数据尚未到达,进程返回用户态并轮询1~4步,此时进程不会阻塞,也不会让出cpu
  • 客户端发起http请求,并携带数据。
  • 客户端发送的网络数据包到达服务器网卡,网卡通过dma将数据包写入内核缓冲区,然后触发硬件中断,抢占cpu,执行中断处理程序
  • 中断处理程序将数据进行网络协议栈的处理(如ip、tcp处理),最终将数据放入对应socket的接收缓冲区,cpu会返回到被中断的地方继续执行,唤醒等待在该socket上的进程,进入就绪态
  • 进程轮询调用read()时,进程从用户态切换到内核态,此时数据已经在内核缓存中,于是将数据从内核缓存拷贝到socket的用户空间
  • 系统调用返回,进程从内核态切换回用户态,并继续执行用户态代码。
  • 处理完成后,进程重新进入内核态阻塞。

非阻塞i/o虽然不需要阻塞等待某一客户端的请求,并且可以同时while轮询监听多个客户端的请求,但是while轮询会查询所有客户端的数据是否到达,线程一直占用cpu,导致cpu利用率低,且轮询所有客户端所以性能也较差。

三、i/o多路复用*

i/o多路复用是利用单个线程同时监听多个文件描述符(每个文件描述符对应一个socket,也就是客户端),也就是说单线程可以 同时 监听多个socket的网络i/o请求。与阻塞i/o不同的是,i/o多路复用使用的是select、poll、epoll系统调用函数,该函数在阻塞状态下可以同时监听多个socket[监听socket、客户端socket],当某一个socket的数据到达时,就会唤醒阻塞线程回到用户态,线程调用recvfrom来读取已经到达的数据到用户空间并处理。与非阻塞i/o不同的是,当没有数据可到达时线程会阻塞并让出cpu。

1.select系统调用

select为读请求、写请求、异常事件创建三个独立的数组,每个数组有32个元素,每个元素占32bit,使用bit位作为标记,因此每个数组可监听1024个请求,其中请求来自不同的线程。通过timeout设定select阻塞等待的超时时间

单线程下select i/o多路复用读取网络数据过程:

  • 单线程(服务器)创建监听socket,用来监听新客户端的连接请求,将监听socket的fd记录到read数组中的某个bit位上。
  • 单线程调用select()函数,触发系统调用,进程从用户态切换到内核态,并将三个数组拷贝到内核态
  • 内核态的该线程检查缓存中是否有数据,如果有,判断数据是哪个fd的,并判断是读请求、写请求还是出现异常,将fd对应数组位置的bit置0,切换回用户态,并将修改后的数组拷贝到用户态
  • 如果未命中说明没有客户端发送数据,线程进入阻塞队列让出cpu,持续监听客户端数据。
  • 如果阻塞时间超过了timeout,那么唤醒该线程,回到用户态。
  • 客户端a或b发起http请求,并携带数据。
  • 客户端a或b发送的网络数据包到达服务器网卡,网卡通过dma将数据包写入内核缓冲区,然后触发硬件中断,抢占cpu,执行中断处理程序
  • 中断处理程序将数据进行网络协议栈的处理(如ip、tcp处理),最终将数据放入对应socket的接收缓冲区,修改对应socket的三个数组,cpu会返回到被中断的地方继续执行,唤醒单线程,进入就绪态
  • 当该线程被调度器选中再次运行时,回到用户态,并将修改后的数组拷贝到用户态
  • 线程遍历三个数组筛选出哪些客户端的数据已经在内核空间的缓存中,记为t[fd1,fd3…]。
  • 如果t[]中有监听socket的fd,说明有新的客户端请求连接,那么单线程调用accept()系统调用与客户端建立连接,得到一个客户端socket文件描述符fd,用于与该客户端通信,将fd添加到三个数组的某个bit位上。
  • t[]中除了监听socket之外的其他fd说明是已建立连接的客户端发送的数据,单线程对t[]调用read()函数,进程从用户态切换到内核态,此时t[]的数据一定在内核缓存中,直接拷贝到对应fd的用户空间,然后切换回用户态。
  • 系统调用返回,线程从内核态切换回用户态,并继续执行用户态代码。
  • 处理完成后,线程传入添加新客户端fd后的三个数组重新进入内核态阻塞监听。

select缺点:

  • fd_set由于可变,需要频繁的修改用户空间和内核空间中的fd_set
  • 在用户态需要遍历原fd_set和修改后的fd_set才能知道那些数据就绪
  • 监听数量不超过1024

2.poll系统调用

poll仍然使用数组的方式监听不同的请求,但数组中每个元素都是一个结构体,记录了请求的文件描述符fd(socket、客户端)、请求类型events、返回值revents。单线程在内核中监听时(等待中断响应),在超时时间内若监听到某个文件描述符的中断响应,就将响应类型记录到revents中,否则revents置0表示未收到相应

单线程下poll i/o多路复用读取网络数据过程:

  • 监听socket接收到客户端a的三次握手,通过accept()建立连接,得到一个socket文件描述符将文件描述符、要监听的事件类型记录到polled数组中。
  • 单线程调用poll()函数,触发系统调用,进程从用户态切换到内核态,并将polled数组拷贝到内核态
  • 内核态的该线程检查缓存中是否有数据,如果有,判断数据是哪个fd的,并判断是读请求、写请求还是出现异常,将fd对应结构体的revents置为相应值,切换回用户态,并将修改后的数组拷贝到用户态,返回就绪文件描述符数量n
  • 线程判断n是否大于0,大于0则遍历polled数组,找到就绪的文件描述符。
  • …同select

3.epoll系统调用

epoll维护一棵红黑树和一个链表,红黑树记录要监听的文件描述符fd,就绪链表记录已就绪的文件描述符fd。与select最大的不同在于,epoll在内核态初始化epoll_create系统调用),不会频繁的在用户态和内核态拷贝。

由于在内核态,所以当线程通过accept()与客户端建立连接后,需要通过epoll_ctl()系统调用向epoll的红黑树中添加fd。此外,添加时会对fd设置一个回调函数,回调函数会在fd的数据写入内核缓存(触发中断响应)时将fd添加到就绪链表中。

线程使用epoll_wait()系统调用持续监听就绪链表,若在超时时间内有fd加入到就绪链表中那么唤醒该线程,将就绪的fd拷贝到用户空间

epoll方案很好的解决了select的三个问题。

单线程下epoll i/o多路复用读取网络数据过程:

  • 单线程调用epoll_create系统调用进入内核态,在内核态初始化一棵红黑树和一个链表,回到用户态。
  • 单线程(服务器)创建监听socket,用来监听新客户端的连接请求。
  • 单线程调用epoll_ctl()系统调用进入内核态,向红黑树中添加监听socket的fd,回到用户态。
  • 单线程调用epoll_wait()函数,触发系统调用,进程从用户态切换到内核态
  • 内核态的该线程检查就绪链表中是否有fd,如果有,切换回用户态,并将就绪链表拷贝到用户态
  • 如果未命中说明没有客户端发送数据,线程进入阻塞队列让出cpu,持续等待客户端数据。
  • 如果阻塞时间超过了timeout,那么唤醒该线程,回到用户态。
  • 客户端a或b发起http请求,并携带数据。
  • 客户端a或b发送的网络数据包到达服务器网卡,网卡通过dma将数据包写入内核缓冲区,然后触发硬件中断,抢占cpu,执行中断处理程序
  • 中断处理程序将数据进行网络协议栈的处理(如ip、tcp处理),最终将数据放入对应socket的接收缓冲区,cpu会返回到被中断的地方继续执行,触发回调函数将数据对应的socket(fd)加入就绪链表,唤醒等待在该socket上的线程,进入就绪态
  • 当该线程被调度器选中再次运行时,切换回用户态,并将就绪链表拷贝到用户态
  • 如果就绪链表中有监听socket的fd,说明有新的客户端请求连接,那么单线程调用accept()系统调用与客户端建立连接,得到一个客户端socket文件描述符fd,用于与该客户端通信,调用epoll_ctl()系统调用向红黑树中添加新客户端socket的fd,回到用户态。
  • 就绪链表中除了监听socket之外的其他fd说明是已建立连接的客户端发送的数据,单线程对就绪链表中的fd调用read()函数,进程从用户态切换到内核态,此时就绪链表中fd的数据一定在内核缓存中,直接拷贝到对应fd的用户空间,然后切换回用户态。
  • 系统调用返回,线程从内核态切换回用户态,并继续执行用户态代码。
  • 线程解析并处理命令,然后将响应结果写入客户端socket(fd)的发送缓冲区,调用write或send系统调用将响应结果通过dma从内核的发送缓冲区由网卡发送出去。
  • 处理完成后,重新进入内核态阻塞。

思考:有没有可能连接请求还没有处理完,没有为客户端socket分配内核缓冲区,客户端的读请求就来了?

应该不会,这个连接请求应该就是tcp三次握手,只有建立连接了,服务器向客户端发送ack,客户端才能发读请求到服务器。

事件通知机制:

leveltriggered:lt,每次将就绪队列中的fd拷贝到用户空间时保留就绪队列中的fd。

  • 可以实现重复读取,适用于fd一次读不全的情况。

edgetriggered:et,每次将就绪队列中的fd拷贝到用户空间时清空就绪队列。

  • 适用于一次性读取fd的情况。
  • 可以通过epoll_ctl函数的修改功能手动将fd添加回就绪链表实现lt的效果

不太理解为什么用lt,每次都从网络读到用户内存就算一次读不全早晚也能读完吧,没必要每次都重复读吧,可能是因为数据在对应用户空间中不连续使用起来麻烦。

四、信号驱动i/o

信号驱动i/o通过sigaction()系统调用对fd设置回调函数,此时线程立即返回用户态执行其他任务。当fd的数据到达内核缓存时会触发回调函数,将就绪的fd拷贝到用户态的信号队列通知线程。线程调用recvfrom进入内核态将数据拷贝到用户内存。

与非阻塞i/o不同的是,线程返回用户态后不会轮询数据是否准备好,而是去执行其他任务,收到来自内核的通知才调用read读数据。

由于信号驱动i/o下每有一个fd就绪内核线程都会执行内核态与用户态切换通知用户线程,影响性能;而多路复用i/o可以一次性获取多个就绪的fd才切换到用户态,性能更好(多路复用i/o中虽然每次fd到达就绪链表都会唤醒线程,但是线程获得cpu前仍然可以有fd进入就绪链表,且调用wait()进入内核态时如果就绪链表有多个fd也可以一次性返回)。因此多路复用i/o的性能更好更适用于有高并发需求的redis。

五、异步i/o

异步i/o整个过程都是非阻塞的,用户进程调用aio_read()系统调用函数声明要读的fd和读到用户内存空间的地址就直接返回到用户态,进行其他任务。而读取数据和将数据从内核缓存拷贝到用户内存的任务完全交由内核线程来完成。

异步i/o比i/o多路复用更高效,因为用户线程对i/o操作完全解耦,可以实现更高并发的处理请求,使用频率较高但是不如多路复用i/o:由于所有任务都交由内核完成,每个任务内核都要开辟新线程来处理(内核是多线程,cpu也是多核,只有用户应用redis是单线程),对内核负载太大。解决方法是在用户应用进行并发控制,限制单位时间向内核分配的任务数量

六、redis网络模型*

1.纯单线程模型

redis通过i/o多路复用来提高网络性能,支持各种不同的多路复用实现,将这些实现封装为统一的接口:

  • ae_epoll:linuxos多路复用实现方案
  • ae_kqueue:macos
  • ae_select:所有os

redis提供了通用的api接口,针对不同操作系统使用不同的实现方案。

#ifdef have_evport
#include "ae_evport.c"
#else
    #ifdef have_epoll
    #include "ae_epoll.c"
    #else
        #ifdef have_kqueue
        #include "ae_kqueue.c"
        #else
        #include "ae_select.c"
        #endif
    #endif
#endif

  • addevent():注册fd,例如epoll_ctl()
  • create():创建多路复用监听器,例如epoll_create()
  • delevent():删除fd,例如epoll_ctl()
  • poll():监听fd就绪,例如epoll_wait()、select()、poll()

linux下redis(单线程) epoll 处理网络数据过程:(相较于三.3没有内核态的切换,但是流程更完整)

  • 单线程调用epoll_create在内核态初始化一棵红黑树和一个链表。
  • 单线程创建监听socket,用来监听新客户端的连接请求。
  • 单线程调用epoll_ctl向红黑树中添加监听socket的fd,设为可读状态,并添加回调函数。
  • 为监听socket添加监听读处理函数
  • 单线程调用epoll_wait,阻塞并监听就绪链表,让出cpu。
  • 如果阻塞时间超过了timeout,那么唤醒该线程。
  • 新客户端a和b发起http请求,请求建立连接
  • dma和中断处理程序将客户端a的数据读到监听socket的接收缓冲区,触发回调函数将监听socket的fd加入就绪链表,标记为读请求
  • 单线程监听到就绪链表中数据发生变化,进入就绪态。
  • 单线程就绪态到运行态过程中可能dma和中断处理程序将客户端b的数据读到监听socket的接收缓冲区,触发回调函数将监听socket的fd加入就绪链表
  • 单线程获取cpu,此时就绪链表中只有监听socket的fd,会触发监听socket的监听读处理函数:单线程调用accept()系统调用与客户端a和b建立连接,分别得到客户端a和b的socket文件描述符fd,为客户端fd分别设为可读状态添加读处理函数,调用epoll_ctl系统调用向红黑树中添加客户端a和b的fd。
  • 单线程调用epoll_wait,检查就绪链表中是否有fd,如果有,说明step11过程中有新的网络数据到达了内核缓存,切换回用户态处理。
  • 如果未命中说明没有i/o请求,线程阻塞并监听就绪链表,让出cpu。
  • 客户端a发来http请求,并携带数据。
  • dma和中断处理程序将客户端a的数据读到客户端a socket的接收缓冲区,触发回调函数将客户端a的fd加入就绪链表,并设为读请求
  • 单线程监听到就绪链表中数据发生变化,进入就绪态。
  • 单线程获取cpu,此时就绪链表中有客户端a的fd,且为读请求,会触发客户端a socket的读处理函数:单线程调用read系统调用将请求携带的数据从内核缓冲区读到为客户端a分配的内存输入缓冲区中。
  • 单线程解析并执行数据中的指令,然后将响应结果写入redis的clients_pending_write链表,调用epoll_ctl将客户端a的fd状态修改为可读+可写类型,并添加写处理函数。(只有内核中该客户端的缓冲区空间不够了才放入链表,不然会直接发送到内核缓冲区,且不会添加写处理函数网卡会自动发送数据,step20不满足,直接异步执行step22,我理解的是数据先正常发送,只有发送空间满了发不出去才设为可写类型表示还有未发出去的数据,然后只有发送完后触发的中断处理程序检查到有可写标识才会将该fd的写请求加入就绪链表)
  • 单线程调用epoll_wait,检查就绪链表中是否有fd…
  • 当客户端a的内核发送缓冲区有空闲且可写状态为true就会发出硬件中断将客户端a的fd加入到就绪链表,并设为写请求
  • 单线程获取cpu,此时就绪链表中有客户端a的fd,且为写请求,会触发客户端a socket的写处理函数:单线程调用send从redis的clients_pending_write链表中取数据,将数据写入客户端a的内核发送缓冲区,单线程检查redis的clients_pending_write链表是否还有客户端a的数据,如果没有那么调用epoll_ctl将客户端a的fd状态修改为可读类型
  • 内核协议栈会将数据通过网卡发送出去,发送成功后触发中断将客户端a的fd加入到就绪链表,并设为写请求(这个过程是异步的,由内核和网卡负责,不需要redis单线程参与)。
  • 单线程调用epoll_wait,检查就绪链表中是否有fd…

因此,redis中的i/o多路复用可以理解为:复用epoll同时监听客户端连接请求、客户端读请求、服务器向客户端的写请求,就绪的任何类型的请求都会放到就绪链表中,并每隔一段时间接收多条请求,针对不同类型的请求使用不同的分支(监听读处理函数step11、读处理函数step17+18、写处理函数step21)处理请求。

复用就绪链表接收三类不同的请求请求:

  • 客户端连接请求从初始化epoll开始,调用epoll_create在内核态创建红黑树和就绪链表,创建监听socket后调用epoll_ctl向红黑树中添加监听socket的fd,设为可读状态,并添加回调函数。当连接请求由网卡到达内核缓冲区会触发中断,触发回调函数将监听socket从红黑树添加到就绪链表,设为读请求。
  • 客户端读请求在建立连接后会调用epoll_ctl将该客户端的fd添加到红黑树,设为可读状态,该客户端的读请求到达后会被写入为该客户端分配的内核缓冲区,触发中断触发回调函数将该客户端socket添加到就绪链表,设为读请求。
  • 服务器向客户端的写请求在线程接收客户端读请求并处理后,如果该客户端的内核发送缓冲区有足够的空闲那么将数据直接发送到内核缓冲区,会异步发送数据给客户端,否则就会将该客户端socket加入到pending_write链表中,并调用epoll_ctl将红黑树中客户端的fd并设为可读+可写类型,当内核缓冲区有空闲也就是发送数据后会引发中断触发回调函数将客户端的fd添加到就绪链表并设为写请求。

复用单线程处理三类不同的请求请求:

线程调用epoll_wait进入内核态检查就绪链表是否有数据,如果有直接将就绪链表拷贝到用户态并处理,如果没有回让出cpu并阻塞,直到就绪链表有数据或超过等待时长会进入就绪态等待分配cpu回到用户态。回到用户态会遍历就绪链表依次处理每个请求:

  • 客户端连接请求会调用之前添加的监听处理函数建立连接,调用epoll_ctl将客户端fd加入到红黑树,设为可读,并添加回调函数和读处理函数。
  • 客户端读请求会调用之前添加的读处理函数,执行数据中的命令例如"get key"并将结果发送到内核发送缓冲区,否则就会将该客户端socket加入到pending_write链表中,调用epoll_ctl将红黑树中客户端的fd并设为可读+可写类型。
  • 服务器向客户端的写请求会调用send从pending_write链表中取该客户端数据,将数据写入客户端的内核发送缓冲区,发送后如果pending_write链表已经没有当前客户端的数据,那么调用epoll_ctl将客户端的fd状态修改为可读类型。

理一下写处理的过程:首先线程接收客户端读请求并处理,处理完成如果该客户端的内核缓冲区有足够的空闲那么将数据直接发送到内核缓冲区,后台会异步发送数据。只有该客户端的内核缓冲区满了才将数据放到clients_pending_write链表中,并设为可写类型,当该客户端的内核缓冲区中的数据发送完后触发中断,中断处理程序检查有足够的空闲了且该fd目前是可写状态,那么会将该fd的写请求放入就绪链表

linux下redis(单线程) epoll 处理网络数据过程:(从用户态调用的角度分析,不涉及内核态,多路复用的思想更直观)

//redis网络i/o入口函数
main {
	server.el = aecreateeventloop();// 创建epoll
	listentoport(server.port,server.ip);// redis创建监听socket,给定监听的ip和port(服务器的ip)
	createsocketaccepthandler(accepttcphandler);// 将监听socket添加到epoll,并添加监听读处理函数(监听读处理函数就是step11) 
	
	// 死循环,该线程一直网络i/o
	while(true){
		// 为clients_pending_write链表中(有写需求)的客户端socket依次添加写处理函数(写处理函数就是step21)
		foreach(clients_pending_write){
			connsetwritehandlerwithbarrier(sendreplytoclient);
		}
		
		/* epoll_wait等待中断信号,返回就绪链表
		客户端发送的连接请求引发的中断会将监听fd添加到就绪链表,设为读请求
		客户端发送的读请求引发的中断会将客户端fd添加到就绪链表,设为读请求
		服务器向客户端发送返回结果后引发的中断会将客户端fd添加到就绪链表,设为写请求
		*/
		numevents = aeapipoll();
		// 依次处理就绪链表中的socket
		for(j = 0; j < numevents; j++){
			if(j is 监听socket){
				fd = accept(newclient);// 接收新客户端的连接请求,得到客户端socket的fd
				connsetreadhandler(fd, readqueryfromclient);// 将客户端socket添加到epoll,并添加读处理函数(读处理函数就是step17+18)
			}
			
			if(j is 客户端读socket){
				connread(c);// 将数据从内核缓冲区读到内存中该客户端的输入缓冲区
				processinputbuffer(c);// 解析数据中的命令为字符串数组[set, name, jack]
				cmd = lookupcommand(c->argv[0]);// redis中命令是以键值对方式存储的,通过key=set就能找到对应的函数体
				proc(cmd,c);// 执行cmd命令,传入数据c
				addreply();// 将命令执行结果写入该客户端的等待队列
			}	

			if(j is 客户端写socket){
			    if (!clienthaspendingreplies(c)) {// 检查是否有数据要写
			        aedeletefileevent(server.el, fd, ae_writable);// 没有数据,立即取消写事件监听
			        return;
			    }
			    int nwritten = write(c->fd, c->buf + c->sentlen, c->bufpos - c->sentlen);// 有数据,直接写入socket
			}	
		}
	}
}

2.命令处理单线程+网络i/o多线程模型

2.1 redis单线程网络模型的瓶颈

redis命令执行部分必须是单线程

为什么redis要做成单线程:

  • 除了持久化操作外,redis是纯内存操作,因此执行速度非常快,限制redis性能的是网络i/o延迟而不是指令执行速度,短板效应下多线程也不会带来性能提升。
  • 多线程会导致上下文切换,会带来额外开销。
  • 单线程是为了保证命令的隔离性,而多线程会有线程安全问题,使用锁机制可以解决线程安全问题但是也会带来额外的开销。

经过上面的分析,redis的执行过程为:接收网络请求(step12~17)->执行命令(step18)->返回响应结果(step19~22)

  • 接收请求:redis单线程阻塞等待,并行的网络请求由内核的中断处理程序并行处理,性能很强,但是redis单线程返回用户态后需要串行执行read系统调用将数据从内核态读到用户态,且需要频繁切换用户态和内核态,这是主要瓶颈
  • 执行命令:redis单线程串行执行命令,因为命令必须串行才能保证原子操作,必须保持现状。
  • 返回响应:由redis单线程串行调用send()函数发送响应,且需要频繁切换用户态和内核态(虽然内核态发送响应可以异步并行执行,但串行send效率太低),这是主要瓶颈

由于redis性能收到网络i/o的限制,经过上述分析,网络i/o的接受请求、返回响应部分可以设计成多线程,且这两部分不涉及命令的执行,所以不会出现并发问题。

2.2 redis多线程网络模型

对于接收请求,由redis单线程分发“read系统调用将数据从内核态读到用户态(step17)”这一操作给多个子线程执行,大大提高了读取速度,且不会出现线程安全问题(不涉及请求数据中的命令执行)。

数据读取并解析完成后,由redis单(主)线程来顺序执行命令,内存执行速度快,切能保证原子操作。虽然接受请求使用多线程,但速度上仍然无法保证指令执行的主线程有100%的利用率。

对于返回响应,由redis单线程分发“send系统调用将数据从用户态读到内核态(step21)”这一操作给多个子线程执行,大大提高了读取速度,且不会出现线程安全问题。

虽然如此,但网络i/o依旧是短板。

七、resp协议

redis是cs架构

  • 服务端:在linux操作系统中安装redis并启动,指定ip地址和端口号。
  • 客户端:redis-cli命令行、jedis。

通信过程分为两步:

  • 客户端向服务端的ip+port发送一条请求,携带数据(命令)。
  • 服务端在端口上持续监听请求,使用网络i/o模型接收到请求,解析并执行命令,返回响应结果给客户端。

redis中采用resp协议来规定客户端和服务器发送请求的规范。

1.数据类型

resp通过首字节的字符来区分不同数据类型,常用的数据类型包括5种:

  • 单行字符串:首字节是‘+’,后面跟单行字符串,以crlf(“\r\n”)结尾,无法使用特殊字符,二进制不安全,一般用于服务端返回响应。
  • errors:首字节是’-',后面跟异常信息,以crlf(“\r\n”)结尾。
  • 数值:首字节是’:',后面跟数字格式的字符串,以crlf(“\r\n”)结尾。
  • 多行字符串:首字节是’$',后根字符串占用字节数量,后根字符串,二进制安全,最大支持512mb。

例如:$3\r\nabc\r\n

如果大小为0,代表空字符串:“$0\r\n\r\n”

如果大小为-1,则代表不存在: “$-1\r\n”

  • 数组:首字节是’*',后根元素个数,后根元素,元素数据类型不限,可以嵌套数组。

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持代码网。

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2025  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com