首页 > 数据库 >[Redis]Redis到底是单线程还是多线程程序?

[Redis]Redis到底是单线程还是多线程程序?

时间:2024-09-07 21:53:26浏览次数:11  
标签:epoll 单线程 Redis 线程 内核 IO 多线程

概述

这里我们先给出问题的全面回答:

Redis到底是多线程还是单线程程序要看是针对哪个功能而言,对于核心业务功能部分(命令操作处理数据),Redis是单线程的,主要是指 Redis 的网络 IO 和键值对读写是由一个线程来完成的,这也是 Redis 对外提供键值存储服务的主要流程,所以一般我们认为Redis是个单线程程序。但是从整个框架层面出发严格来说Redis是多线程的。

在Redis版本迭代过程中,在两个重要的时间节点上引入了多线程的支持:

Redis v4.0:引入多线程异步处理一些耗时较旧的任务,例如异步删除命令unlink,异步持久化等等
Redis v6.0:在核心网络模型中引入多线程,进一步提高对于多核CPU的利用率

Redis为什么采用单线程处理命令操作?

Redis的大部分操作都在内存中完成的,执行速度非常快,所以它的性能瓶颈是网络延迟而不是执行速度,因此多线程并不会带来巨大的性能提升。并且多线程会导致过多的上下文切换,带来不必要的性能开销。

同时多线程编程模式面临共享资源并发访问控制问题。并发访问控制一直是多线程开发中的一个难点问题,如果没有精细的设计,比如说,只是简单地采用一个粗粒度互斥锁,就会出现不理想的结果:即使增加了线程,大部分线程也在等待获取访问共享资源的互斥锁,并行变串行,系统吞吐率并没有随着线程的增加而增加。而且采用多线程开发一般会引入同步原语来保护共享资源的并发访问,这也会降低系统代码的易调试性和可维护性。为了避免这些问题,Redis 直接采用了单线程模式。

Redis采用单线程为什么还那么快?

通常来说,单线程的处理能力要比多线程差很多,但是 Redis 却能使用单线程模型达到每秒数十万级别的处理能力,这是为什么呢?其实,这是 Redis多方面设计选择的一个综合结果。一方面,Redis 的大部分操作在内存上完成,再加上它采用了高效的数据结构,例如哈希表和跳表,这是它实现高性能的一个重要原因。另一方面,就是Redis 采用了多路复用机制,使其在网络 IO 操作中能并发处理大量的客户端请求,实现高吞吐率,这也是Redis单线程如何处理那么多的并发客户端连接的核心所在。

在讲Redis网络模型之前先来看看应用是怎么和系统硬件进行交互的?用户应用如Redis,MySQL等其实是没有办法直接访问我们操作系统硬件的,只能先访问内核linux,再通过内核去访问计算机硬件。计算机硬件包括cpu,内存,网卡等等,内核(通过寻址空间)可以操作硬件的,但是内核需要不同设备的驱动,有了这些驱动之后,内核就可以去对计算机硬件去进行内存管理,文件系统的管理,进程的管理等等。

我们想要用户的应用来访问,计算机就必须要通过对外暴露的一些接口,才能访问到,从而简单的实现对内核的操控,但是内核本身上来说也是一个应用,所以他本身也需要一些内存,cpu等设备资源,用户应用本身也在消耗这些资源,如果不加任何限制,用户去操作随意的去操作我们的资源,就有可能导致一些冲突,甚至有可能导致我们的系统出现无法运行的问题,因此我们需要把用户和内核隔离开

进程的寻址空间划分成两部分:内核空间、用户空间

Linux系统为了提高IO效率,会在用户空间和内核空间都加入缓冲区:

写数据时,要把用户缓冲数据拷贝到内核缓冲区,然后写入设备

读数据时,要从设备读取数据到内核缓冲区,然后拷贝到用户缓冲区

image

