前言

Redis 是在存储系统中使用最为广泛的内存型数据库,在绝大部分项目中都能看到 Redis 的身影,然而 Redis 在设计之初却是使用单线程模型设计的,这与许多人固有的观念有所冲突,那为什么 Redis 使用单线程模型,却能每秒处理上百万次请求呢?

除此之外,Redis 在 4.0 版本之后开始使用多线程模型,甚至在 6.0 版本中引入多线程 I/O,这与当初的设计是不是产生了矛盾?

概述

本篇文章主要讨论两个问题:

  • 为什么 Redis 设计之初选择单线程模型?
  • 为什么在后面的版本中加入多线程模型?

这两个问题看似有些矛盾,实则不然,后面会分别对这个看起来完全相反的设计决策作出分析。

Redis为什么没有开启日志 redis为什么没有windows版本_Redis为什么没有开启日志

Redis 作为一个内存型数据库,它在处理外部发起的网络请求时,内部通过使用文件事件处理器 File event handler,它采用 I/O 多路复用机制来同时监听多个 Socket,根据 Socket 上的事件来选择对应的事件处理器进行处理,一旦收到网络请求就会在内存中进行快速处理,并且绝大多数都是在内存中进行操作,所以处理的速度会非常快。

在 4.0 版本之后开始引入多线程模型,并不是全部操作都采用多线程模型进行处理,仅仅只有部分命令使用「主线程」之外的其他线程,例如 UNLINK SCAN 等非阻塞性操作。

设计

不管是单线程还是多线程,这两个设计上的决定都是为了更好的提升性能,我们从另外的一个角度来看,使用单线程模型和多线程模型其实也并不矛盾。

虽然在 4.0 之后的版本引入了多线程,不过是在部分命令上引入,绝对大部分都是非阻塞性的操作,并非所有命令。

单线程

这里的单线程指的是执行 Redis 命令的核心模块,而不是整个 Redis 实例,其他模块还是有自己的线程。Redis 从设计之初就选择了单线程模型处理请求,其中有几个最重要的几个原因:

  1. 使用单线程模型能够减少没必要的开销
  2. 使用单线程模型也能并发的处理客户端的请求
  3. Redis 服务中运行的绝对大多数操作的性能瓶颈绝不是 CPU

上述的三个原因中最后一个原因起着决定性的因素,其他两个都是单线程所带来的好处。

减少没必要的开销
  • 上下文切换:CPU通过给每个线程分配CPU时间片来实现这个机制,时间片是CPU分配给各个线程的时间,因为时间片非常短,所以CPU通过不停切换线程执行,每个调度器都会为每个可运行的线程分配一个最小执行时间,所以有多线程就有可能需要CPU进行上下文切换。
  • 保存线程 1 的执行上下文
  • 加载线程 2 的执行上下文
  • 阻塞:多个线程同时访问共享资源时需要加锁来保证数据的安全,在当发生锁竞争时,竞争失败的线程就会被阻塞。当多个线程同时访问一个数据时,还需要我们在项目中额外的维护并发控制的相关代码。
并发处理

Redis 虽然使用单线程模型处理用户请求,但是它却是用 I/O 多路复用机制并发处理来自客户端的多个连接,同时等待多个连接发送的请求,从某种意义上来说也可以算为并发处理,哈哈哈。

使用 I/O 多路复用能够极大地减少系统的开销,系统不再需要额外创建和维护进程和线程来监听来自客户端的大量连接。

I/O 多路复用机制:I/O 多路复用是一种同步 I/O 模型,实现一个线程可以监视多个文件句柄,一旦某个文件句柄就绪,就能够通知应用程序进行相应的读写操作。多路是指网络连接,复用指的是同一个线程。

可以举个例子:我们去饭店吃饭,服务员给我们菜单后会一直服务到我们点完餐才会离开,那么这段时间他会一直处于等待状态也就是阻塞状态,这段时间相当于是浪费的。如果他给我们菜单后离开,继续服务其他餐桌的客人,直到我们看完菜单并想好吃什么再通知服务员过来点餐,这样不会使他进入阻塞,也能够极大的利用好资源。这个其实就是 I/O 多路复用简单例子,线程可以理解为服务员。

性能瓶颈

性能瓶颈是 Redis 选择单线程模型的决定性原因,多线程技术能够帮助我们充分利用 CPU 的计算资源来并发的执行不同的任务,但是 CPU 往往不是 Redis 的性能瓶颈,哪怕我们在一个普通的 Linux 服务器上启动 Redis 服务,它也能在一秒钟的时间内处理超过一百万个用户的请求。

如果这种吞吐量不能满足我们的需求,那么就推荐使用 Redis 集群来分担单个 Redis 的请求压力。

简单总结一下,如果不开启 AOF 备份,所有 Redis 的操作都会在内存中完成不会涉及任何的 I/O 操作,这些数据的读写由于只发生在内存中,所以处理速度也是非常快的,整个服务的瓶颈在于网络传输带来的延迟和等待客户端的数据传输,也就是网络 I/O,所以多线程模型处理全部的外部请求可能不是一个好的方案。

AOF: 持久化机制,会记录每次收到客户端的请求时,都会将其记录到日志中,在重启启动时会读取 AOF 日志,恢复数据,保证数据的持久性。

多线程

在 4.0 版本中开始引入多线程来异步处理非阻塞型命令,从上面的文章中可以看出单线程性能已经很高了,为什么还需要引入多线程呢?我们可以这样想一下,如果处理全部命令都是单线程的话,如果一个 Redis 中有一个超大键值对,几十 MB 或者更大的数据并不能在几毫秒的时间内处理完成,那么等待队列中的任务就需要阻塞等待,这样就会影响到 Redis 处理其他的请求。

所以官方将类似批量设置的命令进行后台多线程异步处理,而不是在「主线程」中进行处理。

Redis为什么没有开启日志 redis为什么没有windows版本_Redis为什么没有开启日志_02

在 6.0 之前,网络 IO 一直会影响着处理速度,为此在 6.0 版本中引入了多线程 I/O,多线程部分还只是处理网络数据的读写和协议解析,执行命令仍然是单线程。

总结

Redis 选择单线程模型处理请求是主要还是它的性能瓶颈不在于 CPU,所以使用多线程模型带来的性能提升并不能抵消它带来的开发成本和维护成本,系统的性能瓶颈也主要在网络 I/O 操作上。

引入多线程操作也是出于性能上的考虑,对于一些大键值对的操作,通过多线程处理不阻塞主线程的后续处理请求,可以很大的提高执行的效率。

4.0引入多线程是为了解决对一些大键值对操作时不影响主线程执行后续命令,减少阻塞时间提高效率。

6.0引入多线程IO是为了解决网络 I/O 的问题,可以充分利用服务器 CPU 资源,并可以分摊主线程同步 I/O 读写负荷。

可以看出,每次引入多线程都解决了一个痛点~,再次膜拜大佬。