Redis —单线程
Posted 小呆鸟_coding
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Redis —单线程相关的知识,希望对你有一定的参考价值。
单线程概念
Redis是单线程的原因
:
- Redis 单线程指的是「接收客户端请求->解析请求 ->进行数据读写等操作->发送数据给客户端」这个过程是由一个线程(主线程)来完成的
但是,Redis 程序并不是单线程的,Redis 在启动的时候,是会启动后台线程(BIO) 的:
-
Redis 在 2.6 版本,会启动
2 个后台线程
:处理关闭文件、AOF 刷盘
-
Redis 在 4.0 版本之后
新增了一个新的后台线程
,用来异步释放 Redis 内存
,也就是 lazyfree 线程。- 当要删除一个大 key 的时候,不要使用 del 命令删除,del 是在主线程处理的,这样会导致 Redis 主线程卡顿。
应该使用 unlink 命令来异步删除大key。
- 当要删除一个大 key 的时候,不要使用 del 命令删除,del 是在主线程处理的,这样会导致 Redis 主线程卡顿。
-
Redis 6.0 版本之后,Redis 在启动的时候,默认情况下会额外
创建 6 个线程
- Redis-server : Redis的主线程,主要负责执行命令;
- 三个后台线程(bio_close_file、bio_aof_fsync、bio_lazy_free)
- 三个 I/O 线程,用来分担 Redis 网络 I/O 的压力。
Redis的三个后台线程
BIO_CLOSE_FILE,关闭文件任务队列
:当队列有任务后,后台线程会调用 close(fd) ,将文件关闭;BIO_AOF_FSYNC,AOF刷盘任务队列
:当 AOF 日志配置成 everysec 选项后,主线程会把 AOF 写日志操作封装成一个任务,也放到队列中。当发现队列有任务后,后台线程会调用 fsync(fd),将 AOF 文件刷盘,BIO_LAZY_FREE,lazy free 任务队列
:当队列有任务后,后台线程会 free(obj) 释放对象 / free(dict) 删除数据库所有对象 / free(skiplist) 释放跳表对象;
Redis单线程模型
图片来自于https://www.xiaolincoding.com/
图中的蓝色部分是一个事件循环,是由主线程负责的,可以看到网络 I/O 和命令处理都是单线程。 Redis 初始化的时候,会做下面这几件事情:
- 首先,调用 epoll_create() 创建一个 epoll 对象和调用 socket() 创建一个服务端 socket
- 然后,调用 bind() 绑定端口和调用 listen() 监听该 socket;
- 然后,将调用 epoll_ctl() 将 listen socket 加入到 epoll,同时注册「连接事件」处理函数。
初始化完后,主线程就进入到一个事件循环函数
,主要会做以下事情:
- 首先,先调用
处理发送队列函数
,看是发送队列里是否有任务,如果有发送任务,则通过 write 函数将客户端发送缓存区里的数据发送出去,如果这一轮数据没有发送完,就会注册写事件处理函数,等待 epoll_wait 发现可写后再处理 。 - 接着,调用 epoll_wait 函数等待事件的到来:
- 如果是
连接事件
到来,则会调用连接事件处理函数,该函数会做这些事情:调用 accpet 获取已连接的 socket -> 调用 epoll_ctl 将已连接的 socket 加入到 epoll -> 注册「读事件」处理函数; - 如果是
读事件
到来,则会调用读事件处理函数,该函数会做这些事情:调用 read 获取客户端发送的数据 -> 解析命令 -> 处理命令 -> 将客户端对象添加到发送队列 -> 将执行结果写到发送缓存区等待发送; - 如果是
写事件
到来,则会调用写事件处理函数,该函数会做这些事情:通过 write 函数将客户端发送缓存区里的数据发送出去,如果这一轮数据没有发送完,就会继续注册写事件处理函数,等待 epoll_wait 发现可写后再处理 。
- 如果是
为什么Redis是单线程还这么快呢
- Redis的全部操作是
基于内存
的。因此 Redis 瓶颈可能是机器的内存或者网络带宽,而并非 CPU - Redis 采用单线程模型可以
避免了多线程之间的竞争
(频繁的上下文切换),避免死锁 Redis 采用了 非阻塞I/O 多路复用机制(epoll)
处理大量的客户端 Socket 请求
Redis 6.0 之前为什么使用单线程?
- CPU 并不是制约 Redis 性能表现的瓶颈所在,更多情况下是受到内存大小和网络I/O的限制
- 使用了单线程后,可维护性高,而使用多线程,增加了系统复杂度、同时可能存在线程切换、甚至加锁解锁、死锁造成的性能损耗。
Redis 6.0 之后为什么引入了多线程?
- 在 Redis 6.0 版本之后,也采用了多个 I/O 线程来处理网络请求,这是因为随着网络硬件的性能提升,Redis 的性能瓶颈有时会出现在网络 I/O 的处理上。
Redis引入多线程来处理网络I/O,仍然使用单线程来执行Redis命令。
注意:对于命令的执行,Redis 仍然使用单线程来处理,不要误解 Redis 有多线程同时执行命令。
文章来源https://www.xiaolincoding.com
redis特点单进程单线程高性能服务器,Redis为什么是单线程?高并发响应快?
为什么 redis 单线程却能支撑高并发?
纯内存操作
核心是基于非阻塞的 IO 多路复用机制
单线程反而避免了多线程的频繁上下文切换问题
一、Redis的高并发和快速原因
1.redis是基于内存的,内存的读写速度非常快(纯内存); 数据存在内存中,数据结构用HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1)。
2.redis是单线程的,省去了很多上下文切换线程的时间(避免线程切换和竞态消耗)。
3.redis使用IO多路复用技术(IO multiplexing, 解决对多个I/O监听时,一个I/O阻塞影响其他I/O的问题),可以处理并发的连接(非阻塞IO)。
下面重点介绍单线程设计和IO多路复用核心设计快的原因。
二、为什么Redis是单线程的
2.1.官方答案
因为Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单线程的方案了。
2.2.性能指标
关于redis的性能,官方网站也有,普通笔记本轻松处理每秒几十万的请求。
2.3.详细原因
1)不需要各种锁的性能消耗
Redis的数据结构并不全是简单的Key-Value,还有list,hash等复杂的结构,这些结构有可能会进行很细粒度的操作,比如在很长的列表后面添加一个元素,在hash当中添加或者删除一个对象。这些操作可能就需要加非常多的锁,导致的结果是同步开销大大增加。
总之,在单线程的情况下,就不用去考虑各种锁的问题,不存在加锁、释放锁操作,没有因为可能出现死锁而导致的性能消耗。
2)单线程多进程集群方案
单线程的威力实际上非常强大,单核cpu效率也非常高,多线程自然是可以比单线程有更高的性能上限,但是在今天的计算环境中,即使是单机多线程的上限也往往不能满足需要了,需要进一步摸索的是多服务器集群化的方案,这些方案中多线程的技术照样是用不上的。
所以“单线程、多进程的集群”不失为一个时髦的解决方案。
3)CPU消耗
采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU。
但是如果CPU成为Redis瓶颈,或者不想让服务器其他CUP核闲置,那怎么办?
可以考虑多起几个Redis进程,Redis是key-value数据库,不是关系数据库,数据之间没有约束。只要客户端分清哪些key放在哪个Redis进程上就可以了。
三、Redis单线程的优劣势
3.1.单进程单线程优势
代码更清晰,处理逻辑更简单。
不用去考虑各种锁的问题,不存在加锁、释放锁操作,没有因为可能出现死锁而导致的性能消耗。
不存在“多进程或者多线程导致的切换”而消耗CPU。
3.2.单进程单线程弊端
无法发挥多核CPU性能,不过可以通过在单机开多个Redis实例来完善。
四、IO多路复用技术(多路网络连接复用一个IO线程, 时分复用)
实际上所有的I/O设备都被抽象为了文件这个概念,一切皆文件,Everything isFile,磁盘、网络数据、终端,甚至进程间通信工具管道pipe等都被当做文件对待。
所有的I/O操作也都是通过文件读写来实现的,这一非常优雅的抽象可以让程序员使用一套接口就能实现所有I/O操作。
常用的I/O操作接口一般有以下几类:
打开文件,open
改变读写位置,seek
文件读写,read、write
关闭文件,close
那么,什么是IO多路复用呢?
有了文件描述符,进程对文件一无所知,比如文件在磁盘的什么位置上、内存是如何管理文件的等等,这些信息属于操作系统,进程无需关心,操作系统只需要给进程一个文件描述符就足够了。因此我们来完善上述程序:
int fd = open(file_name);
read(fd, buff);
文件描述符太多了怎么办经过了这么多的铺垫,终于到高性能、高并发这一主题了。从前几节我们知道,所有I/O操作都可以通过文件样的概念来进行,这当然包括网络通信。如果你是一个web服务器,当三次握手成功以后,我们通过调用accept同样会得到一个文件描述符,只不过这个文件描述符是用来进行网络通信的,通过读写该文件描述符你就可以同客户端通信。在这里为了概念上好理解,我们称之为链接描述符,通过这个描述符我们就可以读写客户端的数据了。
int conn_fd = accept(...);
server的处理逻辑通常是读取客户端请求数据,然后执行某些特定逻辑:
if(read(conn_fd, request_buff) > 0)
do_something(request_buff);
是不是非常简单,然而世界终归是复杂的,也不是这么简单的。接下来就是比较复杂的了。
既然我们的主题是高并发,那么server端就不可能只和一个客户端通信,而是成千上万个客户端。这时你需要处理不再是一个描述符这么简单,而是有可能要处理成千上万个描述符。为了不让问题一上来就过于复杂,我们先简单化,假设只同时处理两个客户端的请求。有的同学可能会说,这还不简单,这样写不就行了:
if(read(socket_fd1, buff) > 0) // 处理第一个
do_something();
if(read(socket_fd2, buff) > 0)
do_something();
....
....
....
这是非常典型的阻塞式I/O,如果读取第一个请求进程被阻塞而暂停运行,那么这时我们就无法处理第二个请求了,即使第二个请求的数据已经就位,这也就意味着所有其它客户端必须等待,而且通常情况下也不会只有两个客户端而是成千上万个,上万个连接也要这样串行处理吗。
聪明的你一定会想到使用多线程,为每个请求开启一个线程,这样一个线程被阻塞不会影响到其它线程了,注意,既然是高并发,那么我们要为成千上万个请求开启成千上万个线程吗,大量创建销毁线程会严重影响系统性能。那么这个问题该怎么解决呢?
不要打电话给我,有需要我会打给你
方式一:
我们主动通过I/O接口, 问内核: 这些文件描述符对应的外设是不是已经就绪了?
方式二:
一种更好的方法是,我们把这些文件描述符,一股脑扔给内核,并霸气的告诉内核:“我这里有1万个文件描述符,你替我监视着它们,有可以读写的文件描述符时你就告诉我,我好处理”。
而不是弱弱的问内核:“第一个文件描述可以读写了吗?第二个文件描述符可以读写吗?第三个文件描述符可以读写了吗?”这样应用程序就从“繁忙”的主动变为清闲的被动了,反正哪些设备ok了内核会通知我, 能偷懒我才不要那么勤奋。
你有N个不知道什么时候来水的水龙头需要接水,你根据某种信号一会儿拧这个龙头,一会儿拧那个龙头把水都接了就是多路复用(一个线程)。
所谓I/O多路复用
回到我们的主题。所谓I/O多路复用指的是这样一个过程:我们拿到了一堆文件描述符(不管是网络相关的、还是磁盘文件相关等等,任何文件描述符都可以), 通过调用某个函数告诉内核:“这个函数你先不要返回,你替我监视着这些描述符,当这堆文件描述符中有可以进行I/O读写操作的时候你再返回”。
当调用的这个函数返回后,我们就能知道哪些文件描述符可以进行I/O操作了。
那么有哪些函数可以用来进行I/O多路复用呢?在Linux世界中有这样三种机制可以用来进行I/O多路复用:
select
poll
epoll
Redis 采用网络IO多路复用技术,来保证在多连接的时候系统的高吞吐量。
多路: 指的是多个socket网络连接;
复用: 指的是复用一个线程。多路复用主要有三种技术:select,poll,epoll。epoll是最新的, 也是目前最好的多路复用技术。
采用多路I/O复用技术的好处:
其一,可以让单个线程高效处理多个连接请求(尽量减少网络IO的时间消耗)。
其二,Redis在内存中操作数据的速度非常快(内存里的操作不会成为这里的性能瓶颈)。主要以上两点造就了Redis具有很高的吞吐量。
I/O multiplexing 这里面的 multiplexing 指的其实是在单个线程通过记录跟踪每一个Sock(I/O流)的状态(对应空管塔里面的Fight progress strip槽)来同时管理多个I/O流. 发明它的原因,是尽量多的提高服务器的吞吐能力。
是不是听起来好拗口? 看个图就懂了.
在同一个线程里面, 通过拨开关的方式,来同时传输多个I/O流, (学过EE的人现在可以站出来义正严辞说这个叫“时分复用”了)。
非阻塞 IO 与 epoll ( nginx、redis 和 NIO 等核心思想 )
非阻塞 IO 内部实现采用 epoll,采用了epoll+自己实现的简单的事件框架。epoll中的读、写、关闭、连接都转化成了事件,然后利用epoll的多路复用特性,绝不在 IO 上浪费一点时间。
详细参考:
五、Redis高并发快总结
Redis是纯内存数据库,一般都是简单的存取操作,线程占用的时间很多,时间的花费主要集中在IO上,所以读取速度快。
再说一下IO,Redis使用的是非阻塞IO、IO多路复用,使用了单线程来轮询描述符,将数据库的开、关、读、写都转换成了事件,减少了线程切换时上下文的切换和竞争。
Redis采用了单线程的模型,保证了每个操作的原子性,也减少了线程的上下文切换和竞争。
另外,数据结构也帮了不少忙。
Redis全程使用hash结构,读取速度快,还有一些特殊的数据结构,对数据存储进行了优化,如压缩表,对短数据进行压缩存储,再如,跳表,使用有序的数据结构加快读取的速度。
还有一点,Redis采用自己实现的事件分离器,效率比较高,内部采用非阻塞的执行方式,吞吐能力比较大。
六、Redis常见性能问题和解决方案:
(1) Master最好不要做任何持久化工作,如RDB内存快照和AOF日志文件;(Master写内存快照,save命令调度rdbSave函数,会阻塞主线程的工作,当快照比较大时对性能影响是非常大的,会间断性暂停服务,所以Master最好不要写内存快照;AOF文件过大会影响Master重启的恢复速度)
(2) 如果数据比较重要,某个Slave开启AOF备份数据,策略设置为每秒同步一次
(3) 为了主从复制的速度和连接的稳定性,Master和Slave最好在同一个局域网内
(4) 尽量避免在压力很大的主库上增加从库
(5) 主从复制不要用图状结构,用单向链表结构更为稳定,即:Master
七、Redis的回收策略
volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰
allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
no-enviction(驱逐):禁止驱逐数据
注意这里的6种机制,
(1)volatile和allkeys规定了,是对已设置过期时间的数据集淘汰数据,还是从全部数据集淘汰数据。
(2)后面的lru、ttl以及random是三种不同的淘汰策略,再加上一种no-enviction永不回收的策略。
使用策略规则:
1、如果数据呈现幂律分布,也就是一部分数据访问频率高,一部分数据访问频率低,则使用allkeys-lru
2、如果数据呈现平等分布,也就是所有的数据访问频率都相同,则使用allkeys-random
八. 五种I/O模型介绍
IO 多路复用是5种I/O模型中的第3种,对各种模型讲个故事,描述下区别:
故事情节为:老李去买火车票,三天后买到一张退票。参演人员(老李,黄牛,售票员,快递员),往返车站耗费1小时。
1.阻塞I/O模型
老李去火车站买票,排队三天买到一张退票。
耗费:在车站吃喝拉撒睡 3天,其他事一件没干。
2.非阻塞I/O模型
老李去火车站买票,隔12小时去火车站问有没有退票,三天后买到一张票。
耗费:往返车站6次,路上6小时,其他时间做了好多事。
3.I/O复用模型
select/poll
老李去火车站买票,委托黄牛,然后每隔6小时电话黄牛询问,黄牛三天内买到票,然后老李去火车站交钱领票。
耗费:往返车站2次,路上2小时,黄牛手续费100元,打电话17次
epoll
老李去火车站买票,委托黄牛,黄牛买到后即通知老李去领,然后老李去火车站交钱领票。
耗费:往返车站2次,路上2小时,黄牛手续费100元,无需打电话。
epoll : 进程只要等待在epoll上,epoll 代替进程去各个文件描述符上等待,当哪个文件描述符可读或者可写的时候就告诉epoll,epoll用小本本认真记录下来然后唤醒大哥:“进程大哥,快醒醒,你要处理的文件描述符我都记下来了”。
这样进程被唤醒后就无需自己从头到尾检查一遍,因为epoll都已经记下来了。因此我们可以看到,在这种机制下,实际上利用的就是“不要打电话给我,有需要我会打给你”,这就不需要一遍一遍像孙子一样问各个文件描述符了,而是翻身做主人当大爷了,“你们那个文件描述符可读或者可写了主动报上来”,这中机制实际上就是大名鼎鼎的 —— 事件驱动,event-driven。(https://ssup2.github.io/theory_analysis/Event_Driven_Architecture_on_Linux/)
简单说epoll和select/poll最大区别是:
1.epoll内部使用了mmap共享了用户和内核的部分空间,避免了数据的来回拷贝
2.epoll基于事件驱动,epoll_ctl注册事件,并注册callback回调函数,epoll_wait只返回发生的事件,避免了像select和poll对事件的整个轮询操作。
nginx中使用了epoll,是基于事件驱动模型的。由一个或多个事件收集器来收集或者分发事件,epoll就属于事件驱动模型的事件收集器,将注册过的事件中发生的事件收集起来,master进程负责管理worker进程。
4.信号驱动I/O模型
老李去火车站买票,给售票员留下电话,有票后,售票员电话通知老李,然后老李去火车站交钱领票。
耗费:往返车站2次,路上2小时,免黄牛费100元,无需打电话
5.异步I/O模型
老李去火车站买票,给售票员留下电话,有票后,售票员电话通知老李并快递送票上门。
耗费:往返车站1次,路上1小时,免黄牛费100元,无需打电话。
1同2的区别是:自己轮询
2同3的区别是:委托黄牛
3同4的区别是:电话代替黄牛
4同5的区别是:电话通知是自取还是送票上门
九、事件驱动( event-driven )
Event Driven Architecture Code Demo:
int event_handler1(event *ev)
// Non-blocking
int event_handler2(event *ev)
// Non-blocking
int main()
// Init I/O multiplexer
IOMultiplexer multiplexer;
// Registe event to multiplexer
multiplexer.Add(ev1)
multiplexer.Add(ev2)
// Run main loop
while(ture)
ev_list = multiplexer.wait() // Only blocked here
for(ev:ev_list)
switch(ev)
case: ev1
event_handler1();
break;
case: ev2
event_handler2();
break;
以上是关于Redis —单线程的主要内容,如果未能解决你的问题,请参考以下文章