Redis是单线程的?抛去Redis6.0版本就是单线程了?
单线程指的是Redis的网络IO和键值对的读写是由一个线程来完成的,这也是Redis对外提供键值存储服务的主要流程。BUT
Redis的持久化,异步删除,集群数据同步等,都是由额外的线程执行的.严格意义上来说,Redis并不是单线程的,是由于
Redis 的单线程设计机制以及多路复用机制。
Redis 为什么用单线程?
1. 多线程的开销?
"使用多线程,可以增加系统吞吐率,或是可以增加系统扩展性" 这是真的吗 ,这个是对的,在合理的控制资源的分配情况下,可以增加系统中处理请求操作的资源
实体,进而提升系统能够同时处理的请求数,吞吐量等
我们共享一个数据结构,当有多个线程要修改这个共享资源时,为了保证共享资源的正确性,就必须有额外的机制进行保证,而这个额外的机制,会带来很多额外的开销
例如 : Redis 有 List 的数据类型,并提供出队(LPOP)和入队(LPUSH)操作。假设 Redis 采用多线程设计,
现在有两个线程 A 和 B,线程 A 对一个 List 做 LPUSH 操作,并对队列长度加 1。同时,线程 B 对该 List 执行 LPOP 操作,
并对队列长度减 1。为了保证队列长度的正确性,Redis 需要让线程 A 和 B 的 LPUSH 和 LPOP 串行执行,这样一来,Redis
可以无误地记录它们对 List 长度的修改。否则,我们可能就会得到错误的长度结果。这就是多线程编程模式面临的共享资源的并发访问控制问题.
2. 粗粒度互斥锁
比如说,只是简单地采用一个粗粒度互斥锁,就会出现不理想的结果:即使增加了线程,大部分线程也在等待获取访问共享资源的互斥锁,并行变串行,系统吞吐率并没有随着线程的增加而增加
而且,采用多线程开发一般会引入同步原语来保护共享资源的并发访问,这也会降低系统代码的易调试性和可维护性。为了避免这些问题,Redis 直接采用了单线程模式。
单线程 Redis 为什么那么快?
通常来说,单线程的处理能力要比多线程差很多,但是 Redis 却能使用单线程模型达到每秒数十万级别的处理能力
一方面:Redis 的大部分操作在内存上完成,再加上它采用了高效的数据结构
例如: 例如哈希表和跳表,这是它实现高性能的一个重要原因
二方面:Redis 采用了多路复用机制,使其在网络 IO 操作中能并发处理大量的客户端请求,实现高吞吐率,
Redis 采用单线程进行 IO,如果线程被阻塞了,就无法进行多路复用了。
基本 IO 模型与阻塞点
阻塞模式
KV 为了处理一个 Get 请求,
需要监听客户端请求(bind/listen),
和客户端建立连接(accept),
从 socket 中读取请求(recv),
解析客户端发送请求(parse),
根据请求类型读取键值数据(get),
最后给客户端返回结果,即向 socket 中写回数据(send).
在这里的网络 IO 操作中,有潜在的阻塞点,
分别是 accept() 和 recv()。
当 Redis 监听到一个客户端有连接请求,但一直未能成功建立起连接时,
会阻塞在 accept() 函数这里,导致其他客户端无法和 Redis 建立连接。
类似的,当 Redis 通过 recv() 从一个客户端读取数据时,如果数据一直没有到达,Redis 也会一直阻塞在 recv()。
非阻塞模式
Socket 网络模型的非阻塞模式设置,主要体现在三个关键的函数调用上
| 调用方法 | 返回套接字类型 | 非阻塞模式 | 效果 |
| socket() | 主动套接字 | ... | ... |
| listen() | 监听套接字 | 可设置 | accept()非阻塞 |
| accept() | 已连接套接字 | 可设置 | send()/recv()非阻塞 |
监听套接字,可以设置非阻塞模式:
当 Redis 调用 accept() 但一直未有连接请求到达时,Redis 线程可以返回处理其他操作,而不用一直等待。但是,你要注意的是,
调用 accept() 时,已经存在监听套接字了。调用 accept() 时,已经存在监听套接(listen())字了。
针对已连接套接字设置非阻塞模式:
Redis 调用 recv() 后,如果已连接套接字上一直没有数据到达,Redis 线程同样可以返回处理其他操作。
基于多路复用的高性能 I/O 模型
Linux 中的 IO 多路复用机制是指一个线程处理多个 IO 流,就是我们经常听到的 select/epoll 机制。在 Redis 只运行单线程的情况下, 该机制允许内核中,同时存在多个监听套接字和已连接套接字。内核会一直监听这些套接字上的连接请求或数据请求。 一旦有请求到达,就会交给 Redis 线程处理,这就实现了一个 Redis 线程处理多个 IO 流的效果。
下图中的多个 FD 就是刚才所说的多个套接字。Redis 网络框架调用 epoll 机制,让内核监听这些套接字。Redis 线程不会阻塞在某一个特定的监听或已连接套接字上, 也就是说,不会阻塞在某一个特定的客户端请求处理上。正因为此,Redis 可以同时和多个客户端连接并处理请求,从而提升并发性。图示 :
为了在请求到达时能通知到 Redis 线程,select/epoll 提供了基于事件的回调机制,即针对不同事件的发生,调用相应的处理函数。
这些事件会被放进一个事件队列,Redis 单线程对该事件队列不断进行处理。这样一来,Redis 无需一直轮询是否有请求实际发生,这就可以避免造成 CPU 资源浪费。 同时,Redis 在对事件队列中的事件进行处理时,会调用相应的处理函数,这就实现了基于事件的回调。因为 Redis 一直在对事件队列进行处理,所以能及时响应客户端请求,提升 Redis 的响应性能。
以连接请求(Accept事件)和读数据请求(Read事件)为例
针对这两个 Accept 事件和 Read 事件,Redis 分别对这两个事件注册 accept 和 get 回调函数。当 Linux 内核监听到有连接请求或读数据请求时,
就会触发 Accept 事件和 Read 事件,此时,内核就会回调 Redis 相应的 accept 和 get 函数进行处理。
Redis单线程处理IO请求性能瓶颈主要包括2个方面:
问题1、任意一个请求在server中一旦发生耗时,都会影响整个server的性能,也就是说后面的请求都要等前面这个耗时请求处理完成,自己才能被处理到。耗时的操作包括以下几种:
a、操作bigkey:写入一个bigkey在分配内存时需要消耗更多的时间,同样,删除bigkey释放内存同样会产生耗时;
b、使用复杂度过高的命令:例如SORT/SUNION/ZUNIONSTORE,或者O(N)命令,但是N很大,例如lrange key 0 -1一次查询全量数据;
c、大量key集中过期:Redis的过期机制也是在主线程中执行的,大量key集中过期会导致处理一个请求时,耗时都在删除过期key,耗时变长;
d、淘汰策略:淘汰策略也是在主线程执行的,当内存超过Redis内存上限后,每次写入都需要淘汰一些key,也会造成耗时变长;
e、AOF刷盘开启always机制:每次写入都需要把这个操作刷到磁盘,写磁盘的速度远比写内存慢,会拖慢Redis的性能;
f、主从全量同步生成RDB:虽然采用fork子进程生成数据快照,但fork这一瞬间也是会阻塞整个线程的,实例越大,阻塞时间越久;
问题2、并发量非常大时,单线程读写客户端IO数据存在性能瓶颈,虽然采用IO多路复用机制,但是读写客户端数据依旧是同步IO,只能单线程依次读取客户端的数据,无法利用到CPU多核。
针对问题1,一方面需要业务人员去规避,一方面Redis在4.0推出了lazy-free机制,把bigkey释放内存的耗时操作放在了异步线程中执行,降低对主线程的影响。
针对问题2,Redis在6.0推出了多线程,可以在高并发场景下利用CPU多核多线程读写客户端数据,进一步提升server性能,当然,只是针对客户端的读写是并行的,每个命令的真正操作依旧是单线程的。