Redis6.0为什么要采用单线程模型严格来说Redis4.0之后就不是单线程模型了。除了主线程,还有一些后台线程处理一些慢操作,比如无用连接的释放,大key的删除等等。单线程模型,为什么性能这么高?Redis的作者从设计之初就考虑了很多方面。最终选择了使用单线程模型来处理命令。选择单线程模型有几个重要的原因:Redis操作是基于内存的,大部分操作的性能瓶颈不在CPU单线程模型,避免了线程间切换带来的性能开销。单线程模型也可以并发使用客户端请求(多路复用I/O)的处理使用单线程模型,可维护性更高,开发、调试和维护成本更低。上面的第三个原因是Redis最终采用单线程模型的决定性因素。另外两个原因是使用单线程模型的额外好处,这里我们将依次介绍以上原因。性能瓶颈不在CPU。下图是Redis官网对单线程模型的描述。大概意思是:Redis的瓶颈不是CPU,它的主要瓶颈在内存和网络。在Linux环境下,Redis甚至可以每秒提交100万次请求。为什么说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查看上下文切换次数的例子:vmstat1表示每秒计数一次,cs列是指上下文切换次数。通常,空闲系统上下文切换的数量在每秒1500次以下。客户端请求的并行处理(I/O多路复用)前文提到:Redis的瓶颈不是CPU,它的主要瓶颈是内存和网络。所谓内存瓶颈很容易理解。Redis作为缓存使用时,很多场景需要缓存大量的数据,因此需要大量的内存空间。这个可以通过集群分片来解决,比如Redis自带的去中心化集群分片方案和Codis基于Proxy的集群分片方案。对于网络瓶颈,Redis在网络I/O模型中使用了多路复用技术来降低网络瓶颈的影响。在很多场景下使用单线程模型并不意味着程序不能并发处理任务。Redis虽然采用单线程模型处理用户请求,但是在等待多个连接发送的请求的同时,它利用I/O多路复用技术“并行”处理来自客户端的多个连接。I/O多路复用技术的使用可以大大降低系统的开销。系统不再需要为每个连接创建专门的监听线程,避免了创建大量线程带来的巨大性能开销。让我们详细解释多路复用I/O模型。为了更全面地理解,我们先了解几个基本概念。套接字(Socket):当两个应用程序通过网络进行通信时,套接字可以理解为两个应用程序中的通信端点。通信时,一个应用程序向Socket中写入数据,然后通过网卡将数据发送到另一个应用程序的Socket中。我们通常所说的HTTP和TCP协议的远程通信都是基于Socket实现的。五种网络IO模型也必须基于Socket实现网络通信。阻塞和非阻塞:所谓阻塞就是一个请求不能立即返回,要等到所有的逻辑都处理完才能返回响应。相反,发送请求并立即返回响应,而无需等待所有逻辑处理完毕。内核空间和用户空间:在Linux中,应用程序的稳定性远不如操作系统程序。为了保证操作系统的稳定性,Linux区分了内核空间和用户空间。可以理解为内核空间运行操作系统程序和驱动程序,用户空间运行应用程序。通过这种方式,Linux将操作系统程序和应用程序隔离开来,防止应用程序影响操作系统本身的稳定性。这也是Linux系统超级稳定的主要原因。所有的系统资源操作都在内核空间进行,如磁盘文件的读写、内存的分配与回收、网络接口的调用等,因此在一次网络IO读取过程中,数据并不是直接从网卡读取到applicationbuffer在用户空间,但是先从网卡复制到内核空间buffer,再从内核复制到用户空间Applicationbuffer。对于网络IO写入过程,流程相反,先将数据从用户空间的applicationbuffer复制到kernelbuffer,然后通过网卡将kernelbuffer中的数据发送出去。多路复用I/O模型基于多路事件分离函数select、poll和epoll。以Redis采用的epoll为例,在发起读请求前,先更新epoll的socket监听列表,然后等待epoll函数返回(这个过程是阻塞的,所以多路复用IO本质上是阻塞IO模型).当套接字有数据到达时,epoll函数返回。这时用户线程正式发起读请求,对数据进行读取和处理。这种模式使用一个专门的监控线程来检查多个套接字,如果一个套接字有数据到达,则交给工作线程处理。由于等待Socket数据到达的过程是非常耗时的,所以该方法解决了阻塞IO模型中1个Socket连接需要1个线程的问题,不存在因忙轮询导致CPU性能损失的问题非阻塞IO模型。多路复用IO模型有很多实际应用场景。众所周知的Redis、JavaNIO、Dubbo采用的通信框架Netty,都是采用这种模型。下图是基于epoll函数进行Socket编程的详细过程。可维护性我们知道多线程可以充分利用多核CPU。在高并发场景下,可以减少I/O等待带来的CPU损耗,带来不错的性能。然而,多线程是一把双刃剑。在带来好处的同时,也带来了代码维护困难、线上问题定位调试困难、死锁等问题。多线程模型下代码的执行过程不再是串行的,多个线程同时访问的共享变量如果处理不当会出现怪异的问题。下面通过一个例子来看看在多线程场景下出现的怪异现象。看下面的代码:当标志为真时,cal()方法的返回值是多少?很多人会说:这还用问吗!肯定会返回2个可能会让您大吃一惊的结果!上述代码中,由于语句1和语句2没有数据依赖,可能会出现指令重排序,编译器有可能会将flag=true放在num=1的前面。此时set和cal方法是在不同的线程中执行的,没有顺序关系。cal方法只要标志为真,就会进入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处加一个memoryfence来阻止flag前后代码的重新排序(fence)。当然,多线程也会带来可见性问题、死锁问题、共享资源安全问题。布尔挥发性标志=假;为什么Redis6.0引入多线程?Redis6.0引入的多线程部分其实只是用来处理网络数据的读写和协议解析,执行命令还是单工作线程。为什么Redis6.0引入多线程?单线程不好吗?从上图我们可以看出Redis在处理网络数据的时候,调用epoll的进程是阻塞的,也就是说这个进程会阻塞线程。如果并发很高,达到几万QPS,这可能会成为瓶颈。一般遇到这种网络IO瓶颈问题,我们可以通过增加线程数来解决。启用多线程不仅可以减少网络I/O等待带来的影响,还可以充分利用CPU的多核优势。Redis6.0也不例外。这里加入多线程处理网络数据,提高Redis的吞吐量。当然相关的命令处理还是在单线程中运行,多线程下不存在并发访问带来的问题。性能对比压测配置:RedisServer:阿里云Ubuntu18.04,8个CPU2.5GHZ,8G内存,主机型号ecs.ic5.2xlargeRedisBenchmarkClient:阿里云Ubuntu18.04,8个2.5GHZCPU,8G内存,主机型号ecs.ic5.2xlarge多线程版本是Redis6.0,单线程版本是Redis5.0.5。多线程版本需要增加如下配置:io-threads4#启用4个IO线程io-threads-do-readsyes#请求解析也使用IO线程压力测试命令:redis-benchmark-h192.168.0.49-afoobared-tset,get-n1000000-r100000000--threads4-d${datasize}-c256由上可知,GET/SET命令在多线程版本下的性能差不多与单线程版本相比增加了一倍。另外,这些数据只是为了简单验证多线程I/O是否真的带来了性能优化,并未针对具体场景进行测试。数据仅供参考。本次性能测试基于unstble分支,不排除后面发布的正式版性能会更好。最后可以看出,单线程有单线程的优点,多线程有多线程的优点。只有充分理解其中的基本原理,才能在生产实践中灵活运用。
