前言

Redis是高性能分布式缓存常用中间件,我们经常说Redis是单线程的,
也有人说Redis在6.0版本采用了多线程,那么Redis到底是采用单线程呢?还是多线程?

通常说 Redis 是单线程,其实主要是指 Redis 对外提供键值存储服务的主要流程,
网络 IO 和键值存储服务是由⼀个线程来完成的。

除此之外外的其他功能, 如持久化、 缓存过期、集群同步等,是由额外的线程执⾏的。
防止有同步阻塞,导致主线程被占用影响后续的逻辑执行。

Redis 为什么用单线程

Redis 单线程到底指什么?

大家所熟知的 Redis 确实是单线程模型,
指的是执行 Redis 命令的核心模块是单线程的,
而不是整个 Redis 实例就一个线程,Redis 其他模块还有各自模块的线程。

参见下图:

  • aeApiPoll:事件分派,I/O 多路复用 API,是基于 epoll_wait/select/kevent 等系统调用的封装,监听等待读写事件触发,然后处理,它是事件循环(Event Loop)中的核心函数,是事件驱动得以运行的基础。
  • acceptTcpHandler:连接应答处理器,底层使用系统调用 accept 接受来自客户端的新连接,并为新连接注册绑定命令读取处理器,以备后续处理新的客户端 TCP 连接;除了这个处理器,还有对应的 acceptUnixHandler 负责处理 Unix Domain Socket 以及 acceptTLSHandler 负责处理 TLS 加密连接。
  • readQueryFromClient:命令读取处理器,解析并执行客户端的请求命令。
  • sendReplyToClient:命令回复处理器,当一次事件循环之后写出缓冲区中还有数据残留,则这个处理器会被注册绑定到相应的连接上,等连接触发写就绪事件时,它会将写出缓冲区剩余的数据回写到客户端。

Redis基于Reactor模式开发了网络事件处理器,这个处理器被称为文件事件处理器 。
它的组成结构为4部分:多个套接字、IO多路复用程序、文件事件分派器、事件处理器。
因为文件事件分派器队列的消费是单线程的,所以Redis才叫单线程模型。

优点

  • Redis是基于内存的操作,磁盘IO以及CPU不是Redis的瓶颈,
    Redis的瓶颈最有可能是机器内存的大小和网络带宽。
  • 采用单线程,避免了不必要的上下文切换和竞争条件,
    也不存在多进程或者多线程导致的切换而消耗 CPU。
  • 不用去考虑各种锁的问题,不存在加锁释放锁操作,
    没有因为可能出现死锁而导致的性能消耗

Redis6.0 多线程

Redis 6.0
Redis 6.0 (GA October, 2021) introduced SSL, the new RESP3 protocol, ACLs, client side caching, diskless replicas, I/O threads, faster RDB loading, new modules APIs, and many more improvements.
See the release notes or download 6.0.16.

I/O threads 说的就是多线程。

多线程引入原因

多线程是 Redis6.0 推出的一个新特性。Redis 是核心线程负责网络 IO ,
命令处理以及写数据到缓冲, 而随着网络硬件的性能提升,
单个主线程处理⽹络请求的速度跟不上底层⽹络硬件的速度,
导致网络 IO 的处理成为了 Redis 的性能瓶颈。

而 Redis6.0 就是从单线程处理网络请求到多线程处理,
通过多个 IO 线程并⾏处理网络操作提升实例的整体处理性能。
需要注意的是对于读写命令,Redis 仍然使⽤单线程来处理,
这是因为继续使⽤单线程执行命令操作,就不⽤为了保证 Lua 脚本、事务的原⼦性,
额外开发多线程互斥机制了。

需要注意的是在 Redis6.0 中,多线程机制默认是关闭的,需要在 redis.conf 中
完成以下两个设置才能启用多线程。

设置 io-thread-do-reads 配置项为 yes,表示启用多线程。
io-threads-do-reads yes
设置线程个数。⼀般来说,线程个数要小于 Redis 实例所在机器的 CPU 核数,
例如,对于⼀个 8 核的机器来说,Redis 官⽅建议配置 6 个 IO 线程。
io-threads 6

多线程流程

全部流程主要分为 4 个阶段:

阶段一:服务端和客⼾端建立 Socket 连接,并分配处理线程

当有客⼾端请求和实例建立 Socket 连接时,主线程会创建和客户端的连接,
并把 Socket 放入全局等待队列中。然后主线程通过轮询方法把 Socket 连接
分配给 IO 线程。

阶段二:IO 线程读取并解析请求

主线程把 Socket 分配给 IO 线程后,会进⼊阻塞状态等待 IO 线程
完成客户端请求读取和解析。

阶段三:主线程执⾏请求操作

IO 线程解析完请求后,主线程以单线程的⽅式执⾏这些命令操作。

阶段四:IO 线程回写 Socket 和主线程清空全局队

主线程执行完请求操作后,会把需要返回的结果写入缓冲区。
然后,主线程会阻塞等待 IO 线程把这些结果回写到 Socket 中,
并返回给客户端。等到 IO 线程回写 Socket 完毕,主线程会清空全局队列,
等待客户端的后续请求。

小结

随着线上服务流量越来越大,Redis 的单线程模式会导致系统消耗很多 CPU 时间
网络 I/O 上从而降低吞吐量,要提升 Redis 的性能有两个方向:

  • 优化网络 I/O 模块
  • 提高机器内存读写的速度

后者依赖于硬件的发展,暂时无解。所以只能从前者下手,网络 I/O 的优化又可以分为两个方向:

  • 零拷贝技术或者 DPDK 技术
  • 利用多核优势

模型缺陷

Redis 的多线程网络模型实际上并不是一个标准的 Multi-Reactors/Master-Workers 模型,
和其他主流的开源网络服务器的模式有所区别,
最大的不同就是在标准的 Multi-Reactors/Master-Workers 模式下,
Sub Reactors/Workers 会完成 网络读 -> 数据解析 -> 命令执行 -> 网络写 整套流程,Main Reactor/Master 只负责分派任务,
而在 Redis 的多线程方案中,I/O 线程任务仅仅是通过 socket 读取客户端请求命令并解析,却没有真正去执行命令,所有客户端命令最后还需要回到主线程去执行,因此对多核的利用率并不算高,而且每次主线程都必须在分配完任务之后忙轮询等待所有 I/O 线程完成任务之后才能继续执行其他逻辑。

Redis 之所以如此设计它的多线程网络模型,我认为主要的原因是为了保持兼容性,因为以前 Redis 是单线程的,所有的客户端命令都是在单线程的事件循环里执行的,也因此 Redis 里所有的数据结构都是非线程安全的,现在引入多线程,如果按照标准的 Multi-Reactors/Master-Workers 模式来实现,则所有内置的数据结构都必须重构成线程安全的,这个工作量无疑是巨大且麻烦的。

参考文章

https://blog.csdn.net/weixin_41605937/article/details/111982403
https://segmentfault.com/a/1190000041275783

更多技术文章,请关注公众号『码农札记』!!

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