Redis高性能IO复用模型
“单线程” Redis
我们通常说Redis是单线程,指的是 Redis在进行网络IO和键值对读写是由一个线程来完成的,这也是Redis对外提供键值对存储服务的主要流程。但Redis的其他功能,比如 持久化
, 异步删除
, 集群数据同步
等,其实都是由额外的线程执行。
为什么Redis使用单线程?
避免多线程带来的并发访问控制问题:系统通常情况下会存在多个线程同时访问同一个共享资源,当这个共享资源的信息被同时修改的时候,就需要有额外的同步机制进行保证资源的正确性,而这个额外的机制就会带来额外的开销。为避免这些问题,Redis直接采用单线程模式。
单线程Redis为什么这么快?
- 1.Redis大部分操作都是在内存上进行执行的,再加上Redis使用了高效的底层数据结构(比如:跳表,双向链表,Hash等)
- 2.除了这些点之外,Redis还使用了 IO多路复用机制 使Redis能并发同时处理大量的客户端请求,实现高吞吐率。
基本IO模型与阻塞点
基本IO模型:如:为处理一个Get请求,服务端需要监听客户端请求 (bind/listen), 和客户端建立连接 (accept), 从socket中读取请求 (recv), 解析客户端发送的请求 (parse) ,根据请求类型读取键值数据 (get),最后返回数据给客户端, 即向socket 中写回数据 (send)。
如下图所示,在这个网络IO整个链路中,其中 bind/listen、 accept、 recv、 parse 和 send都属于网络IO处理,而get属于键值对数据操作。既然Redis是单线程的,那么,最基本的一种实现是单线程依次对执行上面的操作。
非阻塞模型
在socket模型中,不同操作调用后会返回不同的套接字类型。socket()方法会返回 主动套接字,然后调用 listen() 方法,将 主动套接字 转化为 监听套接字 ,此时Redis可以监听来自客户端的连接请求。最后,调用 accept() 方法接受到达的客户端连接,并返回连接套接字。
- socket() 方法会返回主动套接字,然后调用 listen() 方法,将主动套接字转化为监听套接字,此时,可以监听来自客户端的连接请求。
- 当 Redis 调用 accept() 但一直未有连接请求到达时,Redis 线程可以返回处理其他操作,而不用一直等待。
- Redis 调用 recv() 后,如果已连接套接字上一直没有数据到达,Redis 线程同样可以返回处理其他操作。
虽然Redis不需要等待,但是总需要有个机制在监听到客户端有连接请求、发送数据的时候通知Redis。(IO多路复用 — select/epoll机制)
基于多路复用的高性能IO模型
Linux 的IO多路复用机制是指一个线程处理多个IO流(select/epoll机制) — 在Redis单线程运行中的情况下,该机制允许内核,同时存在多个监听套接字和已连接套接字
Redis网络框架调用了epoll机制,让内核监听这些套接字(图中的FD),此时Redis并不会阻塞在某个客户端请求处理上,因此Redis可以同时与多个客户端进行请求连接并处理数据接收,提高并发性。
实现机制
为了请求到达时能通知Redis线程,select/epoll 提供了基于事件的回调机制,即针对不同的事件,调用对应的处理函数。
select/epoll 一旦监测到FD上有请求到达时,就会触发相应的事件。这些时间会被放进一个事件队列,Redis单线程就会对这些事件不断进行处理,这样Redis就不用一直轮询是否有请求发生,避免了CPU的浪费和阻塞主线程。
因为Redis一直在对事件进行处理,所以能及时响应客户端的请求,提升Redis的响应性能。
针对不同系统 Redis调用了不同系统内核的多路复用机制,既有基于Linux系统下的select和epoll实现,也有基于FreeBSD的kqueue实现,也有基于Solaris的evport实现。
Redis单线程处理IO瓶颈
单线程瓶颈分2方面
- 1.单线程处理键值服务耗时
任意一个请求在server中一旦发生耗时,都会影响整个Redis的性能,就是说后面的请求必须要等待前面请求处理完成才能到自己处理,耗时处理如下:
- a. 操作big key,给一个big key分配内存和删除释放big key都需要产生耗时。
- b. 使用复杂的命令,如:
SORT
/SUNION
/ZUNIONSTORE
,或者当n很大的时候使用 O(n)的操作都会让Redis产生耗时。 - c. 大量key同时过期:因为Redis的过期机制也是在主线程执行的,因此大量key过期的时候,在处理每个请求时都需要删除过期key,产生耗时。
- d. AOF刷盘开启
aways
机制:每个写入都需要把这个命令写入到AOF磁盘中,因为写磁盘的操作耗时远大于读内存,因此开启AOF的aways会产生巨大的耗时。 - e. 全量同步生成RDB:虽然采用fork子进程进行生成快照,但是fork子进程这一瞬间是会阻塞主线程的,实例越大阻塞的时间越长。
- 2.单线程处理IO 并发量非常高时,Redis单线程读写客户端IO数据也会存在瓶颈,虽然采用多路复用IO机制,但是读取客户端数据还是同步读取IO,一旦并发高,主线程会花费大部分时间在读取客户端的数据上。
解决方案
-
针对问题1:一方面需要业务人员去规避,一方面Redis4.0还推出了 lazy-free机制,可以把big-key释放内存的耗时放在异步线程上进行释放。
-
针对问题2:Redis6.0推出了多线程,可以在高并发的场景下使用多线程进行客户端数据的读写,进一步提升server的性能,多线程是体现在客户端IO数据流的读写是并行的,其中每个命令处理还是Redis主线程单线程执行的。