我们平时看到介绍 Redis 的文章,都会说 Redis 是单线程的。但是我们学习的时候,比如 Redis 的 bgsave 命令,它的作用是在后台异步保存当前数据库的数据到磁盘,那既然是异步了,肯定是由别的线程去完成的,这怎么还能说 Redis 是单线程的呢?
其实通常说的 Redis 是单线程,主要是指 Redis 对外提供键值存储服务的主要流程,即网络 IO 和键值对读写是由⼀个线程来完成的。除此外 Redis 的其他功能,比如持久化、 异步删除、集群数据同步等,是由额外的线程执⾏的。在这一点上 Node 也是一样的,一般提到 Node 也是单线程的,但其实 Node 只有一个主线程是单线程,其他异步任务则由其他线程完成。这样做的原因是防止有同步代码阻塞,导致主线程被占用后影响后续的程序代码执行。
因此,严格地说 Redis 并不是单线程。但是我们⼀般把 Redis 称为单线程高性能,这样显得 Redis 更强一些。
Redis 为什么用单线程
Redis 为什么用单线程?在回答这个问题前,先来看大家都很熟悉的数据库 MySQL,它使用的就是多线程。MySQL 不会每有一个连接就创建一个线程,因为线程过多会带来额外的开销,其中包括创建销毁线程的开销、调度线程的开销等,同时也会降低计算机的整体性能。这个正是多线程会遇到的难点。
此外多线程系统中通常会存在被多线程同时访问的共享资源,比如一个共享的数据结构,当有多个进程要修改这个共享资源时,为了保证共享资源的正确性,就需要有额外的机制进行保证,而这个额外的机制,也会带来额外的开销。还是以 MySQL 举例,MySQL 引入了锁机制来解决这个问题。
从上面不难看出,多线程开发中并发访问控制是⼀个难点,需要精细的设计才能处理。如果只是简单地处理,比如简单地采⽤⼀个粗粒度互斥锁,只会出现不理想的结果。即便增加了线程,系统吞吐率也不会随着线程的增加而增加,因为大部分线程还在等待获取访问共享资源的互斥锁。而且,大部分采用多线程开发引入的同步原语保护共享资源的并发访问,也会降低系统代码的易调试性和可维护性。
而正是以上这些问题,才让 Redis 采⽤了单线程模式。
看到这里大家可能有点疑惑,前面说了 Redis 不是单线程,现在我们也说了 Redis 的键值对读写操作使用采用了单线程模式,那么它的其他线程是是什么样的呢?
主进程的其它线程
Redis 3.0 版本后,主进程中除了主线程处理网络 IO 和命令操作外,还有 3 个辅助 BIO 线程。这 3 个 BIO 线程分别负责处理,文件关闭、AOF 缓冲数据刷新到磁盘,以及清理对象这三个任务队列,从而避免这些任务对主 IO 线程的影响。
Redis 在启动时,会同时启动这三个 BIO 线程,但是 BIO 线程只有在需要执行相关类型后台任务时才会唤醒,其他时间会休眠等待任务。
多进程
除了主进程,在以下场景如果需要进行重负荷任务的处理,Redis 会 fork 一个子进程来处理:
收到 bgrewriteaof 命令:Redis fork 一个子进程,然后子进程往临时 AOF文件中写入重建数据库状态的所有命令。写入完毕后,子进程会通知父进程把新增的写操作追加到临时 AOF 文件。最后将临时文件替换旧的 AOF 文件,并重命名。
收到 bgsave 命令:Redis 构建子进程,子进程将内存中的所有数据通过快照做一次持久化落地,写入到 RDB 中。
当需要进行全量复制:master 启动一个子进程,子进程将数据库快照保存到 RDB 文件。在写完 RDB 快照文件后,master 会把 RDB 发给 slave,同时将后续新的写指令都同步给 slave。
Redis6.0 多线程
多线程是 Redis6.0 推出的一个新特性。正如上面所说 Redis 是核心线程负责网络 IO ,命令处理以及写数据到缓冲,而随着网络硬件的性能提升,单个主线程处理⽹络请求的速度跟不上底层⽹络硬件的速度,导致网络 IO 的处理成为了 Redis 的性能瓶颈。
而 Redis6.0 就是从单线程处理网络请求到多线程处理,通过多个 IO 线程并⾏处理网络操作提升实例的整体处理性能。需要注意的是对于读写命令,Redis 仍然使⽤单线程来处理,这是因为继续使⽤单线程执行命令操作,就不⽤为了保证 Lua 脚本、事务的原⼦性,额外开发多线程互斥机制了。
需要注意的是在 Redis6.0 中,多线程机制默认是关闭的,需要在 redis.conf 中完成以下两个设置才能启用多线程。
io-threads-do-reads yes
io-threads 6
多线程流程
来具体看一下在 Redis6.0 中,主线程和 IO 线程是如何协作完成请求处理的。
△ 整体流程示意图
全部流程分为以下 4 阶段:
阶段⼀:服务端和客⼾端建立 Socket 连接,并分配处理线程
当有客⼾端请求和实例建立 Socket 连接时,主线程会创建和客户端的连接,并把 Socket 放入全局等待队列中。然后主线程通过轮询方法把 Socket 连接分配给 IO 线程。
阶段⼆:IO 线程读取并解析请求
主线程把 Socket 分配给 IO 线程后,会进⼊阻塞状态等待 IO 线程完成客户端请求读取和解析。
阶段三:主线程执⾏请求操作
IO 线程解析完请求后,主线程以单线程的⽅式执⾏这些命令操作。
阶段四:IO 线程回写 Socket 和主线程清空全局队
主线程执行完请求操作后,会把需要返回的结果写入缓冲区。然后,主线程会阻塞等待 IO 线程把这些结果回写到 Socket 中,并返回给客户端。等到 IO 线程回写 Socket 完毕,主线程会清空全局队列,等待客户端的后续请求。
总结
看完了这篇文章,相信大家对 Redis 是单线程的说法已经有了大致概念。我们说它是单线程,主要是因为在以前的版本中网络 IO 和键值对读写是由⼀个线程来完成的。而之所以说 Redis 是多线程,则是因为 Redis6.0 以后的版本里,网络 IO 的部分变为了多线程处理。而且除了主线程,还有 3 个辅助 BIO 线程,分别是 fsync 线程、close 线程、清理回收线程。当然不能忘记的是,想要体验多线程机制,就得通过修改配置文件开启多线程功能。