单线程模型展开目录
Redis 客户端对服务端的每次调用都经历了发送命令,执行命令,返回结果三个过程。其中执行命令阶段,由于 Redis 是单线程来处理命令的,所有每一条到达服务端的命令不会立刻执行,所有的命令都会进入一个队列中,然后逐个被执行。并且多个客户端发送的命令的执行顺序是不确定的。但是可以确定的是不会有两条命令被同时执行,不会产生并发问题,这就是 Redis 的单线程基本模型。
1. redis 单线程问题展开目录
单线程指的是网络请求模块使用了一个线程(所以不需考虑并发安全性),即一个线程处理所有网络请求,其他模块仍用了多个线程。
2. 为什么说 redis 能够快速执行展开目录
(1) 绝大部分请求是纯粹的内存操作(非常快速)
(2) 采用单线程,避免了不必要的上下文切换和竞争条件
(3) 非阻塞 IO - IO 多路复用,Redis 采用 epoll 做为 I/O 多路复用技术的实现,再加上 Redis 自身的事件处理模型将 epoll 中的连接,读写,关闭都转换为了时间,不在 I/O 上浪费过多的时间。
Redis 采用单线程模型,每条命令执行如果占用大量时间,会造成其他线程阻塞,对于 Redis 这种高性能服务是致命的,所以 Redis 是面向高速执行的数据库。
3. redis 的内部实现展开目录
内部实现采用 epoll,采用了 epoll + 自己实现的简单的事件框架。epoll 中的读、写、关闭、连接都转化成了事件,然后利用 epoll 的多路复用特性,绝不在 io 上浪费一点时间 这 3 个条件不是相互独立的,特别是第一条,如果请求都是耗时的,采用单线程吞吐量及性能可想而知了。应该说 redis 为特殊的场景选择了合适的技术方案。
4. Redis 关于线程安全问题展开目录
redis 实际上是采用了线程封闭的观念,把任务封闭在一个线程,自然避免了线程安全问题,不过对于需要依赖多个 redis 操作的复合操作来说,依然需要锁,而且有可能是分布式锁。
另一篇对 redis 单线程的理解:Redis 单线程理解
个人理解展开目录
redis 分客户端和服务端,一次完整的 redis 请求事件有多个阶段(客户端到服务器的网络连接 -->redis 读写事件发生 -->redis 服务端的数据处理(单线程)--> 数据返回)。平时所说的 redis 单线程模型,本质上指的是服务端的数据处理阶段,不牵扯网络连接和数据返回,这是理解 redis 单线程的第一步。接下来,针对不同阶段分别阐述个人的一些理解。
1. 客户端到服务器的网络连接展开目录
首先,客户端和服务器是 socket 通信方式,socket 服务端监听可同时接受多个客户端请求,这点很重要,如果不理解可先记住。注意这里可以理解为本质上与 redis 无关,这里仅仅做网络连接,或者可以理解为,为 redis 服务端提供网络交互 api。假设建立网络连接需要 30 秒(为了更容易理解,所以时间上扩大了 N 倍)
2. redis 读写事件发生并向服务端发送请求数据展开目录
首先确定一点,redis 的客户端与服务器端通信是基于 TCP 连接(不懂去看,基础很重要),第一阶段仅仅是建立了客户端到服务器的网络连接,然后才是发生第二阶段的读写事件。完成了上一个阶段的网络连接,redis 客户端开始真正向服务器发起读写事件,假设是 set(写)事件,此时 redis 客户端开始向建立的网络流中送数据,服务端可以理解为给每一个网络连接创建一个线程同时接收客户端的请求数据。假设从客户端发数据,到服务端接收完数据需要 10 秒。
3. redis 服务端的数据处理展开目录
服务端完成了第二阶段的数据接收,接下来开始依据接收到的数据做逻辑处理,然后得到处理后的数据。数据处理可以理解为一次方法调用,带参调用方法,最终得到方法返回值。不要想复杂,重在理解流程。假设 redis 服务端处理数据需要 0.1 秒。
4. 数据返回展开目录
这一阶段很简单,当 reids 服务端数据处理完后 就会立即返回处理后的数据,没什么特别需要强调的。假设服务端把处理后的数据回送给客户端需要 5 秒。
那么什么是 Reids 的单线程展开目录
第一阶段说过,redis 是以 socket 方式通信,socket 服务端可同时接受多个客户端请求连接,也就是说,redis 服务同时面对多个 redis 客户端连接请求,而 redis 服务本身是单线程运行。
假设,现在有 A,B,C,D,E 五个客户端同时发起 redis 请求,A 优先稍微一点点第一个到达,然后是 B,C,D,E 依次到达,此时 redis 服务端开始处理 A 请求,建立连接需要 30 秒,获取请求数据需要 10 秒,然后处理数据需要 0.1 秒,回送数据给客户端需要 5 秒,总共大概需要 45 秒。也就是说,下一个 B 请求需要等待 45 秒,这里注意,也许这五个几乎同时请求,由于 socket 可以同时处理多个请求,所以建立网络连接阶段时间差可忽略,但是在第二阶段,服务端需要什么事都不干,坐等 10 秒中,对于 CPU 和客户端来说是无法忍受的。所以说单线程效率非常,非常低,但是正是因为这些类似问题,Redis 单线程本质上并不是如此运行。接下来讨论 redis 真正的单线程运行方式。
客户端与服务端建立连接交由 socket,可以同时建立多个连接(这里应该是多线程 / 多进程),建立的连接 redis 是知道的(为什么知道,去看 socket 编程,再次强调基础很重要),然后 redis 会基于这些建立的连接去探测哪个连接已经接收完了客户端的请求数据(注意:不是探测哪个连接建立好了,而是探测哪个接收完了请求数据),而且这里的探测动作就是单线程的开始,一旦探测到则基于接收到的数据开始数据处理阶段,然后返回数据,再继续探测下一个已经接收完请求数据的网络连接。注意,从探测到数据处理再到数据返回,全程单线程。这应该就是所谓的 redis 单线程。至于内部有多复杂我们无需关心,我们追求的是理解流程,苛求原理,但不能把内脏都挖出来。
从探测到接受完请求数据的网络连接到最终的数据返回,服务器只需要 5.1 秒,这个时间是我放大 N 倍后的数据,实际时间远远小于这个,可能是 5.1 的 N 万分之一时间,为什么这么说,因为数据的处理是在本地内存中,速度有多快任你想象,最终的返回数据虽然牵扯到网络,但是网络连接已经建立,这个速度也是非常非常快的,只是比数据处理阶段慢那么一点点。因此单线程方式在效率上其实并不需要担心。
IO 多路复用展开目录
参考:https://www.zhihu.com/question/32163005 要弄清问题先要知道问题的出现原因
原因:
由于进程的执行过程是线性的 (也就是顺序执行), 当我们调用低速系统 I/O (read,write,accept 等等), 进程可能阻塞,此时进程就阻塞在这个调用上,不能执行其他操作。阻塞很正常.
接下来考虑这么一个问题:一个服务器进程和一个客户端进程通信,服务器端 read (sockfd1,bud,bufsize), 此时客户端进程没有发送数据,那么 read (阻塞调用) 将阻塞,直到客户端调用 write (sockfd,but,size) 发来数据。在一个客户和服务器通信时这没什么问题;
当多个客户与服务器通信时当多个客户与服务器通信时,若服务器阻塞于其中一个客户 sockfd1, 当另一个客户的数据到达套接字 sockfd2 时,服务器不能处理,仍然阻塞在 read (sockfd1,...) 上;此时问题就出现了,不能及时处理另一个客户的服务,咋么办?
I/O 多路复用来解决!
I/O 多路复用:
继续上面的问题,有多个客户连接,sockfd1,sockfd2,sockfd3..sockfdn 同时监听这 n 个客户,当其中有一个发来消息时就从 select 的阻塞中返回,然后就调用 read 读取收到消息的 sockfd, 然后又循环回 select 阻塞;这样就不会因为阻塞在其中一个上而不能处理另一个客户的消息
“I/O 多路复用” 的英文是 “I/O multiplexing”, 可以百度一下 multiplexing,就能得到这个图:
Q:
那这样子,在读取 socket1 的数据时,如果其它 socket 有数据来,那么也要等到 socket1 读取完了才能继续读取其它 socket 的数据吧。那不是也阻塞住了吗?而且读取到的数据也要开启线程处理吧,那这和多线程 IO 有什么区别呢?
A:
1.CPU 本来就是线性的不论什么都需要顺序处理并行只能是多核 CPU
2.io 多路复用本来就是用来解决对多个 I/O 监听时,一个 I/O 阻塞影响其他 I/O 的问题,跟多线程没关系.
3. 跟多线程相比较,线程切换需要切换到内核进行线程切换,需要消耗时间和资源。而 I/O 多路复用不需要切换线 / 进程,效率相对较高,特别是对高并发的应用 nginx 就是用 I/O 多路复用,故而性能极佳。但多线程编程逻辑和处理上比 I/O 多路复用简单。而 I/O 多路复用处理起来较为复杂.
I/O 指的是网络 I/O。
多路指的是多个 TCP 连接(Socket 或 Channel)。
复用指的是复用一个或多个线程。
它的基本原理就是不再由应用程序自己监视连接,而是由内核替应用程序监视文件描述符。
客户端在操作的时候,会产生具有不同事件类型的 socket。在服务端,I/O 多路复用程序(I/O Multiplexing Module)会把消息放入队列中,然后通过文件事件分派器(Fileevent Dispatcher),转发到不同的事件处理器中。
多路复用有很多的实现,以 select 为例,当用户进程调用了多路复用器,进程会被阻塞。内核会监视多路复用器负责的所有 socket,当任何一个 socket 的数据准备好了,多路复用器就会返回。这时候用户进程再调用 read 操作,把数据从内核缓冲区拷贝到用户空间。
I/O 多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪(readable)状态,select () 函数就可以返回。
Redis 单线程 是否就代表线程安全?
- import redis.clients.jedis.Jedis;
- import redis.clients.jedis.JedisPool;
-
- class Demo extends Thread
- {
- public void run()
- {
- Jedis jedis1 = new Jedis();
- for (int i=0;i<100;i++){
- int num = Integer.parseInt(jedis1.get("num"));// 1: 代码行1
- num = num + 1; // 2: 代码行2
- jedis1.set("num",num+"");
- System.out.println(jedis1.get("num"));
- }
- }
- }
-
- public class test{
-
- public static void main(String... args){
- Jedis jedis = new Jedis();
- jedis.set("num","1");
- new Demo().start();
- new Demo().start();
- }
- }
如代码所示,例如当线程 1 在代码行读取数值为 99 时候,此时线程 2 页执行读取操作也是 99,随后同时执行 num=num+1,之后更新,导致一次更新丢失,这就是这个代码测试的错误之处。所以 Redis 本身是线程安全的,但是你还需要保证你的业务必须也是线程安全的。
注意:千万不要以为原子操作是线程安全的,原子操作只能保证命令全执行或者全不执行,并不会保证线程安全操作。例如数据库中的事务就是原子的,依旧还需要提供并发控制!!!!
原子性操作是否线程安全?
原文:https://stackoverflow.com/questions/14370575/why-are-atomic-operations-considered-thread-safe
1. 原子操作是针对访问共享变量的操作而言的。涉及局部变量访问的操作无所谓是否原子的。
2. 原子操作是从该操作的执行线程以外的线程来描述的,也就是说它只有在多线程环境下才有意义。
原子操作得 “不可分割” 包括两层含义
1. 访问(读、写)某个共享变量的操作从其执行线程以外的任何线程来看,该操作要么已经执行结束要么尚未发生,* 即其他线程不会 “看到” 该操作执行了部分的中间效果。*
2. 访问同一组共享变量的原子操作是不能够被交错的。
此原子性与数据库原子性有区别:最主要区别是数据库的原子性,可以被其他线程看见中间状态,否则就不会有隔离级别的事了。
另一篇关于 redis I/O 多路复用的介绍展开目录
redis I/O 多路复用机制
为什么 redis 要使用 I/O 多路复用技术?
Redis 是跑在单线程中的,所有的操作都是按照顺序线性执行的,但是由于读写操作等待用户输入或输出都是阻塞的,所以 I/O 操作在一般情况下往往不能直接返回,这会导致某一文件的 I/O 阻塞导致整个进程无法对其它客户提供服务,而 I/O 多路复用就是为了解决这个问题而出现的。
多路 I/O 复用模型是利用 select、poll、epoll 可以同时监察多个流的 I/O 事件的能力,在空闲的时候,会把当前线程阻塞掉。当有一个或多个流有 I/O 事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll 是只轮询那些真正发出了事件的流),并且只依次顺序的处理就绪的流,这种做法就避免了大量的无用操作。
这里 “多路” 指的是多个网络连接,“复用” 指的是复用同一个线程。
采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络 IO 的时间消耗),且 Redis 在内存中操作数据的速度非常快,也就是说内存内的操作不会成为影响 Redis 性能的瓶颈,主要由以下几点造就了 Redis 具有很高的吞吐量。
(1) 网络 IO 都是通过 Socket 实现,Server 在某一个端口持续监听,客户端通过 Socket(IP+Port)与服务器建立连接(ServerSocket.accept),成功建立连接之后,就可以使用 Socket 中封装的 InputStream 和 OutputStream 进行 IO 交互了。针对每个客户端,Server 都会创建一个新线程专门用于处理。
(2) 默认情况下,网络 IO 是阻塞模式,即服务器线程在数据到来之前处于【阻塞】状态,等到数据到达,会自动唤醒服务器线程,着手进行处理。阻塞模式下,一个线程只能处理一个流的 IO 事件。
(3) 为了提升服务器线程处理效率,有以下三种思路
a. 非阻塞 [忙轮询]: 采用死循环方式轮询每一个流,如果有 IO 事件就处理,这样一个线程可以处理多个流,但效率不高,容易导致 CPU 空转。
b.Select 代理 (无差别轮询): 可以观察多个流的 IO 事件,如果所有流都没有 IO 事件,则将线程进入阻塞状态,如果有一个或多个发生了 IO 事件,则唤醒线程去处理。但是会遍历所有的流,找出流需要处理的流。如果流个数为 N,则时间复杂度为 O (N)
c.Epoll 代理:Select 代理有一个缺点,线程在被唤醒后轮询所有的 Stream,会存在无效操作。Epoll 哪个流发生了 I/O 事件会通知处理线程,对流的操作都是有意义的,复杂度降低到了 O (1)。
举个栗子:
每个快递员 ------------------> 每个线程
每个快递 --------------------> 每个 socket (I/O 流)
快递的物流位置 -------------->socket 的不同状态
客户 (寄 / 收) 快递请求 --------------> 来自客户端的请求
快递公司的经营方式 --------------> 服务端运行的代码
一辆车 ---------------------->CPU 的核数
1. 经营方式一就是传统的并发模型,每个 I/O 流 (快递) 都有一个新的线程 (快递员) 管理。
2. 经营方式二就是 I/O 多路复用。只有单个线程 (一个快递员),通过跟踪每个 I/O 流 (快递) 的状态 (每个快递的送达地点),来管理多个 I/O 流。
redis 线程模型:
epoll IO 多路复用模型实现机制
epoll 没有这个限制,它所支持的 FD 上限是最大可以打开文件的数目,这个数字一般远大于 2048, 一般来说这个数目和系统内存关系很大,具体数目可以 cat /proc/sys/fs/file-max 查看,在 1GB 内存的机器上大约是 10 万左右
场景:有 100 万个客户端同时与一个服务器进程保持着 TCP 连接。而每一时刻,通常只有几百上千个 TCP 连接是活跃的。
在 select/poll 时代,服务器进程每次都把这 100 万个连接告诉操作系统 (从用户态复制句柄数据结构到内核态),让操作系统内核去查询这些套接字上是否有事件发生,轮询完后,再将句柄数据复制到用户态,让服务器应用程序轮询处理已发生的网络事件,这一过程资源消耗较大,因此,select/poll 一般只能处理几千的并发连接。
如果没有 I/O 事件产生,我们的程序就会阻塞在 select 处。有个问题,我们从 select 仅知道了有 I/O 事件发生了,但却不知是哪几个流,只能无差别轮询所有流,找出能读或写数据的流进行操作。
使用 select,O (n) 的无差别轮询复杂度,同时处理的流越多,每一次无差别轮询时间就越长。
epoll 的设计和实现与 select 完全不同。epoll 通过在 Linux 内核中申请一个简易的文件系统 (文件系统一般用 B + 树数据结构实现)。把原先的 select/poll 调用分成了 3 个部分:
1)调用 epoll_create () 建立一个 epoll 对象 (在 epoll 文件系统中为这个句柄对象分配资源)
2)调用 epoll_ctl 向 epoll 对象中添加这 100 万个连接的套接字
3)调用 epoll_wait 收集发生的事件的连接实现上面场景只需要在进程启动时建立一个 epoll 对象,在需要的时候向 epoll 对象中添加或者删除连接。同时 epoll_wait 的效率也非常高,因为调用 epoll_wait 时,并没有一股脑的向操作系统复制这 100 万个连接的句柄数据,内核也不需要去遍历全部的连接。
底层实现:
当某一进程调用 epoll_create 方法时,Linux 内核会创建一个 eventpoll 结构体,这个结构体中有两个成员与 epoll 的使用方式密切相关。eventpoll 结构体如下所示:
每一个 epoll 对象都有一个独立的 eventpoll 结构体,用于存放通过 epoll_ctl 方法向 epoll 对象中添加进来的事件。这些事件都会挂载在红黑树中,通过红黑树可以高效的识别重复事件 (红黑树的插入时间效率是 lg (n),其中 n 为树的高度)。
所有添加到 epoll 中的事件都会与设备 (网卡) 驱动程序建立回调关系,当相应的事件发生时会调用这个回调方法。这个回调方法在内核中叫 ep_poll_callback, 它会将发生的事件添加到 rdlist 双链表中。
在 epoll 中,对于每一个事件,都会建立一个 epitem 结构体,如下所示:
当调用 epoll_wait 检查是否有事件发生时,只需要检查 eventpoll 对象中的 rdlist 双链表中是否有 epitem 元素即可。如果 rdlist 不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户。
优势:
1. 不用重复传递。我们调用 epoll_wait 时就相当于以往调用 select/poll,但是这时却不用传递 socket 句柄给内核,因为内核已经在 epoll_ctl 中拿到了要监控的句柄列表。
2. 在内核里,一切皆文件。epoll 向内核注册了一个文件系统,用于存储上述的被监控 socket。当你调用 epoll_create 时,就会在这个虚拟的 epoll 文件系统里创建一个 file 结点。这个 file 不是普通文件,它只服务于 epoll。epoll 在被内核初始化时(操作系统启动),同时会开辟出 epoll 自己的内核高速 cache 区,用于安置每一个我们想监控的 socket,这些 socket 会以红黑树的形式保存在内核 cache 里,以支持快速的查找、插入、删除。这个内核高速 cache 区,就是建立连续的物理内存页,然后在之上建立 slab 层,简单的说,就是物理上分配好你想要的 size 的内存对象,每次使用时都是使用空闲的已分配好的对象。
3. 极其高效的原因:我们在调用 epoll_create 时,内核除了帮我们在 epoll 文件系统里建了个 file 结点,在内核 cache 里建了个红黑树用于存储以后 epoll_ctl 传来的 socket 外,还会再建立一个 list 链表,用于存储准备就绪的事件,当 epoll_wait 调用时,仅仅观察这个 list 链表里有没有数据即可。有数据就返回,没有数据就 sleep,等到 timeout 时间到后即使链表没数据也返回。
这个准备就绪 list 链表是怎么维护的呢?当我们执行 epoll_ctl 时,除了把 socket 放到 epoll 文件系统里 file 对象对应的红黑树上之外,还会给内核中断处理程序注册一个回调函数,告诉内核,如果这个句柄的中断到了,就把它放到准备就绪 list 链表里。所以,当一个 socket 上有数据到了,内核在把网卡上的数据 copy 到内核中后就来把 socket 插入到准备就绪链表里了。epoll 的基础就是回调呀!
一颗红黑树,一张准备就绪句柄链表,少量的内核 cache,就帮我们解决了大并发下的 socket 处理问题。执行 epoll_create 时,创建了红黑树和就绪链表,执行 epoll_ctl 时,如果增加 socket 句柄,则检查在红黑树中是否存在,存在立即返回,不存在则添加到树干上,然后向内核注册回调函数,用于当中断事件来临时向准备就绪链表中插入数据。执行 epoll_wait 时立刻返回准备就绪链表里的数据即可。
epoll 独有的两种模式 LT 和 ET。无论是 LT 和 ET 模式,都适用于以上所说的流程。区别是,LT 模式下,只要一个句柄上的事件一次没有处理完,会在以后调用 epoll_wait 时次次返回这个句柄,而 ET 模式仅在第一次返回。
LT 和 ET 都是电子里面的术语,ET 是边缘触发,LT 是水平触发,一个表示只有在变化的边际触发,一个表示在某个阶段都会触发。
LT, ET 这件事怎么做到的呢?当一个 socket 句柄上有事件时,内核会把该句柄插入上面所说的准备就绪 list 链表,这时我们调用 epoll_wait,会把准备就绪的 socket 拷贝到用户态内存,然后清空准备就绪 list 链表,最后,epoll_wait 干了件事,就是检查这些 socket,如果不是 ET 模式(就是 LT 模式的句柄了),并且这些 socket 上确实有未处理的事件时,又把该句柄放回到刚刚清空的准备就绪链表了。所以,非 ET 的句柄,只要它上面还有事件,epoll_wait 每次都会返回这个句柄。
转载于:https://www.cnblogs.com/myseries/p/11733861.html
Redis 采用单线程模型,每条命令执行如果占用大量时间,会造成其他线程阻塞。这个其他线程是什么?
比如现在有 ABCDE5 个线程同时来操作数据,A 线程执行了一个大 key 的删除操作,删除需要 5 秒。BCDE 则分别执行 get、set 等操作。那么 BCDE 就是 “其他线程”,他们 4 个必须等待 A 执行完才能轮到他们执行。