12.单线程Redis为什么快

Redis 3.0-单线程

Redis的单线程架构

首先要明白,Redis的单线程指的是执行命令时的单线程

Redis客户端与服务端的模型可以简化成下图。

每次客户端调用都经历了发送命令、执行命令、返回结果三个过程,具体来说是发送命令、接收命令、命令排队、执行命令、返回结果5个过程。

我们说的单线程就是在第二步执行命令

一条命令从从客户端达到服务端不会立刻被执行,而是会进入一个队列中等待,每次只会有一条指令被选中执行。

Redis的单线程是指Redis 的网络 IO 和键值对读写是由一个线程来完成的,这也是 Redis 对外提供键值存储服务的主要流程。也有人说Redis的单线程指的是执行Redis 命令(读写事件)的核心模块是单线程。

但 Redis 的其他功能是有多线程的。

比如持久化、异步删除、集群数据同步、快照生成、AOF 重写等,其实是由额外的线程执行的。

所以,严格来说,Redis 并不是单线程,但是我们一般把 Redis 称为单线程高性能,这样显得"酷"些。

Redis的单线程执行流程

Redis 是跑在单线程中的,所有的操作都是按照顺序线性执行的。客户端命令的请求获取 (socket 读)、解析、执行、内容返回 (socket 写) 等等都是由一个线程处理,所有操作是一个个挨着串行执行的 (主线程),这也是 Redis 有 "单线程" 定义的来源。

多线程适用场景

一个计算机程序在执行的过程中,主要需要进行两种操作分别是读写操作和计算操作。

其中读写操作主要是涉及到的就是I/O操作,其中包括网络I/O和磁盘I/O。

计算操作主要涉及到CPU。而多线程的目的,就是通过并发的方式来提升I/O的利用率和CPU的利用率。

那么,Redis需不需要通过多线程的方式来提升提升I/O的利用率和CPU的利用率呢?

首先,我们可以肯定的说,Redis不需要提升CPU利用率,因为Redis的操作基本都是基于内存的,CPU资源根本就不是Redis的性能瓶颈。所以,通过多线程技术来提升Redis的CPU利用率这一点是完全没必要的。

Redis确实是一个I/O操作密集的框架,他的数据操作过程中,会有大量的网络I/O和磁盘I/O的发生。要想提升Redis的性能,是一定要提升Redis的I/O利用率的,这一点毋庸置疑。但是,提升I/O利用率,并不是只有采用多线程技术这一条路可以走!

多线程的优点

而多线程的目的,就是通过并发的方式来提升I/O的利用率和CPU的利用率

多线程的弊端

Java中的多线程技术,如内存模型、锁、CAS等,这些都是Java中提供的一些在多线程情况下保证线程安全的技术。采用多线程可以帮助我们提升CPU和I/O的利用率,但是多线程带来的并发问题也给这些语言和框架带来了更多的复杂性。而且,多线程模型中,多个线程的互相切换也会带来一定的性能开销。

why not 多线程

所以,在提升I/O利用率这个方面上,Redis并没有采用多线程技术,而是选择了多路复用 I/O技术。

多路复用 I/O技术

由于读写操作等待用户输入或输出都是阻塞的,所以 I/O 操作在一般情况下往往不能直接返回,这会导致某一文件的 I/O 阻塞导致整个进程无法对其它客户提供服务。

而 I/O 多路复用就是为了解决这个问题而出现的多路I/O复用模型是利用 select、poll、epoll 可以同时监察多个流的 I/O 事件的能力,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有 I/O 事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll 是只轮询那些真正发出了事件的流),并且只依次顺序的处理就绪的流,这种做法就避免了大量的无用操作。

"多路"指的是多个网络连接,"复用"指的是复用同一个线程。采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络 IO 的时间消耗) 。且 Redis 在内存中操作数据的速度非常快,也就是说内存内的操作不会成为影响Redis性能的瓶颈,主要由以上几点造就了 Redis 具有很高的吞吐量。

Redis为什么用单线程?

多线程要额外同步的开销

要更好地理解 Redis 为什么用单线程,我们就要先了解多线程的开销。

日常写程序时,我们经常会听到一种说法:"使用多线程,可以增加系统吞吐率,或是可以增加系统扩展性。"

对于一个多线程的系统来说,在有合理的资源分配的情况下,可以增加系统中处理请求操作的资源实体,进而提升系统能够同时处理的请求数,即吞吐率。

