Redis6.0为什么引入多线程?单线程不香吗?

本文主要分两部分。首先咱们先聊一下Redis6.0以前为何采用单线程模型。而后再详细解释Redis6.0的多线程。css

Redis6.0以前为何采用单线程模型java

严格地说,从Redis 4.0以后并非单线程。除了主线程外,还有一些后台线程处理一些较为缓慢的操做,例如无用链接的释放、大 key 的删除等等。redis

单线程模型,为什么性能那么高?编程

Redis做者从设计之初,进行了多方面的考虑。最终选择使用单线程模型来处理命令。之因此选择单线程模型,主要有以下几个重要缘由:缓存

  1. Redis操做基于内存,绝大多数操做的性能瓶颈不在CPU
  2. 单线程模型,避免了线程间切换带来的性能开销
  3. 使用单线程模型也能并发的处理客户端的请求(多路复用I/O)
  4. 使用单线程模型,可维护性更高,开发,调试和维护的成本更低

上述第三个缘由是Redis最终采用单线程模型的决定性因素,其余的两个缘由都是使用单线程模型额外带来的好处,在这里咱们会按顺序介绍上述的几个缘由。安全

性能瓶颈不在CPU性能优化

下图是Redis官网对单线程模型的说明。大概意思是:Redis的瓶颈并不在CPU,它的主要瓶颈在于内存和网络。在Linux环境中,Redis每秒甚至能够提交100万次请求。网络

Redis6.0为什么引入多线程?单线程不香吗?

 

为何说Redis的瓶颈不在CPU?多线程

首先,Redis绝大部分操做是基于内存的,并且是纯kv(key-value)操做,因此命令执行速度很是快。咱们能够大概理解成,redis中的数据存储在一张大HashMap中,HashMap的优点就是查找和写入的时间复杂度都是O(1)。Redis内部采用这种结构存储数据,就奠基了Redis高性能的基础。根据Redis官网描述,在理想状况下Redis每秒能够提交一百万次请求,每次请求提交所需的时间在纳秒的时间量级。既然每次的Redis操做都这么快,单线程就能够彻底搞定了,那还何须要用多线程呢!并发

线程上下文切换问题

另外,多线程场景下会发生线程上下文切换。线程是由CPU调度的,CPU的一个核在一个时间片内只能同时执行一个线程,在CPU由线程A切换到线程B的过程当中会发生一系列的操做,主要过程包括保存线程A的执行现场,而后载入线程B的执行现场,这个过程就是“线程上下文切换”。其中涉及线程相关指令的保存和恢复。

频繁的线程上下文切换可能会致使性能急剧降低,这会致使咱们不只没有提高处理请求的速度,反而下降了性能,这也是 Redis 对于多线程技术持谨慎态度的缘由之一。

在Linux系统中可使用vmstat命令来查看上下文切换的次数,下面是vmstat查看上下文切换次数的示例:

Redis6.0为什么引入多线程?单线程不香吗?

 

vmstat 1 表示每秒统计一次, 其中cs列就是指上下文切换的数目. 通常状况下, 空闲系统的上下文切换每秒在1500如下。

并行处理客户端的请求(I/O多路复用)

如上所述:Redis的瓶颈并不在CPU,它的主要瓶颈在于内存和网络。所谓内存瓶颈很好理解,Redis作为缓存使用时不少场景须要缓存大量数据,因此须要大量内存空间,这能够经过集群分片去解决,例如Redis自身的无中心集群分片方案以及Codis这种基于代理的集群分片方案。

对于网络瓶颈,Redis在网络I/O模型上采用了多路复用技术,来减小网络瓶颈带来的影响。不少场景中使用单线程模型并不意味着程序不能并发的处理任务。Redis 虽然使用单线程模型处理用户的请求,可是它却使用 I/O 多路复用技术“并行”处理来自客户端的多个链接,同时等待多个链接发送的请求。使用 I/O多路复用技术能极大地减小系统的开销,系统再也不须要为每一个链接建立专门的监听线程,避免了因为大量的线程建立带来的巨大性能开销。

Redis6.0为什么引入多线程?单线程不香吗?

 

下面咱们详细解释一下多路复用I/O模型。为了能更充分理解,咱们先了解几个基本概念。

Socket(套接字):Socket能够理解成,在两个应用程序进行网络通讯时,分别在两个应用程序中的通讯端点。通讯时,一个应用程序将数据写入Socket,而后经过网卡把数据发送到另一个应用程序的Socket中。咱们日常所说的HTTP和TCP协议的远程通讯,底层都是基于Socket实现的。5种网络IO模型也都要基于Socket实现网络通讯。

阻塞与非阻塞:所谓阻塞,就是发出一个请求不能马上返回响应,要等全部的逻辑全处理完才能返回响应。非阻塞反之,发出一个请求马上返回应答,不用等处理完全部逻辑。

内核空间与用户空间:在Linux中,应用程序稳定性远远比不上操做系统程序,为了保证操做系统的稳定性,Linux区分了内核空间和用户空间。能够这样理解,内核空间运行操做系统程序和驱动程序,用户空间运行应用程序。Linux以这种方式隔离了操做系统程序和应用程序,避免了应用程序影响到操做系统自身的稳定性。这也是Linux系统超级稳定的主要缘由。全部的系统资源操做都在内核空间进行,好比读写磁盘文件,内存分配和回收,网络接口调用等。因此在一次网络IO读取过程当中,数据并非直接从网卡读取到用户空间中的应用程序缓冲区,而是先从网卡拷贝到内核空间缓冲区,而后再从内核拷贝到用户空间中的应用程序缓冲区。对于网络IO写入过程,过程则相反,先将数据从用户空间中的应用程序缓冲区拷贝到内核缓冲区,再从内核缓冲区把数据经过网卡发送出去。