针对这个操作:我们的用户在读读数据时,会去向内核态申请,想要读取内核的数据,而内核数据要去等待驱动程序从硬件上读取数据,当从磁盘上加载到数据之后,内核会将数据写入到内核的缓冲区中,然后再将数据拷贝到用户态的buffer中,然后再返回给应用程序,整体而言速度慢,我们希望read也好,还是wait for data也最好都不要等待,或者时间尽量的短。

当我们发送请求调用网络套接字socket的读写方法,默认它们是阻塞的,比如 read 方法要传递进去一个参数n,表示读取这么多字节后再返回,如果没有读够线程就会卡在那里,直到新的数据到来或者连接关闭了,read 方法才可以返回,线程才能继续处理。而 write 方法一般来说不会阻塞,除非内核为套接字分配的写缓冲区已经满了,write 方法就会阻塞,直到缓存区中有空闲空间挪出来了。这就导致 Redis 整个线程阻塞,无法处理其他客户端请求,效率很低。不过,幸运的是,socket 网络模型本身支持非阻塞模式,这时候IO多路复用事件驱动机制就闪亮登场了。

在单线程情况下,依次处理IO事件,如果正在处理的IO事件恰好未就绪(数据不可读或不可写),线程就会被阻塞,所有IO事件都必须等待,性能自然会很差。这里先来举个生动形象的例子来便于大家理解:众多客户端访问Redis服务,就好比去一家餐厅吃饭柜台就一个服务员点餐,每个顾客都要想一下吃什么(等待数据就绪),想好之后开始点餐(读取数据),这个过程后面的顾客(客户端)即使想好了点什么也只能白白干等着,可见这种方式能快起来吗?所以采取了另一种方式进餐厅告诉想吃饭不用排队,谁想好了吃什么(数据就绪了),就通知服务员给谁点餐(用户应用就去读取数据)。这样就有效提高了资源利用率。

本质来说,事件驱动是一种思想(事实上它不仅仅局限于编程) ,事件驱动思想是实现 异步非阻塞特性 的一个重要手段。对于web服务器来说,造成性能拉胯不支持高并发的常见原因就是由于使用了传统的I/O模型造成在内核没有可读/可写事件(或者说没有数据可供用户进程读写)时,用户线程 一直在等待(其他事情啥也干不了就是干等等待内核上的数据可读/可写),这样的话其实是一个线程(ps:线程在Linux系统也是进程)对应一个请求,请求是无限的,而线程是有限的从而也就形成了并发瓶颈。而大佬们为了解决此类问题,运用了事件驱动思想来对传统I/O模型做个改造,即在客户端发起请求后,用户线程不再阻塞等待内核数据就绪,而是立即返回(可以去执行其他业务逻辑或者继续处理其他请求)。当内核的I/O操作完成后,内核系统会向用户线程发送一个事件通知,用户线程才来处理这个读/写操作,之后拿到数据再做些其他业务后响应给客户端,从而完成一次客户端请求的处理。事件驱动的I/O模型中,程序不必阻塞等待I/O操作的完成,也无需为每个请求创建一个线程,从而提高了系统的并发处理能力和响应速度。事件驱动型的I/O模型通常也被被称为I/O多路复用,即这种模型可以在一个线程中,处理多个连接(复用就是指多个连接复用一个线程,多路也即所谓的 多个连接),通过这种方式避免了线程间切换的开销,同时也使得用户线程不再被阻塞,提高了系统的性能和可靠性。Redis支持事件驱动是因为他利用了操作系统提供的I/O多路复用接口,如Linux系统中,常用的I/O多路复用接口有select/poll,epoll。这些接口可以监视多个文件描述符FD的状态变化,当文件描述符可读或可写时,就会向用户线程发送一个事件通知。用户线程通过事件处理机制(读取/写入数据)来处理这个事件,之后进行对应的业务逻辑完了进行响应。简单一句话概括: 事件驱动机制就是指当有读/写/连接事件就绪时 再去做读/写/接受连接这些事情,而不是一直在那里傻傻的等,也正应了他的名词: 【事件驱动!】,基于事件驱动思想设计的多路复用I/O(如select/poll,epoll),相对于传统I/O模型,达到了异步非阻塞的效果! linux采用的epoll机制,接下来就让我们详细看看。