下面的左图是我们采用多线程时所期待的结果。

但是,请你注意,通常情况下,在我们采用多线程后,如果没有良好的系统设计,实际得到的结果,其实是右图所展示的那样。我们刚开始增加线程数时,系统吞吐率会增加,但是,再进一步增加线程时,系统吞吐率就增长迟缓了,有时甚至还会出现下降的情况。

为什么会出现这种情况呢?

一个关键的瓶颈在于,系统中通常会存在被多线程同时访问的共享资源,比如一个共享的数据结构。当有多个线程要修改这个共享资源时,为了保证共享资源的正确性,就需要有额外的机制进行保证,而这个额外的机制,就会带来额外的开销。

举例说明:

less 复制代码
"假设,此刻有任务A和任务B,现在有如下两种执行方式"
​
方式一:两个线程,一个线程执行A,另一个线程执行B
方式二:一个线程先执行A,执行完以后继续执行B
​
"请问,哪种方式执行更快?"
​
只见烟哥眉头微微一皱,说道:"我夜观天象,掐指一算,小刘你大学在上《计算机组成原理》这门课的时候,一定逃课了!"
​
"应该是方式二更快,因为方式一中,CPU在切换线程的时候,有一个上下文切换时间,而这个上下文切换时间是非常耗时的!打个比方,一个CPU主频是 2.6GHz,这意味着每秒可以执行:2.6*10^9 个指令,那么每个指令的时间大概是0.38ns!而一次上下文切换,将近需要耗时2000ns!而这个时间内,CPU什么都干不了,只是做了保存上下文都动作!"

拿 Redis 来说,Redis 有 List 的数据类型,并提供出队(LPOP)和入队(LPUSH)操作。

假设 Redis 采用多线程设计,如下图所示,现在有两个线程 A 和 B,线程 A 对一个 List 做 LPUSH 操作,并对队列长度加 1。同时,线程 B 对该 List 执行 LPOP 操作,并对队列长度减 1。

为了保证队列长度的正确性,Redis 需要让线程 A 和 B 的 LPUSH 和 LPOP 串行执行,这样一来,Redis 可以无误地记录它们对 List 长度的修改。

否则,我们可能就会得到错误的长度结果:-1。这就是多线程编程模式面临的共享资源的并发访问控制问题。

并发访问控制一直是多线程开发中的一个难点问题,如果没有精细的设计,比如说,只是简单地采用一个粗粒度互斥锁,就会出现不理想的结果:即使增加了线程,大部分线程也在等待获取访问共享资源的互斥锁,并行变串行,系统吞吐率并没有随着线程的增加而增加。

而且,采用多线程开发一般会引入同步来保护共享资源的并发访问,这也会降低系统代码的易调试性和可维护性。

为了避免多线程开发需要引入同步来保护共享资源的并发访问的问题,Redis 直接采用了单线程模式。

CPU不是瓶颈,不需要多线程利用CPU

Redis是基于内存的操作,CPU成为Redis的瓶颈的情况很少见。

Redis的瓶颈最有可能是内存的大小或者网络限制。

★单线程Redis为什么那么快?

通常来说,单线程的处理能力要比多线程差很多,但是 Redis 却能使用单线程模型达到每秒数十万级别的处理能力,这是为什么呢?

完全基于内存

绝大部分请求是纯粹的内存操作,非常快速,数据存在内存中。如果想要最大程度利用CPU,可以在一台机器上启动多个Redis实例。

数据结构简单

对数据操作也简单,Redis中的数据结构是专门进行设计的.类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1);

采用单线程

避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;

多路I/O复用模型,非阻塞IO

C语言编写离系统调用更近

原文链接:blog.csdn.net/vipshop_fin...

基本 IO 模型与阻塞点

单线程的缺点

注意:上面说的都是单线程的优点,但单线程也有其缺点,对于每个命令的执行时间有要求,如果某个命令执行时间过长,会造成其他命令阻塞,对于Redis这种高性能的服务来说是致命的。

首先,我们要弄明白网络操作的基本 IO 模型和潜在的阻塞点。毕竟,Redis 采用单线程进行 IO,如果线程被阻塞了,就无法进行多路复用了。

当 Redis 监听到一个客户端有连接请求,但一直未能成功建立起连接时,会阻塞在 accept() 函数这里,导致其他客户端无法和 Redis 建立连接。

