1. Redis演进史
如果单纯的说redis是单线程或多线程,这个回答肯定不严谨,不同版本使用的线程模型是不一样的。【相关推荐:Redis视频教程】
版本3.x ,最早版本,也就是大家口口相传的redis是单线程。
版本4.x,严格意义来说也不是单线程,而是负责处理客户端请求的线程是单线程,但是 开始加了点
多线程的东西(异步删除)
。最新版本的6.0.x后, 告别了大家印象中的单线程,用一种全新的
多线程
来解决问题。
2. Redis单线程模型
2.1 单线程真实含义
主要是指Redis的网络IO和键值对读写是由一个线程来完成的,Redis在处理客户端的请求时包括获取 (socket 读)、解析、执行、内容返回 (socket 写) 等都由一个顺序串行的主线程处理,这就是所谓的“单线程”。这也是Redis对外提供键值存储服务的主要流程。
但Redis的其他功能, 比如持久化、异步删除、集群数据同步等等,其实是由额外的线程
执行的。
可以这么说,Redis工作线程是单线程的。但是,整个Redis来说,是多线程的
;
2.2 单线程性能快原因
Redis 3.x 单线程时代但是性能很快的主要原因
:
- 基于内存操作:所有数据都存在内存中,因此所有的运算都是内存级别的
- 数据结构简单:Redis的数据结构是专门设计的,而这些简单的数据结构的查找和操作的时间大部分复杂度都是
o(1)
- 多路复用和费阻塞IO:使用IO多路复用功能监听多个socket连接客户端,这样就可以使用一个线程连接处理多个请求,减少线程切换带来的开销,同时避免IO阻塞操作
- 避免上下文切换:因为是单线程模型,就可以避免不必要的上先文切换和多线程竞争,这样可以省去多线程切换带来的时间和性能上的消耗,而且单线程不会导致死锁问题的发生
2.3 采用单线程原因
Redis 是基于内存操作的, 因此他的瓶颈可能是机器的内存或者网络带宽而并非 CPU
,既然 CPU 不是瓶颈,那么自然就采用单线程的解决方案了,况且使用多线程比较麻烦。 但是在 Redis 4.0 中开始支持多线程了,例如后台删除等功能
。
简单来说,Redis 4.0 之前一直采用单线程的主要原因有以下三个:
使用单线程模型是 Redis 的开发和维护更简单,因为单线程模型方便开发和调试;多线程模型虽然在某些方面表现优异,但是它却引入了程序执行顺序的不确定性,带来了并发读写的一系列问题,增加了系统复杂度、同时可能存在线程切换、甚至加锁解锁、死锁造成的性能损耗。Redis通过AE事件模型以及IO多路复用等技术,处理性能非常高,因此没有必要使用多线程。单线程机制使得 Redis 内部实现的复杂度大大降低,Hash 的惰性 Rehash、Lpush 等等 “线程不安全” 的命令都可以无锁进行。
即使使用单线程模型也并发的处理多客户端的请求,主要使用的是多路复用和非阻塞 IO;
对于 Redis 系统来说, 主要的
性能瓶颈是内存或者网络带宽而并非 CPU
。
3. Redis多线程模型
3.1 引入多线程原因
既然单线程那么好,为啥又要引入多线程?
单线程也有自己的烦恼,比如大key删除问题:
正常情况下使用 del 指令可以很快的删除数据,而当被删除的 key 是一个非常大的对象时,例如时包含了成千上万个元素的 hash 集合时,那么 del 指令就会造成 Redis 主线程卡顿。
因此,在 Redis 4.0 中就新增了多线程的模块,当然此版本中的多线程主要是为了解决删除数据效率比较低的问题。可以通过惰性删除有效避免Redis卡顿问题(大key删除等问题),步骤如下:
unlink key
: 与DEL一样删除key功能的lazy free实现,唯一不同是,UNLINK在删除集合类型键时,如果集合键的元素个数大于64个,主线程中只是把待删除键从数据库字典中摘除,会把真正的内存释放操作,给单独的bio来操作。如果元素个数较少(少于64个)或者是String类型,也会在主线程中直接删除。flushall/flushdb async
: 对于清空数据库命令flushall/flushdb,添加了async异步清理选项,使得redis在清空数据库时都是异步操作。实现逻辑是为数据库新建一个新的空的字典,把原有旧的数据库字典给后台线程来逐一删除其中的数据,释放内存。把删除工作交给了后台子进程异步删除数据
因为Redis是单个主线程处理,redis之父antirez一直强调"Lazy Redis is better Redis". 而lazy free
的本质就是把某些cost(主要时间复制度,占用主线程cpu时间片)较高删除操作
, 从redis主线程剥离让bio子线程来处理,极大地减少主线阻塞时间。从而减少删除导致性能和稳定性问题。
Redis 4.0 就引入了多个线程来实现数据的异步惰性删除等功能,但是其处理读写请求的仍然只有一个线程,所以仍然算是狭义上的单线程。
从上一小结分析:Redis的主要性能瓶颈是内存或网络带宽而并非CPU。内存问题比较好解决,因此Redis的瓶颈原因为网络IO。接下来,引入多线程模型。
3.2 多线程工作原理
I/O 的读和写本身是堵塞的,比如当 socket 中有数据时,Redis 会通过调用先将数据从内核态空间拷贝到用户态空间,再交给 Redis 调用,而这个拷贝的过程就是阻塞的,当数据量越大时拷贝所需要的时间就越多,而这些操作都是基于单线程完成的。
在 Redis 6.0 中新增了多线程的功能来提高 I/O 的读写性能,他的主要实现思路是将主线程的 IO 读写任务拆分给一组独立的线程去执行,这样就可以使多个 socket 的读写可以并行化了,采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络IO的时间消耗),将最耗时的Socket的读取、请求解析、写入单独外包出去,剩下的命令执行仍然由主线程串行执行并和内存的数据交互。
结合上图可知,将网络数据读写、请求协议解析通过多个IO线程的来处理,对于真正的命令执行来说,仍然使用主线程操作(线程安全),是个不错的折中办法。因此,对于整个Redis来说是多线程的,但是对于工作线程(命令执行)仍旧是单线程
。
3.3 工作流程
核心流程大概如下:
流程简述如下:
- 主线程获取 socket 放入等待列表
- 将 socket 分配给各个 IO 线程(并不会等列表满)
- 主线程
阻塞等待 IO 线程(多线程)
读取 socket 完毕 - 主线程执行命令 -
单线程
(如果命令没有接收完毕,会等 IO 下次继续) - 主线程
阻塞等待 IO 线程(多线程)
将数据回写 socket 完毕(一次没写完,会等下次再写) - 解除绑定,清空等待队列
特点如下:
- IO 线程要么同时在读 socket,要么同时在写,不会同时读或写
- IO 线程只负责读写 socket 解析命令,不负责命令处理(主线程串行执行命令)
- IO 线程数可自行配置(目前代码限制上限为 512,默认为 1(关闭此功能))
经过有心人士的压测,目前性能能提高 1 倍以上。
疑问1:等待列表不满 一直阻塞不处理吗?
回复:阻塞时检测的是,IO 线程是否还有任务。等处理完了才继续往下。这些任务是在执行时添加的,如果 任务数< 线程数,那有些线程就拿不到任务,它的待处理任务就是 0 。分配了任务的线程,在处理好 IO 事件后,pending 就会清零,没拿到任务的线程 pending 本来就是 0,所以不会阻塞。 这块还是有点疑问,哪位大佬可以解释下(评论哈)?
3.4 默认开启多线程吗?
在Redis6.0中, 多线程机制默认是关闭的
,如果需要使用多线程功能,需要在redis.conf中完成两个设置。
- 设置io-thread-do-reads配置项为yes,表示启动多线程。
- 设置线程个数。关于线程数的设置,官方的建议是如果为 4 核的 CPU,建议线程数设置为 2 或 3,
如果为 8 核 CPU 建议线程数设置为 6
,线程数一定要小于机器核数,线程数并不是越大越好。
4. 总结
Redis自身出道就是优秀,基于内存操作、数据结构简单、多路复用和非阻塞 I/O、避免了不必要的线程上下文切换等特性,在单线程的环境下依然很快;
但对于大数据的 key 删除还是卡顿厉害,因此在 Redis 4.0 引入了多线程unlink key/flushall async 等命令,主要用于 Redis 数据的异步删除;
而在 Redis 6.0 中引入了 I/O 多线程的读写,这样就可以更加高效的处理更多的任务了, Redis 只是将 I/O 读写变成了多线程
,而 命令的执行依旧是由主线程串行执行的
,因此在多线程下操作 Redis 不会出现线程安全的问题
。
Redis 无论是当初的单线程设计,还是如今与当初设计相背的多线程,目的只有一个:让 Redis 变得越来越快。
更多编程相关知识,请访问:编程入门!!
以上就是浅析Redis6中的单线程和多线程模型的详细内容,更多请关注自由互联其它相关文章!