一、Redis的单线程模型
Redis基于Reactor模式实现了自己的网络事件处理器,这个处理器称为文件事件处理器(file event handler)。
- 文件事件处理器使用IO多路复用程序来同时监听多个socket,并根据socket的执行任务来为socket分配不同的事件处理器。
- 当被监听的socket准备好执行连接应答(accept)、读取(read)、写入(write)、关闭(close)等操作时,对应的文件事件处理器就会调用之前分配好的事件处理器来处理这些事件。
文件事件处理器是以单线程方式运行,但通过IO多路复用来监听多个socket,实现了高性能的网络通信模型。
文件事件处理器由四个部分组成:
- Socket
- IO多路复用程序
- 文件事件分派器(dispatcher)
- 事件处理器
文件事件是对Socket操作的抽象,每当socket准备好连接应答(accept)、写入、读取、关闭等操作时,都会产生一个文件事件,文件事件分为可读事件和可写事件两类。同一时刻可能会有多个客户端请求的到来产生多个Socket,就会有多个文件事件同时出现。
IO多路复用程序的作用是负责监听多个Socket,并将产生事件的Socket传递给文件事件分派器。虽然多个文件事件会并发出现,但IO多路复用程序会将所有产生了事件的Socket放到一个队列里面,然后通过这个队列,以有序、同步的方式每次将一个Socket传递到文件事件分派器。当上一个Socket产生的事件被处理完毕后,IO多路复用程序才会继续传递下一个Socket。文件事件分配器接收到IO多路复用程序传递过来的Socket后,根据Socket的事件类型,选择相应的事件处理器来处理。
补充:
Redis的事件包括文件事件和时间事件,时间事件又包括定时事件和周期性事件。
服务器将所有时间事件都放在一个无序链表中,每当时间事件执行器执行时,就会遍历整个链表,查找已到达的时间事件,并调用相应的时间处理器来处理。
IO多路复用程序可以监听多个Socket的可读事件和可写事件,而一个Socket可以同时产生可读事件和可写事件,文件分派器会优先处理可读事件,然后再处理可写事件。
也就是说,如果一个Socket既可读又可写,那么服务器优先读取Socket,然后写Socket。
Redis的IO多路复用程序是通过包装常见的select,epoll,evport和kqueue这些IO多路复用函数库来实现的。
单线程的redis为什么这么快?
(1)纯内存访问,Redis将所有数据放在内存中,内存的响应时长大约为100纳秒,这是Redis达到每秒万级别访问的重要基础。
(2)非阻塞I/O,Redis使用epoll作为I/O多路复用技术的实现,再加上Redis自身的事件处理模型将epoll中的连接、读写、关闭都转换为事件,不在网络I/O上浪费过多的时间,如下图所示。
(3)单线程避免了线程切换和线程同步的消耗。单线程能带来几个好处:
①单线程可以简化数据结构和算法的实现。如果对高级编程语言熟悉的读者应该了解并发数据结构实现不但困难而且开发测试比较麻烦。
②单线程避免了线程切换和竞态产生的消耗,对于服务端开发来说,锁和线程切换通常是性能杀手。
但是单线程会有一个问题:对于每个命令的执行时间是有要求的。如果某个命令执行过长,会造成其他命令的阻塞,对于Redis这种高性能的服务来说是致命的,所以Redis是面向快速执行场景的数据库。
redis为什么要使用单线程?
官方曾做过类似问题的回复:使用Redis时,几乎不存在CPU成为瓶颈的情况, Redis主要受限于内存和网络。例如在一个普通的Linux系统上,Redis通过使用pipelining每秒可以处理100万个请求,所以如果应用程序主要使用O(N)或O(log(N))的命令,它几乎不会占用太多CPU。
使用了单线程后,可维护性高。多线程模型虽然在某些方面表现优异,但是它却引入了程序执行顺序的不确定性,带来了并发读写的一系列问题,增加了系统复杂度、同时可能存在线程切换、甚至加锁解锁、死锁造成的性能损耗。Redis通过AE事件模型以及IO多路复用等技术,处理性能非常高,因此没有必要使用多线程。单线程机制使得 Redis 内部实现的复杂度大大降低,Hash 的惰性 Rehash、Lpush 等等 “线程不安全” 的命令都可以无锁进行。
二、Redis真的是单线程的吗?
严格来说,Redis并不是单线程的。
1.Redis 4.0中的多线程
其实Redis 早在 v4.0 就已经引入了多线程,用来处理异步任务。比如使用DEL命令删除一些大key时,由于单线程的事件循环,会阻塞后续的操作。但Redis作者开始使用的渐进式的删除方式每次删除一小部分数据,该实现增加了复杂性。但实际上对于一些耗时命令,使用多线程来实现更为简单,所以后续Redis作者选择使用多线程来实现这一类非阻塞的命令,如UNLINK、FLUSHALL ASYNC、FLUSHDB ASYNC。
2.Redis 6.0网络模型引入多线程
我们通常说的Redis是单线程的,这里的单线程指的是Redis的网络模型,也就是Reactor 模型。单Reactor模式是利用多路复用技术(epoll/select/kqueue 等),在单线程的事件循环中不断去处理事件。Reactor的知识可以参考:Reactor线程模式
而在Redis v6.0中正式在网络模型中实现 I/O 多线程。
Redis6.0的多线程默认是禁用的,只使用主线程。需要手动设置开启,另外还需要设置线程数,否则是不生效的。
io-threads-do-reads yes io-threads 4
下面是官方的一些建议:
1.建议至少在4核及以上的机器上使用,并且至少预留一个备用核。使用8个以上的线程不太可能有很大帮助。
2.建议仅在确实存在性能问题的情况下使用,Redis需要占用相当大比例的CPU时间,否则使用此功能毫无意义。
4.io-threads参数的设置:4核可以尝试设置为2或3,8核尝试设置到6。设置为1通常只会使用主IO线程。
参考资料:
《Redis设计与实现》第2版
Redis配置文件redis.conf