Redis(一):单线程为何还能这么快?
提到 redis 马上在我们脑海中会浮现出这样一些关键字:单线程、高性能、内存数据库、kv 存储......这些关键字都从不同层面描述了 redis 的一些相关特性和技术实现。那么为什么 redis 具备这些特性以及是如何实现的,本文将进行一一分析。
一、单线程
1.1 为什么是单线程
总结 Redis 的普通 KV 存储瓶颈不在 CPU,而往往可能受到内存和网络 I/O 的制约。
Redis 中有多种类型的数据操作,甚至包括一些事务处理,如果采用多线程,则会被多线程产生的切换问题而困扰,也可能因为加锁导致系统架构变的异常复杂造成性能损耗。
Redis 作者咋说的:
总结来说就是对于 redis 来说单线程的设计能够保证性能,多线程在设计和实现上会带来更多的复杂度。但是使用单线程的方式确实无法很好发挥多核 CPU 的性能,可以通过在单机开多个 Redis 实例来完善!
1.2 有多线程的考量吗
Redis4.0 版本对于一些大键值对的删除操作,引入多线程来非阻塞地释放内存空间,能减少对 Redis 主线程阻塞的时间,提高执行的效率。
Redis6.0 引入多线程来提高网络 IO 读写性能。
这里要注意的是 Redis 的多线程只是在网络数据的读写这类耗时操作上使用了, 执行命令仍然是单线程顺序执行。Redis6 中默认是禁用多线程的,可以通过修改 redis 的配置文件中 io-threads-do-reads=true 来开启。除此之外还需要设置现场的数量才能正真开启多线程,配置参数为 io-threads 3 表示开启三个线程。
线程设置建议:关于线程数的设置,官方的建议是如果为 4 核的 CPU,建议线程数设置为 2 或 3,如果为 8 核 CPU 建议线程数设置为 6,线程数一定要小于机器核数,线程数并不是越大越好。
二、高性能
通常我们的理解是单线程性能没有多线程好,那 redis 又是如何做到高性能的了?
2.1 I/O 多路复用
这是我们最多看到的一句解释,redis 使用了 I/O 多路复用的模式,所以性能高,那么到底什么是 I/O 多路复用模型,以及在 redis 中怎么实现的。我这里先打几个比方来方便大家理解。
要过年了,老王去火车站买票回家过年,春运期间票不好买,老王买了三天买到了一张退票。这样一个场景老王有三种方式来完成这次买票:
方式一:老王去到火车站售票大厅,在长椅上躺了两夜,终于在第三天等到了一张票,兴高采烈的回家了。(老王在火车站待了三天,其他啥事没干,还耗费了 6 桶泡面一床棉被)
方式二:老王去到火车站售票大厅买票,没买到,之后每天中午再去一次,终于在第三天买到了票。(老王往返车站 6 次,路上耗费了 3 小时,不过这几天其他时间送了三天外卖,又给家里的老婆挣了不少钱)
方式三:老王去到火车站售票大厅买票,没买到,这时候看到一个黄牛在帮别人买票,老王想着还有外卖要送,就让黄牛帮他买,三天后黄牛买到了票通知他下班后来取。(老王往返车站两次,路上耗费 1 小时,给了黄牛 50 块手续费,其他时间送了三天外卖,由于老王临近过年每天都没耽搁的加班送外卖,平台奖励了老王 500 块)
第一种方式就是阻塞 IO 模型,第二种方式就是非阻塞 IO 模型,第三种方式就是 IO 复用模型了。除了老王,老张老李......都找了黄牛买票,这样大家都可以不用跑火车站了,等黄牛消息就行。黄牛帮一个人买是买,帮多个人买也是买,反正都要在这里排队,还能多挣几份钱。老王老张老李的请求,都复用这个黄牛搞定了,老王他们节省了时间和精力干了其他事,黄牛一个人花费了近乎一样的时间和精力赚了多份钱。 大概理解了 I/O 多路复用的概念接下来就看看在 redis 中是如何实现的。针对 IO 复用思想前后主要有 select, poll, epoll 三种技术实现。
Select:select 是 I/O 多路复用的第一个实现(1983 年),有 I/O 事件发生了,却并不知道是哪几个流,只能无差别轮询所有流,找出能读出数据,或者写入数据的流,同时处理的流越多,轮询时间就越长。就好比黄牛给多个人买票,但是并不知道买到票是谁的,只能不停的去问所有买票的人是不是你的。这样买票的人越多,黄牛要问的人就越多。
select 本质上是通过设置或者检查存放 fd 标志位的数据结构来进行下一步处理。这样所带来的缺点是:
单个进程可监视的 fd 数量被限制,即能监听端口的大小有限。一般来说这个数目和系统内存关系很大,具体数目可以 cat /proc/sys/fs/file-max 察看。32 位机默认是 1024 个。64 位机默认是 2048.
对 socket 进行扫描时是线性扫描,即采用轮询的方法,效率较低。
需要维护一个用来存放大量 fd 的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大。
Poll:poll 本质上和 select 没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个 fd 对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有 fd 后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历 fd。这个过程经历了多次无谓的遍历。它没有最大连接数的限制,原因是它是基于链表来存储的。
Epoll:epoll 可以理解为 event poll,不同于忙轮询和无差别轮询,epoll 会把哪个流发生了怎样的 I/O 事件通知我们。epoll 实际上是事件驱动(每个事件关联上 fd)的,此时我们对这些流的操作都是有意义的。
通过以上三种技术实现的分析,epoll 无疑是最好的选择,那么 redis 中是这样选择的吗?先来看下 redis 在做多路复用函数选择时的代码实现:
执行逻辑如下图:
可以看到 redis 针对不同的操作系统会选用不同的实现,主流操作系统都有类似 epoll 的实现作为选择,同时也提供了 select 方式作为备选。
2.2 Reactor(反应堆模式)
有了 epoll 等 IO 复用技术的支撑,接下来我们看看 redis 是如何利用 IO 复用来串连起 socket 连接请求和具体任务处理的。
由上图可以看出 redis 处理并发客户端连接的方式是利用 epoll 来实现 IO 多路复用,将连接信息和事件放到队列中,之后依次放到文件事件分派器,事件分派器将事件分发给事件处理器。这种处理方式叫做反应堆模式。Redis 是基于 Reactor 模式(反应堆模式)开发了自己的网络模型,形成了一个完备的基于 IO 复用的事件驱动服务器。
上面我们了解到 epoll 方式的多路复用实现已经是很高性能的了,那么为什么 redis 在此基础上还要基于 Reactor 来实现自己的网络模型了?
epoll 将收集到的可读写事件全部放入队列中等待业务线程的处理,此时线程池的工作线程拿到任务进行处理,实际场景中可能有很多种请求类型,工作线程每拿到一种任务就进行相应的处理,处理完成之后继续处理其他类型的任务,工作线程需要关注各种不同类型的请求,对于不同的请求选择不同的处理方法,因此请求类型的增加会让工作线程复杂度增加,维护起来也变得越来越困难。
如果我们在 epoll 的基础上进行业务区分,并且对每一种业务设置相应的处理函数,每次来任务之后对任务进行识别和分发,每种处理函数只处理一种业务,这种模型也就是 Reactor 反应堆模式的设计思路。
通俗点讲就是黄牛的业务做的很好,找黄牛除了买火车票还有买机票电影票的,那么黄牛每次处理不同的业务的时候就要不断跑来跑去切换业务场景,显然这样业务没法做大做强,黄牛就找了多个业务员,负责专门买火车票,飞机票,电影票,这样黄牛接到不同业务的时候就交给不同的业务员去做,接客能力一下就增强了。
三、告一段落
到这里我们从 redis 的线程模型分析了 redis 为什么使用单线程,以及从单线程性能依旧很出色分析了基于 I/O 多路复用的反应堆模式请求处理流程。下一篇将从 redis 的内存模型来解读 redisdb 的数据结构以及内存回收机制。
评论