类似的,当 Redis 通过 recv() 从一个客户端读取数据时,如果数据一直没有到达,Redis 也会一直阻塞在 recv()。

这就导致 Redis 整个线程阻塞,无法处理其他客户端请求,效率很低。

不过,幸运的是,socket 网络模型本身支持非阻塞模式。

非阻塞模式

Socket网络模型的非阻塞模式设置,主要体现在三个关键的函数上,如果想要使用 socket 非阻塞模式,就必须要了解这三个函数的调用返回类型和设置模式。

在 socket 模型中,不同操作调用后会返回不同的套接字类型。

socket() 方法会返回主动套接字,然后调用 listen() 方法,将主动套接字转化为监听套接字,此时,可以监听来自客户端的连接请求。

最后,调用 accept() 方法接收到达的客户端连接,并返回已连接套接字。

针对监听套接字,我们可以设置非阻塞模式:当 Redis 调用 accept() 但一直未有连接请求到达时,Redis 线程可以返回处理其他操作,而不用一直等待。

但是,你要注意的是,调用 accept() 时,已经存在监听套接字了。

虽然 Redis 线程可以不用继续等待,但是总得有机制继续在监听套接字上等待后续连接请求,并在有请求时通知 Redis。

类似的,我们也可以针对已连接套接字设置非阻塞模式:Redis 调用 recv() 后,如果已连接套接字一直没数据到达,Redis 线程同样可以返回处理其他操作。

我们也需要有机制继续监听该已连接套接字,并在有数据达到时通知 Redis。

这样才能保证 Redis 线程,既不会像基本 IO 模型中一直在阻塞点等待,也不会导致 Redis 无法处理实际到达的连接请求或数据。

到此,Linux 中的IO多路复用机制就要登场了。

★基于多路复用IO模型

Linux 中的IO多路复用是指一个线程处理多个IO流,就是我们经常听到的select/epoll机制。

简单来说,在 Redis 只运行单线程的情况下,该机制允许内核中,同时存在多个监听套接字和已连接套接字。内核会一直监听这些套接字上的连接请求或数据请求。一旦有请求到达,就会交给 Redis 线程处理,这就实现了一个 Redis 线程处理多个 IO 流的效果。

下图就是基于多路复用的 Redis IO 模型。

图中的多个 FD 就是刚才所说的多个套接字。Redis 网络框架调用 epoll 机制,让内核监听这些套接字。

此时,Redis 线程不会阻塞在某一个特定的监听或已连接套接字上,也就是说,不会阻塞在某一个特定的客户端请求处理上。

正因为此,Redis 可以同时和多个客户端连接并处理请求,从而提升并发性。

为了在请求到达时能通知到 Redis 线程,select/epoll 提供了基于事件的回调机制,即针对不同事件的发生,调用相应的处理函数。

那么,回调机制是怎么工作的呢?

其实,select/epoll 一旦监测到 FD 上有请求到达时,就会触发相应的事件。

这些事件会被放进一个事件队列,Redis 单线程对该事件队列不断进行处理。这样一来,Redis 无需一直轮询是否有请求实际发生,这就可以避免造成 CPU 资源浪费

同时,Redis 在对事件队列中的事件进行处理时,会调用相应的处理函数,这就实现了基于事件的回调。

因为 Redis 一直在对事件队列进行处理,所以能及时响应客户端请求,提升 Redis 的响应性能。

为了方便你理解,我再以连接请求和读数据请求为例,具体解释一下。

这两个请求分别对应Accept事件和Read事件,Redis 分别对这两个事件注册 accept 和 get 回调函数。当 Linux 内核监听到有连接请求或读数据请求时,就会触发 Accept 事件和 Read 事件,此时,内核就会回调 Redis 相应的 accept 和 get 函数进行处理。

这就像病人去医院瞧病。在医生实际诊断前,每个病人(等同于请求)都需要先分诊、测体温、登记等。

如果这些工作都由医生来完成,医生的工作效率就会很低。所以,医院都设置了分诊台,分诊台会一直处理这些诊断前的工作(类似于 Linux 内核监听请求),然后再转交给医生做实际诊断。

这样即使一个医生(相当于 Redis 单线程),效率也能提升。

