1 Redis的单线程认知
一个基本事实
我们通常说的Redis单线程,主要是指:Redis 6.0 之前版本的 网络I/O 和 键值对读写 是由一个线程来完成的。
除了网络I/O 和 键值对读写 之外的其他功能,大多都是由额外的线程执行的。比如:持久化、异步删除、集群数据同步 等操作。
Note:Redis 6.0之后对网络I/O改为使用多线程,但是,仍然使用单线程处理 键值对的读写操作。
Why 单线程?
因为 多线程开发会不可避免的带来并发控制的问题。
系统中通常会存在被多线程同时访问的共享资源,比如一个共享的数据结构。而当多个线程要修改这个共享资源的时候,为了保证共享资源的正确性,就需要有额外的机制进行保证。
以Redis为例,它提供了List数据类型,我们可以拿它来做队列,入队(LPOP)和出队(LPUSH)就是两个基本操作。如果采用多线程设计,那么当两个线程同一时间一个操作LPOP一个操作LPUSH,为了保证队列长度的正确性,Redis就需要保证串行执行。而要保证串行执行,可能就需要一些额外的开销,比如我们常见的加锁。但在Redis的使用场景下,简单地加锁可能并不能得到理想的效果,会导致大部分线程在等待获取互斥锁,并行变串行,从而降低吞吐率。此外,多线程开发一般也还会引入同步源于来保护共享资源的并发访问,这也会降低系统代码的易调试性和可维护性。
综上所述,为了避免这些问题,尽可能保证简单高效,Redis直接采用了单线程模式。
2 Redis的单线程效率
我们都知道,Redis公开出来的数据:Redis使用单线程也可以达到每秒10万级的处理能力。
前提条件:在一定的服务器配置下才能达到。
Why 这么高效?
核心原因有两个:
(1)Redis的大部分操作都在内存上完成 + 采用了高效的数据结构
eg. 哈希表、跳表 等。如果你对数据结构还不熟悉,可以阅读 Redis学习总结(1)。
(2)Redis采用了多路复用机制,使其在网络I/O操作中能够并发处理大量的客户端请求,从而实现高吞吐率。
其中,原因(2)是Redis单线程高效率的重点,它避免了accept() 和 send()/recv() 潜在的网络I/O操作的阻塞点。
如果不想了解细节,那么知道这几个核心的原因就够了。
而要理解多路复用模型的优势,就得了解一下基本的IO模型。
3 基本的IO模型
在网络处理程序中,都会存在一些潜在的阻塞点,比如:常见服务端Socket程序中的accept() 和 recv() 函数。比如服务端监听到一个客户端有连接请求,但是一直没有能够成功建立连接,就会阻塞在accept()函数中,导致其他客户端无法和服务端建立连接,这就可能会导致服务端的线程阻塞。
那么,有没有不阻塞的IO模型?
别急,我们从阻塞IO模型看起。我们也不看什么原理,举例子-买火车票场景来理解。
阻塞式IO模型
老周去火车站买票,排队三天买到一张退票。
开销:在车站吃喝拉撒睡 3天,其他事一件没干。
非阻塞式IO模型
老周去火车站买票,隔12小时去火车站问有没有退票,三天后买到一张票。
开销:往返车站6次,路上6小时,其他时间做了好多事。
IO多路复用模型
对于IO多路复用,不同的操作系统平台有不同的系统调用实现,主要以select/poll 和 epoll 最为人知。
(1)select/poll
老周去火车站买票,委托黄牛,然后每隔6小时电话黄牛询问,黄牛三天内买到票,然后老李去火车站交钱领票。
开销:往返车站2次,路上2小时,黄牛手续费100元,打电话17次。
(2)epoll
老周去火车站买票,委托黄牛,黄牛买到后即通知老李去领,然后老李去火车站交钱领票。
开销:往返车站2次,路上2小时,黄牛手续费100元,无需打电话。
信号驱动IO模型
老周去火车站买票,给售票员留下电话,有票后,售票员电话通知老李,然后老李去火车站交钱领票。
开销:往返车站2次,路上2小时,免黄牛费100元,无需打电话。
异步IO模型
老周去火车站买票,给售票员留下电话,有票后,售票员电话通知老李并快递送票上门。
开销:往返车站1次,路上1小时,免黄牛费100元,无需打电话。
Redis IO模型
Redis在设计中基于Linux的IO多路复用机制实现了自己的IO模型,如下图所示:
上图中的多个FD就是多个套接字(Socket),Redis的网络框架通过调用epoll让内核监听这些套接字。此时,Redis线程不会阻塞在某一个特定的监听 或 已连接的套接字上。因此,Redis可以同时和多个客户端连接并处理请求,从而提升并发性。
正如刚刚的例子中提到,黄牛买到票后会通知老周去领票,为了在请求到达时能够通知到Redis线程,epoll提供了基于事件的回调机制,即针对不同事件的发生,调用响应的处理函数。
Note:比如 连接请求对应Accept事件,读取数据对应 Read事件。Redis会分别对这两个事件注册 accept 和 get 回调函数。
这些事件会被放进一个事件队列,Redis单线程会对该队列不断地进行处理:如果Linux内核监听到有实际请求时,就会触发对应事件,然后Linux内核就会回调Redis对应的函数开始处理。
因此,Redis不用一直阻塞等待是否有实际请求发生,避免CPU资源浪费,进而提高吞吐率。
Note:IO 模型的演进,其实就是时代的变化,倒逼着操作系统将更多的功能加到自己的内核。
4 总结
本文总结了Redis单线程的几个核心要点:
(1)Redis单线程的基本认知,即Redis只是对网络I/O和数据读写采用了单线程。
(2)Redis为何要使用单线程,即Redis为了避免多线程开发中的并发控制问题。
(3)Redis单线程为何很高效,即Redis使用了高性能的多路复用IO模型。
参考资料
极客时间,蒋德钧《Redis核心技术与实战》