多路复用I/O模型,创建在多路事件分离函数select,poll,epoll之上。以Redis采用的epoll为例,在发起read请求前,先更新epoll的socket监控列表,而后等待epoll函数返回(此过程是阻塞的,因此说多路复用IO本质上也是阻塞IO模型)。当某个socket有数据到达时,epoll函数返回。此时用户线程才正式发起read请求,读取并处理数据。这种模式用一个专门的监视线程去检查多个socket,若是某个socket有数据到达就交给工做线程处理。因为等待Socket数据到达过程很是耗时,因此这种方式解决了阻塞IO模型一个Socket链接就须要一个线程的问题,也不存在非阻塞IO模型忙轮询带来的CPU性能损耗的问题。多路复用IO模型的实际应用场景不少,你们耳熟能详的Redis,Java NIO,以及Dubbo采用的通讯框架Netty都采用了这种模型。

Redis6.0为什么引入多线程?单线程不香吗?

 

下图是基于epoll函数Socket编程的详细流程。

Redis6.0为什么引入多线程?单线程不香吗?

 

可维护性

咱们知道,多线程能够充分利用多核CPU,在高并发场景下,可以减小因I/O等待带来的CPU损耗,带来很好的性能表现。不过多线程倒是一把双刃剑,带来好处的同时,还会带来代码维护困难,线上问题难于定位和调试,死锁等问题。多线程模型中代码的执行过程再也不是串行的,多个线程同时访问的共享变量若是处理不当也会带来诡异的问题。

Redis6.0为什么引入多线程?单线程不香吗?

 

咱们经过一个例子,看一下多线程场景下发生的诡异现象。看下面的代码:

Redis6.0为什么引入多线程?单线程不香吗?

 

flag为true时,cal() 方法返回值是多少?不少人会说:这还用问吗!确定返回2

结果可能会让你大吃一惊!上面的这段代码,因为语句1和语句2没有数据依赖性,可能会发生指令重排序,有可能编译器会把flag=true放到num=1的前面。此时set和cal方法分别在不一样线程中执行,没有前后关系。cal方法,只要flag为true,就会进入if的代码块执行相加的操做。可能的顺序是:

  • 语句1先于语句2执行,这时的执行顺序多是:语句1->语句2->语句3->语句4。执行语句4前,num = 1,因此cal的返回值是2
  • 语句2先于语句1执行,这时的执行顺序多是:语句2->语句3->语句4->语句1。执行语句4前,num = 0,因此cal的返回值是0

咱们能够看到,在多线程环境下若是发生了指令重排序,会对结果形成严重影响。

固然能够在第三行处,给flag加上关键字volatile来避免指令重排。即在flag处加上了内存栅栏,来阻隔flag(栅栏)先后的代码的重排序。固然多线程还会带来可见性问题,死锁问题以及共享资源安全等问题。

boolean volatile flag = false;

Redis6.0为什么引入多线程?

Redis6.0引入的多线程部分,实际上只是用来处理网络数据的读写和协议解析,执行命令仍然是单一工做线程。

Redis6.0为什么引入多线程?单线程不香吗?

 

从上图咱们能够看到Redis在处理网络数据时,调用epoll的过程是阻塞的,也就是说这个过程会阻塞线程,若是并发量很高,达到几万的QPS,此处可能会成为瓶颈。通常咱们遇到此类网络IO瓶颈的问题,能够增长线程数来解决。开启多线程除了能够减小因为网络I/O等待形成的影响,还能够充分利用CPU的多核优点。Redis6.0也不例外,在此处增长了多线程来处理网络数据,以此来提升Redis的吞吐量。固然相关的命令处理仍是单线程运行,不存在多线程下并发访问带来的种种问题。

性能对比

压测配置:

Redis Server: 阿里云 Ubuntu 18.04,8 CPU 2.5 GHZ, 8G 内存,主机型号 ecs.ic5.2xlarge
Redis Benchmark Client: 阿里云 Ubuntu 18.04,8 2.5 GHZ CPU, 8G 内存,主机型号 ecs.ic5.2xlarge

多线程版本Redis 6.0,单线程版本是 Redis 5.0.5。多线程版本须要新增如下配置:

io-threads 4 # 开启 4 个 IO 线程
io-threads-do-reads yes # 请求解析也是用 IO 线程

压测命令: redis-benchmark -h 192.168.0.49 -a foobared -t set,get -n 1000000 -r 100000000 --threads 4 -d ${datasize} -c 256

Redis6.0为什么引入多线程?单线程不香吗?

 

Redis6.0为什么引入多线程?单线程不香吗?

 

从上面能够看到 GET/SET 命令在多线程版本中性能相比单线程几乎翻了一倍。另外,这些数据只是为了简单验证多线程 I/O 是否真正带来性能优化,并无针对具体的场景进行压测,数据仅供参考。本次性能测试基于 unstble 分支,不排除后续发布的正式版本的性能会更好。

最后

可见单线程有单线程的好处,多线程有多线程的优点,只有充分理解其中的本质原理,才能灵活运用于生产实践当中。