每天一个知识点:单线程 Redis 为什么能那么快?

今天,我们来探讨一个很多人都很关心的问题:“为什么单线程的 Redis 能那么快?”
首先,要厘清一个事实,我们通常说,Redis 是单线程,主要是指 Redis 的网络 IO 和键值对读写是由一个线程来完成的,这也是 Redis 对外提供键值存储服务的主要流程。但 Redis 的其他功能,比如持久化、异步删除、集群数据同步等,其实是由额外的线程执行的。

Redis 为什么用单线程?

  • 本身是基于内存的,CPU并不是其主要的性能瓶颈。
  • 对于一些共享资源的访问使用多线程的话,常常需要一些别的机制来保证数据库的一致性(例如锁机制),会造成一定的资源浪费。

多线程的开销

  • 在有合理的资源分配的情况下,可以增加系统中处理请求操作的资源实体,进而提升系统能够同时处理的请求数,即吞吐率。(提高并发度,并不一定非得采用多线程的方式;采用多路复用、事件驱动的方式也可以。)
  • 如果没有良好的系统设计,实际得到的结果,其实是刚开始增加线程数时,系统吞吐率会增加,但是,再进一步增加线程时,系统吞吐率就增长迟缓了,有时甚至还会出现下降的情况。为什么会出现这种情况呢?系统中通常会存在被多线程同时访问的共享资源(1、线程之间的竞争2、线程切换),比如一个共享的数据结构。当有多个线程要修改这个共享资源时,为了保证共享资源的正确性,就需要有额外的机制进行保证,而这个额外的机制,就会带来额外的开销。
  • 共享资源的并发访问控制问题。如果没有精细的设计,比如说,只是简单地采用一个粗粒度互斥锁,就会出现不理想的结果:即使增加了线程,大部分线程也在等待获取访问共享资源的互斥锁,并行变串行,系统吞吐率并没有随着线程的增加而增加。采用多线程开发一般会引入同步原语来保护共享资源的并发访问,这也会降低系统代码的易调试性和可维护性。

单线程 Redis 为什么那么快?

  • 大部分操作在内存上完成。
  • 采用了高效的数据结构,例如哈希表和跳表,这是它实现高性能的一个重要原因。
  • 采用了多路复用机制,使其在网络 IO 操作中能并发处理大量的客户端请求,实现高吞吐率。

基本 IO 模型与阻塞点

  1. 监听客户端请求(bind/listen);
  2. 与客户端建立连接(accept)— 阻塞点;
  3. 从socket中读取请求(recv)— 阻塞点;
  4. 解析客户端请求(parse);
  5. 向socket中写回数据(send)。

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

  • Linux 中的 IO 多路复用机制是指一个线程处理多个 IO 流,就是我们经常听到的 select/epoll 机制。简单来说,在 Redis 只运行单线程的情况下,该机制允许内核中,同时存在多个监听套接字和已连接套接字。内核会一直监听这些套接字上的连接请求或数据请求。一旦有请求到达,就会交给 Redis 线程处理,这就实现了一个 Redis 线程处理多个 IO 流的效果。
  • Redis IO 多路复用机制总结为:
    • 基于linux select/epoll
    • 内核可同时监听多个监听套接字和多个已连接套接字
    • 一旦内核监听到套接字上有数据返回,立刻交给redis线程处理数据
  • 为了在请求到达时能通知到 Redis 线程,select/epoll 提供了基于事件的回调机制,即针对不同事件的发生,调用相应的处理函数。所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当相应的事件发生时会调用这个回调方法。这个回调方法在内核中叫ep_poll_callback,它会将发生的事件添加到rdlist双链表中。
  • 回调机制是怎么工作的呢?select/epoll 一旦监测到 FD 上有请求到达时,就会触发相应的事件。这些事件会被放进一个事件队列,Redis 单线程对该事件队列不断进行处理。这样一来,Redis 无需一直轮询是否有请求实际发生,这就可以避免造成 CPU 资源浪费。同时,Redis 在对事件队列中的事件进行处理时,会调用相应的处理函数,这就实现了基于事件的回调。因为 Redis 一直在对事件队列进行处理,所以能及时响应客户端请求,提升 Redis 的响应性能。即所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,一旦监测到fd上有请求到达时,就会触发相应的事件,当相应的事件发生时会调用回调方法。这个回调方法在内核中叫ep_poll_callback,它会将发生的事件添加到rdlist双链表中。select 轮询遍历 文件对象的被监控的事件(accept, read, write),一旦某个文件对象的监控事件被触发(读或者写或者请求就绪),满足条件,这个事件就会被放到事件队列进行处理,处理的过程就是调用对应的回调函数。
  • 不同的操作系统,多路复用机制也是适用的。因为这个机制的实现有很多种,既有基于 Linux 系统下的 select 和 epoll 实现,也有基于 FreeBSD 的 kqueue 实现,以及基于 Solaris 的 evport 实现,这样,你可以根据 Redis 实际运行的操作系统,选择相应的多路复用实现。

总结

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

你可能感兴趣的:(每天一个知识点:单线程 Redis 为什么能那么快?)