不过,需要注意的是,即使你的应用场景中部署了不同的操作系统,多路复用机制也是适用的。因为这个机制的实现有很多种,既有基于 Linux 系统下的 select 和 epoll 实现,也有基于 FreeBSD 的 kqueue 实现,以及基于 Solaris 的 evport 实现,windows:IOCP这样,你可以根据 Redis 实际运行的操作系统,选择相应的多路复用实现。

Redis4.0-多线程

单线程问题:要删除一个大key时,del bigkey会一直阻塞,等待删除完成,才能继续操作,会导致Redis主线程卡顿

解决方法:引入了惰性删除有效避免Redis主线程卡顿。

lazy free的本质就是把某些cost(主要时间复制度,占用主线程cpu时间片)较高删除操作,从redis主线程剥离让BIO子线程来处理,极大地减少主线阻塞时间。从而减少删除导致性能和稳定性问题。

虽然引入了多个线程来实现数据的异步惰性删除等功能,但其处理读写请求的仍然只有一个线程,所以仍然是狭义单线程。

原文链接:blog.csdn.net/kuangd_1992...

Redis6.0多线程

引入的IO多线

既然单线程Redis这么优越,Redis 6.0引入的IO多线程岂不是多余?

非也,目前对于单线程Redis来说,性能瓶颈主要在于网络的 IO 消耗,优化主要有两个方向:

1.提高网络 IO 性能,典型的实现像使用 DPDK 来替代内核网络栈的方式

2.使用多线程充分利用多核,典型的实现像 Memcached

因为读写网络的read/write系统调用在Redis执行期间占用了大部分CPU时间,如果把网络读写做成多线程的方式对性能会有很大提升,这里所说的多线程,是通过系统调用写操作,将客户端的输入输出缓冲中的数据通过多线程IO与客户端交互。

现在已经实现了第一版,write side 即回复客户端这部分已经完成了,并且去掉了主线程和 IO线程之间的互斥锁,采用 busy loop 的形式来等待 io 线程工作结束,这部分能够有 50% 的性能提升。

多线程 IO 的读(请求)和写(响应)在实现流程是一样的,只是执行读还是写操作的差异。同时这些 IO 线程在同一时刻全部是读或者写,不会部分读或部分写的情况,所以下面以读流程作为例子。

分析过程中只会覆盖核心逻辑而不是全部细节。如果想完全理解细节,建议看完之后再次看一次源码实现。

加入多线程 IO 之后,整体的读流程如下:

1.主线程负责接收建立连接请求,读事件到来(收到请求)则放到一个全局等待读处理队列

2.主线程处理完读事件之后,通过轮询将连接分配给 IO 线程,然后主线程忙等待(spinlock 的效果)状态

3.IO 线程将请求数据读取并解析完成(这里只是读数据和解析并不执行命令)

4.主线程执行所有命令并清空整个请求等待读处理队列(执行部分串行)

上面的这个过程是完全无锁的,因为在 IO 线程处理的时主线程会等待全部的 IO 线程完成,所以不会出现data race的场景。

  • 线程A执行一个变量的写操作;
  • 线程B执行这个变量的读操作;
  • 这两个操作没有被正确的同步处理,也就是说他们执行的先后顺序是不确定的。
  • 当出现了以上类似情况我们通常称在这个变量上存在"数据竞争"(a data race)。一个存在数据竞争的程序都是没有正确同步过的程序,存在隐患。

回想我们写过的代码,有没有类似的场景?覆盖更新。

如果A和B同时修改某个数据的某个字段.就会出现覆盖更新的情况,那我们的代码里是否对这一问题做了处理呢?

答案是没有,因为类似的场景出现的频率太低。

同理在Redis中,A和B2个请求同时对1个key做了set操作,也可以认为是覆盖更新操作,那么需要处理吗?

答案是:看场景,如果存在类似的场景,那么可以加锁,判断A和B的先后顺序,比如状态是由待支付变为已支付。

那肯定是由待支付变为已支付。如果是先已支付再待支付肯定是有问题的。

Redis6.0多线程

Redis 一直被大家熟知的就是它的单线程架构,虽然有些命令操作可以用后台线程或子进程执行(比如数据删除、快照生成、AOF 重写),但是,从网络 IO 处理到实际的读写命令处理,都是由单个线程完成的。

redis 的瓶颈并不在 CPU,而在内存和网络io。随着网络硬件的性能提升,Redis 的性能瓶颈有时会出现在网络 IO 的处理上,也就是说,单个主线程处理网络请求的速度跟不上底层网络硬件的速度。

