手记

"单线程"的Redis为什么这么快?

Redis的单线程

Redis不同版本之间采用的线程模型是不一样的,在Redis4.0版本之前使用的是单线程模型,在4.0版本之后增加了多线程的支持。
在4.0之前Redis的单线程是指网络IO和键值对读写是由一个线程完成的,但是Redis的其他功能,如:持久化、集群数据同步等,
都是有其他线程完成的。
4.0之后添加了多线程的支持,主要是体现在大数据的异步删除功能上,如: unlink key、flushdb async、flushall async。
所以严格来讲Redis并不是单线程。一般把Redis称为单线程高性能或单线程模式。

为什么使用单线程?

多线程的开销

一个系统中,在合理的资源分配的情况下,增加系统中处理请求的资源实体(线程),可以提升系统能够同时处理的请求数(吞吐量);
通常在采用多线程后,如果没有好的系统设计,往往会适得其反。例如我们刚开始增加线程数时,系统吞吐率会增加,但是,再进一步
增加线程时,系统吞吐率就增长迟缓了,有时甚至还会出现下降的情况。

出现这种情况的一个关键点就是系统中通常会存在多个线程同时访问的共享资源,当有多个线程要修改这个共享资源时,为了保证共享资源的正确性,就需要有额外的机制进行保证,而这个额外的机制,就会带来额外的开销。这就是多线程编程模式面临的共享资源的并发访问控制问题。

如果只是简单地采用一个粗粒度互斥锁,就会出现即使增加了线程,大部分线程也在等待获取访问共享资源的互斥锁,并行变串行,系统吞吐率并没有随着线程的增加而增加。
而且采用多线程开发一般会引入同步原语来保护共享资源的并发访问,这也会降低系统代码的易调试性和可维护性。为了避免这些问题,Redis 直接采用了单线程模式。

使用单线程避免了多线程的竞争,省去了多线程切换带来的时间和性能开销,并且不会出现死锁。但同时单线程也不能完全发挥出多核CPU的性能。

单线程为什么能这么快?

1. Redis 的大部分操作都在内存中完成,内存中的执行效率本身就很快,并且采用了高效的数据结构,比如哈希表和跳表。

Redis底层数据结构这篇文章中有详细介绍

2. 采用 I/O 多路复用机制处理大量客户端的Socket请求,因为这是基于非阻塞的 I/O 模型,这就让Redis可以高效地进行网络通信,I/O的读写流程也不再阻塞。

多路复用

基本 IO 模型与阻塞点
因为Redis采用单线程进行IO,如果线程阻塞了,就无法进行多路复用了,所以首先要明白网络的基本IO模型和阻塞点。
如下图所示:

在网络IO操作中,潜在的阻塞点分别是 accept() 和 recv()。当 Redis 监听到一个客户端有连接请求,但一直未能成功建立起连接时,会阻塞在 accept() 函数这里,导致其他客户端无法和 Redis 建立连接。类似的,当 Redis 通过 recv() 从一个客户端读取数据时,如果数据一直没有到达,Redis 也会一直阻塞在 recv()。
这就导致 Redis 整个线程阻塞,无法处理其他客户端请求,效率很低。
非阻塞模式

Socket网络模型的非阻塞模式设置,主要是三个关键函数的调用。

  1. socket() 方法会返回主动套接字
  2. 然后调用 listen() 方法,将主动套接字转化为监听套接字,此时,可以监听来自客户端的连接请求。
  3. 最后,调用 accept() 方法接收到达的客户端连接,并返回已连接套接字。

对于监听套接字,可以设置非阻塞模式:当Redis调用accept()但一直未有连接请求到达时,Redis线程可以返回处理其他操作,而不用一直等待。但值得注意的是,调用accept()时,已经存在监听套接字了。
所以Redis线程可以不用继续等待,但是总得有机制继续在监听套接字上等待后续连接请求,并在有请求时通知 Redis。
这样才能保证 Redis 线程,既不会像基本 IO 模型中一直在阻塞点等待,也不会导致 Redis 无法处理实际到达的连接请求或数据。

此时就要利用IO多路复用机制了。

多路复用的高性能 I/O 模型

在Linux中,IO多路复用指的是一个线程处理多个IO流,即select/epoll机制。在Redis单线程模式下,允许内核中同时存在多个监听套接字
和已连接套接字。内核会一直监听这些套接字上的连接请求或数据请求。一旦有请求到达,就会交给 Redis 线程处理,这就实现了一个 Redis 线程处理多个 IO 流的效果。

多路复用的IO模型如下图所示:

而为了在请求到达时能及时通知到Redis线程,select/epoll 提供了基于事件的回调机制,即针对不同事件的发生,调用相应的处理函数。
回调机制就是select/epoll 一旦监测到 FD 上有请求到达时,就会触发相应的事件。
这些事件会被放进一个事件队列,Redis 单线程对该事件队列不断进行处理。这样一来,Redis 无需一直轮询是否有请求实际发生,这就可以避免造成 CPU 资源浪费。同时,Redis 在对事件队列中的事件进行处理时,会调用相应的处理函数,这就实现了基于事件的回调。因为 Redis 一直在对事件队列进行处理,所以能及时响应客户端请求,提升 Redis 的响应性能。

总结

Redis 单线程是指它对网络 IO 和数据读写的操作采用了一个线程,而采用单线程的一个核心原因是避免多线程开发的并发控制问题。单线程的 Redis 也能获得高性能,跟多路复用的 IO 模型密切相关,因为这避免了 accept() 和 send()/recv() 潜在的网络 IO 操作阻塞点。

0人推荐
随时随地看视频
慕课网APP