文件描述符(File Descriptor) :简称FD,是一个从0 开始的无符号整数,用来关联Linux中的一个文件。在Linux中,一切皆文件,例如常规文件、视频、硬件设备等,当然也包括网络套接字(Socket)。

IO多路复用: 是利用单个线程来同时监听多个FD,并在某个FD可读、可写时得到通知,从而避免无效的等待,充分利用CPU资源。

epoll 是 Linux 上高效的多路复用机制,它与传统的 select 和 poll 相比,有更好的性能。

注册事件: 程序通过 epoll_create 创建一个 epoll 对象,然后使用 epoll_ctl 向其中注册需要监视的文件描述符和关注的事件,如读或写事件。

int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

等待事件: 使用 epoll_wait 等待文件描述符上的事件发生。epoll_wait 会阻塞,直到注册的文件描述符中的事件发生或者超时。

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

处理事件: 当 epoll_wait 返回时,程序可以迭代处理发生的事件,执行相应的读或写操作。

for (int i = 0; i < nfds; ++i) {
    if (events[i].events & EPOLLIN) {
        // 处理读事件
    }
    if (events[i].events & EPOLLOUT) {
        // 处理写事件
    }
}

epoll 采用了事件通知的机制,只有真正发生事件时才进行处理,避免了轮询的开销,因此在处理大量并发连接时性能更好。

总的来说,多路复用通过允许单一进程或线程同时监视多个文件描述符,提高了 I/O 操作的效率,特别适用于高并发的网络应用场景

image

select模式存在的三个问题:能监听的FD最大不超过1024。每次select都需要把所有要监听的FD都拷贝到内核空间每次都要遍历所有FD来判断就绪状态。

poll模式的问题:poll利用链表解决了select中监听FD上限的问题,但依然要遍历所有FD,如果监听较多,性能会下降epoll模式中如何解决这些问题的?

基于epoll实例中的红黑树保存要监听的FD,理论上无上限,而且增删改查效率都非常高每个FD只需要执行一次epoll_ctl添加到红黑树,以后每次epol_wait无需传递任何参数,无需重复拷贝FD到内核空间利用ep_poll_callback机制来监听FD状态,无需遍历所有FD,因此性能不会随监听的FD数量增多而下降。

下面就来看看Redis基于IO多路复用事件驱动机制实现的网络模型:

image

当我们的客户端想要去连接我们服务器,会去先到IO多路复用模型去进行排队,会有一个连接应答处理器,他会去接受读请求,然后又把读请求注册到具体模型中去,此时这些建立起来的连接,如果是客户端请求处理器去进行执行命令时,他会去把数据读取出来,然后把数据放入到client中, clinet去解析当前的命令转化为redis认识的命令,接下来就开始处理这些命令,从redis中的command中找到这些命令,然后就真正的去操作对应的数据了,当数据操作完成后,会去找到命令回复处理器,再由他将数据写出。大体流程如下:

1.Redis服务启动之后,就会创建一个server Socket服务器套接字得到对应文件描述符FD,调用epoll机制进行注册监听

2.客户端client进行连接,服务器套接字的FD会进入就绪,进行回调事件tcpAccepthandler处理,调用accept()接收客户端socket,得到对应FD进行注册监听。

3.当客户端socket FD就绪,会调用相应的可读readQueryFromClient读取请求数据,或者可写事件sendReplyToClient写出相应数据。

项目推荐:基于SpringBoot2.x、SpringCloud和SpringCloudAlibaba企业级系统架构底层框架封装,解决业务开发时常见的非功能性需求,防止重复造轮子,方便业务快速开发和企业技术栈框架统一管理。引入组件化的思想实现高内聚低耦合并且高度可配置化,做到可插拔。严格控制包依赖和统一版本管理,做到最少化依赖。注重代码规范和注释,非常适合个人学习和企业使用

