赞
踩
Redis 以其高效的单线程模型著称,从设计之初,Redis 就选择了单线程模式,这在很大程度上简化了其内部实现和维护。单线程模式避免了多线程编程中常见的竞争条件和锁机制问题,从而确保了 Redis 的高性能和稳定性。
单线程模型的优点
单线程模型的缺点
Redis 使用 Reactor 模式处理 I/O 操作。Reactor 模式是一种事件驱动的设计模式,通过非阻塞 I/O 和事件通知机制,实现高效的网络通信。
什么是 Reactor 模式?
Reactor 模式中的 "反应" 一词源自 "倒置" 和 "控制逆转"。具体事件处理程序不直接调用反应器,而是向反应器注册一个事件处理器,表示自己对某些事件感兴趣。当事件发生时,反应器调用事件处理器对事件做出反应。这种控制逆转也称为 "好莱坞法则"(不要调用我,让我来调用你)。
例如:
路人甲去做男士 SPA,前台的接待小姐接待了路人甲。路人甲对 10000 号技师感兴趣,告诉接待小姐,当 10000 号技师上班或空闲时通知他。接到通知后,路人甲做出反应,占用了 10000 号技师。
然后,路人甲想要 10000 号房间,告诉接待小姐,当房间空闲时通知他。接到通知后,路人甲再次做出反应。
在这个例子中,路人甲是具体事件处理程序,前台接待小姐是反应器,技师上班和房间空闲是事件。接待小姐不仅服务路人甲,还可以同时服务其他人,每个人的事件不一样。
单线程 Reactor 模式流程
服务器端的 Reactor 是一个线程对象,启动事件循环,并使用 Acceptor 事件处理器关注 ACCEPT 事件,监听客户端连接请求。客户端发起连接请求,Reactor 监听到 ACCEPT 事件,将其派发给 Acceptor 处理器处理。建立连接后,Reactor 监听 READ 事件。当 Reactor 监听到 READ 事件,将事件派发给处理器处理,读取数据。
在单线程 Reactor 模式中,不仅 I/O 操作在该线程上,非 I/O 业务操作也在该线程上,可能大大延迟 I/O 请求的响应。因此,应将非 I/O 业务逻辑操作从 Reactor 线程上卸载,加速 I/O 请求响应。
单线程 Reactor,工作者线程池
与单线程 Reactor 模式不同,添加了工作者线程池,将非 I/O 操作转交给线程池执行,提高 I/O 响应。对于小容量应用场景,单线程模型适用,但对于高负载、大并发、大数据量应用场景不合适,原因如下:
多 Reactor 线程模式
Reactor 线程池中的每个 Reactor 线程都有自己的 Selector、线程和事件循环逻辑。mainReactor 可以只有一个,但 subReactor 一般有多个。mainReactor 负责接收客户端连接请求,将 SocketChannel 传递给 subReactor 完成与客户端的通信。
多 Reactor 线程模式将 "接受客户端连接请求" 和 "与客户端通信" 分开,由两个 Reactor 线程完成。mainReactor 接收连接请求,不负责通信,将连接转交给 subReactor 线程。这样,read() 数据量大时,不会影响后续客户端连接请求的即时处理。多 Reactor 线程模式在海量客户端并发请求下,通过实现 subReactor 线程池,将连接分发给多个 subReactor 线程,大大提升负载和吞吐量。
Redis 基于 Reactor 模式开发了网络事件处理器 - 文件事件处理器(file event handler,简称 FEH)。该处理器是单线程的,所以 Redis 设计为单线程模型。通过 I/O 多路复用同时监听多个 socket,根据 socket 当前执行的事件选择对应的事件处理器。当被监听的 socket 准备好执行 accept、read、write、close 等操作时,FEH 调用 socket 关联的事件处理器处理事件。
虽然 FEH 是单线程运行,但通过 I/O 多路复用监听多个 socket,实现高性能的网络通信模型,并与 Redis 其他同样单线程运行的模块交互,保证 Redis 内部单线程模型的简洁设计。
文件事件处理器的组成部分
Socket
文件事件就是对 socket 操作的抽象,每当一个 socket 准备好执行连接 accept、read、write、close 等操作时,就会产生一个文件事件。一个服务器通常会连接多个 socket,多个 socket 可能并发产生不同操作,每个操作对应不同文件事件。
I/O 多路复用程序
I/O 多路复用程序负责监听多个 socket。尽管文件事件可能并发出现,但 I/O 多路复用程序将所有产生事件的 socket 放入队列,以有序、同步且每次一个 socket 的方式向文件事件分派器传送 socket。当上一个 socket 产生的事件被事件处理器执行完后,I/O 多路复用程序才会向文件事件分派器传送下一个 socket。
I/O 多路复用程序的实现
Redis 的 I/O 多路复用程序功能通过包装常见的 select、epoll、evport 和 kqueue 这些 I/O 多路复用函数库实现。每个函数库在 Redis 源码中对应一个单独文件。Redis 为每个 I/O 多路复用函数库实现了相同的 API,底层实现可互换。在 ae.c
文件中宏定义了相应规则,使程序在编译时自动选择系统中性能最高的 I/O 多路复用函数库作为底层实现。
Evport、Epoll 和 KQueue 具有 O(1) 复杂度,使用内核空间内存结构,可提供很多文件描述符。select 最多提供 1024 个描述符,对描述符完全扫描,复杂性为 O(n)。
文件事件分派器
文件事件分派器接收 I/O 多路复用程序传来的 socket,根据事件类型调用相应事件处理器。
文件事件处理器
服务器为执行不同任务的套接字关联不同事件处理器,这些处理器定义了事件发生时的动作。Redis 为各种文件事件需求编写了多个处理器,如连接应答处理器、命令请求处理器、命令回复处理器、复制处理器等。
文件事件类型
I/O 多路复用程序可监听多个 socket 的 AE_READABLE
和 AE_WRITABLE
事件。当 socket 可读或有新的可应答 socket 出现时,产生 AE_READABLE
事件。当 socket 可写时,产生 AE_WRITABLE
事件。I/O 多路复用程序可同时监听 AE_READABLE
和 AE_WRITABLE
事件,如同时产生,优先处理 AE_READABLE
事件。
总结
客户端和 Redis 服务器通信过程如下:
AE_READABLE
事件关联。AE_READABLE
事件,由连接应答处理器建立连接,创建客户端 socket,将 AE_READABLE
事件与命令请求处理器关联。AE_READABLE
事件,触发命令请求处理器,读取命令内容,传给相关程序执行。AE_WRITABLE
事件与命令回复处理器关联。当客户端准备好读取响应数据时,socket 产生 AE_WRITABLE
事件,由命令回复处理器处理,将响应数据写入 socket,供客户端读取。删除 socket 的 AE_WRITABLE
事件与命令回复处理器的映射。
随着 Redis 的发展,尤其是 Redis 6.0 的发布,引入了多线程模型,以提高在高并发场景下的性能。多线程的引入,旨在优化网络 I/O 操作,使 Redis 能够更好地利用多核 CPU 的性能。
Redis 6.0 之前的版本真的是单线程吗?
Redis 在处理客户端请求时,包括获取 (socket 读)、解析、执行、内容返回 (socket 写) 等都由一个顺序串行的主线程处理,这就是所谓的“单线程”。但从 Redis 4.0 之后,除了主线程外,还有后台线程处理一些较为缓慢的操作,如清理脏数据、无用连接的释放、大 key 的删除等。
为什么 Redis 6.0 之前一直不使用多线程?
官方曾解释:使用 Redis 时,几乎不存在 CPU 成为瓶颈的情况,主要受限于内存和网络。例如在普通的 Linux 系统上,Redis 通过 pipelining 每秒可处理 100 万个请求。如果应用程序主要使用 O(N) 或 O(log(N)) 的命令,它几乎不会占用太多 CPU。使用单线程后,可维护性高。多线程模型虽然在某些方面表现优异,但引入了程序执行顺序的不确定性,带来了并发读写问题,增加了系统复杂度,存在线程切换、加锁解锁、死锁造成的性能损耗。Redis 通过 AE 事件模型及 I/O 多路复用等技术,处理性能非常高,无需使用多线程。单线程机制使 Redis 内部实现的复杂度大大降低,“线程不安全”的命令可无锁进行。
为什么 Redis 6.0 要引入多线程?
随着业务场景复杂度增加,有些公司有上亿的交易量,需要更大的 QPS。常见解决方案是在分布式架构中对数据分区并采用多个服务器,但有缺点:管理的 Redis 服务器太多,维护代价大;单个 Redis 服务器的命令不适用于数据分区;数据分区无法解决热点读/写问题;数据偏斜,重新分配和放大/缩小变得复杂。
从 Redis 自身角度来说,读写网络的 read/write 系统调用占用了 Redis 执行期间大部分 CPU 时间,瓶颈主要在网络 I/O 消耗。优化有两个方向:
协议栈优化方式与 Redis 关系不大,支持多线程是最有效便捷的操作方式。总结:Redis 支持多线程主要是为了充分利用 CPU 资源,分摊同步 I/O 读写负荷。
Redis 6.0 默认是否开启多线程?
Redis 6.0 的多线程默认禁用,只使用主线程。如需开启需修改 redis.conf
配置文件:io-threads-do-reads yes
。开启多线程后,还需设置线程数,否则不生效。官方建议:4 核机器设置 2 或 3 个线程,8 核设置 6 个线程,线程数一定小于机器核数。线程数并非越大越好,超过 8 个基本无意义。
Redis 6.0 采用多线程后,性能提升效果如何?
Redis 作者 antirez 在 RedisConf 2019 分享时提到:Redis 6 引入多线程 I/O 特性,性能提升至少一倍以上。国内测试表明,GET/SET 命令在 4 线程 I/O 时性能比单线程几乎翻倍。如果开启多线程,至少需 4 核机器,且 Redis 实例占用较大 CPU 时才建议使用,否则无意义。
Redis 6.0 多线程实现机制
流程简述如下:
该设计特点:
开启多线程后,是否存在线程并发安全问题?
从实现机制看,Redis 多线程部分仅处理网络数据读写和协议解析,命令执行仍为单线程顺序执行。因此,无需考虑 key、lua、事务,LPUSH/LPOP 等并发及线程安全问题。
相同点
不同点
高并发场景应用
在电商、金融等高并发场景下,Redis 6.0 的多线程模型显著提高系统响应速度和处理能力。例如,在秒杀活动中,Redis 处理大量订单请求,确保系统稳定性和高效性。
日志处理系统
在日志处理系统中,大量日志数据需实时写入和读取。Redis 6.0 的多线程模型可有效分担网络 I/O 负载,确保日志数据高效处理和存储。
实时监控系统
在实时监控系统中,海量监控数据需实时处理和展示。通过 Redis 6.0 的多线程模型,可大幅提升系统吞吐量和响应速度,确保监控数据及时性和准确性。
Redis 的线程模型和 I/O 模型是其高性能的关键。从最初的单线程模型到 6.0 引入的多线程模型,Redis 在保持高效的同时,不断优化以适应复杂的业务场景。多线程模型通过分摊网络 I/O 操作,提高了 Redis 的性能和扩展性。在高并发、高吞吐量的应用场景中,Redis 6.0 的多线程模型展示了强大的优势和应用潜力。
希望本文能够帮助读者深入理解 Redis 的线程模型、I/O 模型和多线程机制,为实际项目提供有力支持和指导。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。