在 Redis 6.0 中,非常受关注的第一个新特性就是多线程。

Redis 6 中的多线程 主要在处理 网络 I/O 方面,多线程主要解决I/O读写瓶颈问题。

Redis 6.0 只有在网络请求的接收和解析,以及请求后的数据通过网络返回给时,使用了多线程。而数据读写操作还是由单线程来完成的(所以不存在数据安全问题)

但是,Redis 的多 IO 线程只是用来处理网络请求的,对于读写命令,Redis 仍然使用单线程来处理。这是因为,Redis 处理请求时,网络处理经常是瓶颈,通过多个 IO 线程并行处理网络操作,可以提升实例的整体处理性能。 而继续使用单线程执行命令操作,就不用为了保证 Lua 脚本、事务的原子性,额外开发多线程互斥机制了。这样一来,Redis 线程模型实现就简单了。

我们来看下,在 Redis 6.0 中,主线程和 IO 线程具体是怎么协作完成请求处理的。掌握了具体原理,你才能真正地会用多线程。

为了方便你理解,我们可以把主线程(单线程)和多 IO 线程的协作分成四个阶段。

阶段一:服务端和客户端建立 Socket 连接,并分配处理线程。首先,主线程负责接收建立连接请求。当有客户端请求和实例建立 Socket 连接时,主线程会创建和客户端的连接, 并把 Socket 放入全局等待队列中。紧接着,主线程通过轮询方法把 Socket 连接分配给 IO 线程。

阶段二:主线程一旦把 Socket 分配给 IO 线程,就会进入阻塞状态,等待 IO 线程完成客户端请求读取和解析。 因为有多个 IO 线程在并行处理,所以,这个过程很快就可以完成。

阶段三:主线程执行请求操作等到 IO 线程解析完请求,主线程还是会以单线程的方式执行这些命令操作。

下面这张图显示了刚才介绍的这三个阶段,你可以看下,加深理解。

阶段四:IO 线程回写 Socket 和主线程清空全局队列当主线程执行完请求操作后,会把需要返回的结果写入缓冲区,然后,主线程会阻塞等待 IO 线程把这些结果回写到 Socket 中,并返回给客户端。

和 IO 线程读取和解析请求一样,IO 线程回写 Socket 时,也是有多个线程在并发执行,所以回写 Socket 的速度也很快。等到 IO 线程回写 Socket 完毕,主线程会清空全局队列,等待客户端的后续请求。

我也画了一张图,展示了这个阶段主线程和 IO 线程的操作,你可以看下。

Redis------Redis6.0多线程问题_庄小焱-CSDN博客

参考:linuxcpp.0voice.com/?id=493

总结

现在,我们知道了,Redis 单线程是指它对网络 IO 和数据读写的操作采用了一个线程,而采用单线程的一个核心原因是避免多线程开发的并发控制问题和线程安全问题。

单线程的 Redis 也能获得高性能,跟多路复用的 IO 模型密切相关,因为这避免了 accept() 和 send()/recv() 潜在的网络 IO 操作阻塞点。

Redis3.0之前是单线程,Redis4.0使用多线程优化big key的删除,Redis 6.0引入的IO多线程,不管单线程还是多少线程,其实对应Redis不同模块而已。对于Redis 6.0 2019 年底发布,其将在性能、协议以及权限控制都会有很大的改进,还是值得期待的。

相关推荐
LuckyLay4 分钟前
Spring学习笔记_27——@EnableLoadTimeWeaving
java·spring boot·spring
向阳121817 分钟前
Dubbo负载均衡
java·运维·负载均衡·dubbo
水月梦镜花21 分钟前
redis:list列表命令和内部编码
数据库·redis·list
Gu Gu Study27 分钟前
【用Java学习数据结构系列】泛型上界与通配符上界
java·开发语言
WaaTong1 小时前
《重学Java设计模式》之 原型模式
java·设计模式·原型模式
m0_743048441 小时前
初识Java EE和Spring Boot
java·java-ee
AskHarries1 小时前
Java字节码增强库ByteBuddy
java·后端
佳佳_1 小时前
Spring Boot 应用启动时打印配置类信息
spring boot·后端
小灰灰__1 小时前
IDEA加载通义灵码插件及使用指南
java·ide·intellij-idea
夜雨翦春韭1 小时前
Java中的动态代理
java·开发语言·aop·动态代理