Github地址:github.com/plasticene/…

Gitee地址:gitee.com/plasticene3…

微信公众号:Shepherd进阶笔记

交流探讨qun:Shepherd_126

Redis6.0为什么采用了多线程

上文说的单线程指的是从网络 IO 处理到实际的读写命令处理,都是由单个线程完成的。

随着网络硬件的性能提升,Redis 的性能瓶颈有时会出现在网络 IO 的处理上,也就是说,单个主线程处理网络请求的速度跟不上底层网络硬件的速度。采用多线程 I/O 可以让 Redis 在一个单一进程内同时处理多个客户端的请求,充分利用多核处理器的优势,提高系统的并发性能,提升系统吞吐量。

Redis 的多线程 I/O类似于一个餐厅,只有一名服务员负责接受客人点餐(处理命令的执行),而多名厨师则负责烹饪和准备食物(处理 I/O 操作,如读取和写入)。在这个场景中,服务员负责与客人直接交互,接受点餐信息,这相当于 Redis 的主线程负责处理命令。而厨师在后厨专注于将点餐信息转化为实际的菜品,这就类似于 Redis 的工作线程专注于处理 I/O 操作,如读取和写入数据。

我们来看下,在 Redis 6.0 中,主线程和 IO 线程具体是怎么协作完成请求处理的。掌握了具体原理,你才能真正地会用多线程。为了方便你理解,我们可以把主线程和多 IO 线程的协作分成四个阶段。

阶段一:服务端和客户端建立 Socket 连接,并分配处理线程

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

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

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

image

阶段四:IO 线程回写 Socket 和主线程清空全局队列当主线程执行完请求操作后,会把需要返回的结果写入缓冲区,然后,主线程会阻塞等待 IO 线程把这些结果回写到 Socket 中,并返回给客户端。和 IO 线程读取和解析请求一样,IO 线程回写 Socket 时,也是有多个线程在并发执行,所以回写 Socket 的速度也很快。等到 IO 线程回写 Socket 完毕,主线程会清空全局队列,等待客户端的后续请求。

image

了解了 Redis 主线程和多线程的协作方式,我们该怎么启用多线程呢?在 Redis 6.0 中,多线程机制默认是关闭的,如果需要使用多线程功能,需要在 redis.conf 中完成两个设置。

  1. 设置 io-threads-do-reads 配置项为 yes,表示启用多线程。
    io-threads-do-reads yes
  2. 设置线程个数。一般来说,线程个数要小于 Redis 实例所在机器的 CPU 核个数,例如,对于一个 8 核的机器来说,Redis 官方建议配置 6 个 IO 线程。
    io-threads 6

如果你在实际应用中,发现 Redis 实例的 CPU 开销不大,吞吐量却没有提升,可以考虑使用 Redis 6.0 的多线程机制,加速网络处理,进而提升实例的吞吐量。

总结

Redis 的网络模型主要使用 epoll(在 Linux 上)作为事件通知机制,并且在版本 6.0 中引入了多线程 I/O 模型。

epoll:
设计目标: 主要用于实现单线程的事件驱动模型,处理大量的并发连接。
机制: Redis 使用 epoll 机制,通过单个线程监听多个文件描述符上的事件,实现非阻塞 I/O。当某个连接有数据到达时,通过事件通知机制触发相应的处理。
适用场景: 适用于 I/O 密集型的场景,其中大量的连接需要同时被高效处理,例如网络通信密集型的应用场景。

多线程 I/O:
设计目标: 引入多线程用于更好地利用多核处理器,提高系统的并发性能。
机制: Redis 6.0 引入了多线程 I/O 模型,其中一个线程负责处理命令的执行,而其他的工作线程则负责处理 I/O 操作,如读取和写入。这样可以同时处理多个连接的 I/O 操作。
适用场景: 适用于需要更好地利用多核处理器、同时处理大量 I/O 操作的场景。特别在 CPU 密集型的情况下,通过多线程可以提高性能。

