目录
Redis为什么快?
Redis到底是单线程还是多线程?
Redis网络模型
初始化
事件循环
Redis为什么快?
Redis之所以能够提供高速的性能主要得益于两个方面:
- 内存存储:除了持久化外,Redis的操作都在内存中,这是其性能优势的根本。内存的读写速度远远快于磁盘,因此Redis能够实现极低的读写延迟。
- 单线程与IO多路复用模型:Redis采用了单线程的事件驱动模型,并结合了IO多路复用技术。单线程处理避免了多线程的锁机制问题,降低了上下文切换的开销。IO多路复用允许一个线程同时监控多个文件描述符(即多个连接),一旦某个连接的状态发生变化,该线程就会立即处理该事件,从而高效地处理大量客户端连接。
本文我们会为大家介绍 Redis 的网络模型。
Redis到底是单线程还是多线程?
有一个常见的面试问题:Redis到底是单线程还是多线程?
如果仅仅聊Redis的核心业务部分(命令处理),答案是单线程,如果是聊整个Redis,那么答案就是多线程。
Redis在核心业务的处理即命令的执行是由单线程执行,Redis 6.0开始,Redis引入了多线程IO模型。网络数据的读写和请求协议解析可以通过多个IO线程来处理,但命令的执行仍然由主线程串行执行。在Redis4.0版本中引入了多线程来异步处理一些耗时任务,如对于大key的异步删除命令 unlink 和异步持久化 bgsave命令 等。避免这些操作阻塞主线程,从而提高性能。
因此,对于Redis的核心网络模型,在Redis 6.0之前确实都是单线程。是利用epoll(Linux系统)这样的IO多路复用技术在事件循环中不断处理客户端情况。
为什么Redis要选择单线程?
- 抛开持久化不谈,Redis是纯内存操作,执行速度非常快,它的性能瓶颈是网络延迟而不是执行速度,因此多线程并不会带来巨大的性能提升。多线程会导致过多的上下文切换,带来不必要的开销。引入多线程会面临线程安全问题,必然要引入线程锁这样的安全手段,实现复杂度增高,而且性能也会大打折扣
Redis网络模型
Redis通过IO多路复用来提高网络性能,并且支持各种不同的多路复用实现,并且将这些实现进行封装, 提供了统一的高性能事件库API库 AE。
AE事件库是Redis用来处理IO事件的库,它根据系统的不同选用尽可能高效率的IO实现。在ae.c中使用条件编译根据系统支持的多路复用器来选用相应的文件,其按优先级排序为:evport、epoll、kqueue、select。在各个源文件中实现了ae库基本操作的统一接口。
Redis单线程网络模型的整个流程如下:
初始化
这是简化后的Redis源码中的主函数,我们先看 initServer() 函数,
在初始化中会先调用 aeCreateEventLoop() 这个函数会创建IO多路复用实例,类似于 epoll_create() 。
Redis 使用 TCP 协议进行网络通信,因此我们需要创建一个监听套接字,用来接收客户端的连接请求,这里调用了 listenToPort() 函数。
createSockerAcceptHandler() 函数是绑定一个回调函数,当epoll 检测到监听套接字有读事件时,epoll 会调用该函数,即事件分发。
在数据读处理器中会接受客户端的连接,然后将客户端的套接字加入 epoll 关心读事件,同时绑定读处理器,即客户端发生读事件时会调用客户端读处理函数 readQueryFromClient()。
最后 initSrever() 会通过 aeSetBeforeSleepProc() 设置一个回调函数 aeSetBeforeSleepProc(),该函数将在Redis的事件循环每次进入睡眠(即等待事件)之前被调用。
事件循环
aeMain函数会进行监听事件循环。
eventLoop->stop 是初始化事件循环的停止标志。这里将其设置为0,表示事件循环不停止。
在循环中,调用 aeProcessEvents 函数来处理事件。
进入等待事件之前,调用 beforeSleep() 回调函数。这个回调函数在事件循环的每个迭代开始时执行用于执行定时任务、清理工作或其他需要定期执行的操作比如清理 TTL 过期的key。
然后调用 aeApiPoll 函数,负责等待文件描述符就绪。对应于 epoll_wait,tvp 是一个指向时间结构的指针,用于指定等待事件的最长时间。numevents 将存储就绪事件的数量。
在等待结束后,遍历所有就绪的事件。对于每个就绪的事件,执行相应的事件处理函数。
对于监听套接字,是连接应答处理器,对应客户端套接字,是命令请求处理器和命令回复处理器。
对于客户端的请求会执行如下逻辑,
这三个函数的功能简而言之就是读取客户端请求、解析命令、执行命令,然后保存到缓存区,如果用户的写缓存区满了,就将响应,写添加到待写出链表。然后将将客户端添加到服务器的待写队列,这个步骤并没有发送响应?那么响应什么时候发送呢?
在 beforesleep() 中即 epoll_wait 等待之前,会将待写出的客户端套接字加入监听写事件,同时绑定写处理器,在客户端套接字写事件触发后,会执行写处理,发回响应。
总体模型如下:
在整个处理过程中性能瓶颈主要在 IO 而非命令执行,因此 Redis 引入多线程,主要在命令解析,和响应回复。其核心命令执行还是单线程。
到此Redis网络模型介绍,本文就到这里。
参考:
黑马程序员Redis入门到实战教程,深度透析redis底层原理+redis分布式锁+企业解决方案+黑马点评实战项目_哔哩哔哩_bilibili