标签:epoll,单线程,Redis,线程,内核,IO,多线程
From: https://www.cnblogs.com/DCFV/p/18402169

相关文章

  • Java多线程中常见死锁问题及解决方案
    在编写Java多线程代码的时候,很难避免会出现线程安全问题,在线程安全问题中也有一个很常见的现象就是死锁现象。今天我们就来聊一聊Java中的死锁问题,以及如何避免死锁问题。本次知识点讲解建立在大家已经知道“锁”......
  • Centos7怎么安装Redis5.0
    Centos7怎么安装Redis5.0转载:https://www.php.cn/faq/553616.htmlWBOY发布:2023-06-0119:08:49转载1737人浏览过 一、安装gcc依赖由于 redis 是用C语言开发,安装之前必先确认是否安装gcc环境(gcc-v),如果没有安装,执行以下命令进行安装 [root@localho......
  • 【redis】数据量庞大时的应对策略
    文章目录为什么数据量多了主机会崩分布式系统应用数据分离架构应用服务集群架构负载均衡器数据库读写分离引入缓存冷热分离架构分库分表微服务是什么代价优势为什么数据量多了主机会崩一台主机的硬件资源是有上限的,包括但不限于一下几种:CPU内存硬盘网络…服务器......
  • 【redis】redis编译和redis.conf配置
    下载源码reids解压编译#解压tar-zxvfredis-5.0.14.tar.gzcdredis-5.0.14/makePREFIX=/opt/redisinstall#requirepassroot#开启远程访问bind0.0.0.0protected-modeno#修改日志打印路径,修改redis.confdaemonizeyeslogfile/var/log/redis.lo......
  • redis的主从复制、哨兵和集群部署
    Redis的主从复制主从复制引言实际生产环境下,单机的redis服务器是无法满足实际的生产需求的。第一,单机的redis服务器很容易发生单点故障,即使redis提供了各种持久化的方法来避免数据的丢失,但是物理上的故障(硬盘损毁等)还是无法完全避免的。第二,如果对单台机器的性能进行纵......
  • redis的基本使用
    Redis简介Redis是完全开源免费的,遵守BSD协议,高性能的基于键值对(key-value)的NoSQL(NotOnlySQL)数据库。SQL(StructQueryLanauge结构化的查询语言)。引申含义RDBMS产品,传统的关系型数据库,存储格式化的表格数据。NOSQL(NotOnlySQL)不仅仅只有关系型数据库。引申含......
  • Redis 哨兵模式搭建
    1.Redis:Redis是一款基于内存的非关系型数据库(5种类型String哈希ListSetZset)可能会发生的故障(缓存击穿:某热点数据或者没有缓存的时候直接打到数据库上、缓存穿透:大量请求查询不存在的数据,直接打到数据库上、缓存雪崩:缓存过期或者不存在打到数据库上)持久化RDB(RedisD......
  • Redis MGET实现机制解析
    Redis是一种广泛应用于分布式系统中的内存数据库,以其高效的存储和访问方式著称。而在高并发的应用场景中,Redis提供了多种数据获取方式,其中MGET是用于一次获取多个键值对的命令。与GET一次获取一个键值不同,MGET可以在一次请求中返回多个键的值,显著提高了读取性能,减少了网络往......
  • Redis使用场景
    Redis使用场景目录缓存缓存穿透缓存击穿缓存雪崩双写一致性持久化数据过期策略数据淘汰策略分布式锁实现原理(setnx、redission)其他哨兵模式、集群脑裂分片集群、数据读取规则redis是单线程的却很快缓存一、缓存穿透定义:查询一个不存在的数据,Mysql查......
  • docker 安装 redis 集群
    集群搭建(三主三从)集群搭建集群中的节点都需要打开两个TCP连接。一个连接用于正常的给Client提供服务,比如6379,还有一个额外的端口(通过在这个端口号上加10000)作为数据端口,例如:redis的端口为6379,那么另外一个需要开通的端口是:6379+10000,即需要开启16379。16379端口用于......