8、Redis 的线程模型、I/O 模型和多线程

Redis 的线程模型、I/O 模型和多线程

1. Redis 的线程模型

Redis 以其高效的单线程模型著称,从设计之初,Redis 就选择了单线程模式,这在很大程度上简化了其内部实现和维护。单线程模式避免了多线程编程中常见的竞争条件和锁机制问题,从而确保了 Redis 的高性能和稳定性。

单线程模型的优点

  1. 简化编程:没有多线程竞争的问题,无需使用复杂的锁机制。
  2. 高效利用 CPU:避免了线程切换和上下文切换的开销。
  3. 快速响应:在大多数情况下,单线程能够以极快的速度处理请求。

单线程模型的缺点

  1. CPU 利用率:单线程模式下,Redis 只能利用一个 CPU 核心,无法充分利用多核 CPU 的性能。
  2. I/O 阻塞:单线程模式容易在 I/O 操作上阻塞,影响整体性能。
2. Redis 的 I/O 模型

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 响应。对于小容量应用场景,单线程模型适用,但对于高负载、大并发、大数据量应用场景不合适,原因如下:

  1. 一个 NIO 线程同时处理成百上千的链路,性能无法支撑。
  2. 当 NIO 线程负载过重,处理速度变慢,导致大量客户端连接超时和消息积压,成为系统瓶颈。

多 Reactor 线程模式

Reactor 线程池中的每个 Reactor 线程都有自己的 Selector、线程和事件循环逻辑。mainReactor 可以只有一个,但 subReactor 一般有多个。mainReactor 负责接收客户端连接请求,将 SocketChannel 传递给 subReactor 完成与客户端的通信。

多 Reactor 线程模式将 "接受客户端连接请求" 和 "与客户端通信" 分开,由两个 Reactor 线程完成。mainReactor 接收连接请求,不负责通信,将连接转交给 subReactor 线程。这样,read() 数据量大时,不会影响后续客户端连接请求的即时处理。多 Reactor 线程模式在海量客户端并发请求下,通过实现 subReactor 线程池,将连接分发给多个 subReactor 线程,大大提升负载和吞吐量。

3. Redis 中的线程和 I/O 概述

Redis 基于 Reactor 模式开发了网络事件处理器 - 文件事件处理器(file event handler,简称 FEH)。该处理器是单线程的,所以 Redis 设计为单线程模型。通过 I/O 多路复用同时监听多个 socket,根据 socket 当前执行的事件选择对应的事件处理器。当被监听的 socket 准备好执行 accept、read、write、close 等操作时,FEH 调用 socket 关联的事件处理器处理事件。

虽然 FEH 是单线程运行,但通过 I/O 多路复用监听多个 socket,实现高性能的网络通信模型,并与 Redis 其他同样单线程运行的模块交互,保证 Redis 内部单线程模型的简洁设计。

文件事件处理器的组成部分

  1. Socket

    文件事件就是对 socket 操作的抽象,每当一个 socket 准备好执行连接 accept、read、write、close 等操作时,就会产生一个文件事件。一个服务器通常会连接多个 socket,多个 socket 可能并发产生不同操作,每个操作对应不同文件事件。

  2. I/O 多路复用程序

    I/O 多路复用程序负责监听多个 socket。尽管文件事件可能并发出现,但 I/O 多路复用程序将所有产生事件的 socket 放入队列,以有序、同步且每次一个 socket 的方式向文件事件分派器传送 socket。当上一个 socket 产生的事件被事件处理器执行完后,I/O 多路复用程序才会向文件事件分派器传送下一个 socket。

  3. I/O 多路复用程序的实现

    Redis 的 I/O 多路复用程序功能通过包装常见的 select、epoll、evport 和 kqueue 这些 I/O 多路复用函数库实现。每个函数库在 Redis 源码中对应一个单独文件。Redis 为每个 I/O 多路复用函数库实现了相同的 API,底层实现可互换。在 ae.c 文件中宏定义了相应规则,使程序在编译时自动选择系统中性能最高的 I/O 多路复用函数库作为底层实现。

    • evport = Solaris 10
    • epoll = Linux
    • kqueue = OS X,FreeBSD
    • select = 通常作为 fallback 安装在所有平台上

    Evport、Epoll 和 KQueue 具有 O(1) 复杂度,使用内核空间内存结构,可提供很多文件描述符。select 最多提供 1024 个描述符,对描述符完全扫描,复杂性为 O(n)。

  4. 文件事件分派器

    文件事件分派器接收 I/O 多路复用程序传来的 socket,根据事件类型调用相应事件处理器。

  5. 文件事件处理器

    服务器为执行不同任务的套接字关联不同事件处理器,这些处理器定义了事件发生时的动作。Redis 为各种文件事件需求编写了多个处理器,如连接应答处理器、命令请求处理器、命令回复处理器、复制处理器等。

  6. 文件事件类型

    I/O 多路复用程序可监听多个 socket 的 AE_READABLE 和 AE_WRITABLE 事件。当 socket 可读或有新的可应答 socket 出现时,产生 AE_READABLE 事件。当 socket 可写时,产生 AE_WRITABLE 事件。I/O 多路复用程序可同时监听 AE_READABLE 和 AE_WRITABLE 事件,如同时产生,优先处理 AE_READABLE 事件。

总结

客户端和 Redis 服务器通信过程如下:

  • Redis 启动初始化,将连接应答处理器与 AE_READABLE 事件关联。
  • 客户端发起连接,产生 AE_READABLE 事件,由连接应答处理器建立连接,创建客户端 socket,将 AE_READABLE 事件与命令请求处理器关联。
  • 客户端向 Redis 发送请求时,socket 产生 AE_READABLE 事件,触发命令请求处理器,读取命令内容,传给相关程序执行。
  • Redis 服务器准备好响应数据后,将 socket 的 AE_WRITABLE 事件与命令回复处理器关联。当客户端准备好读取响应数据时,socket 产生 AE_WRITABLE 事件,由命令回复处理器处理,将响应数据写入 socket,供客户端读取。
  • 命令回复处理器写完数据后,

删除 socket 的 AE_WRITABLE 事件与命令回复处理器的映射。

4. Redis 6.0 的多线程

随着 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 消耗。优化有两个方向:

  1. 提高网络 I/O 性能,如使用 DPDK 替代内核网络栈。
  2. 使用多线程充分利用多核 CPU,典型实现如 Memcached。

协议栈优化方式与 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 多线程实现机制

流程简述如下:

  1. 主线程负责接收连接请求,获取 socket 放入全局等待读队列。
  2. 主线程处理完读事件后,通过 RR (Round Robin) 将连接分配给 I/O 线程。
  3. 主线程阻塞等待 I/O 线程读取 socket 完毕。
  4. 主线程通过单线程方式执行请求命令,读取并解析数据,但不执行回写 socket。
  5. 主线程阻塞等待 I/O 线程将数据回写 socket 完毕。
  6. 解除绑定,清空等待队列。

该设计特点:

  1. I/O 线程要么同时在读 socket,要么同时在写,不会同时读或写。
  2. I/O 线程只负责读写 socket 解析命令,不负责命令处理。

开启多线程后,是否存在线程并发安全问题?

从实现机制看,Redis 多线程部分仅处理网络数据读写和协议解析,命令执行仍为单线程顺序执行。因此,无需考虑 key、lua、事务,LPUSH/LPOP 等并发及线程安全问题。

5. Redis 与 Memcached 多线程模型对比

相同点

  1. 主从模型:都采用主线程和工作线程模型。
  2. 线程分工:主线程处理连接,工作线程处理数据读写。

不同点

  1. Memcached:工作线程负责所有命令解析和执行,实现真正的线程隔离。
  2. Redis:主线程负责命令解析和执行,I/O 线程只负责网络数据读写,避免了线程安全问题。
6. 实践案例

高并发场景应用

在电商、金融等高并发场景下,Redis 6.0 的多线程模型显著提高系统响应速度和处理能力。例如,在秒杀活动中,Redis 处理大量订单请求,确保系统稳定性和高效性。

日志处理系统

在日志处理系统中,大量日志数据需实时写入和读取。Redis 6.0 的多线程模型可有效分担网络 I/O 负载,确保日志数据高效处理和存储。

实时监控系统

在实时监控系统中,海量监控数据需实时处理和展示。通过 Redis 6.0 的多线程模型,可大幅提升系统吞吐量和响应速度,确保监控数据及时性和准确性。

7. 结论

Redis 的线程模型和 I/O 模型是其高性能的关键。从最初的单线程模型到 6.0 引入的多线程模型,Redis 在保持高效的同时,不断优化以适应复杂的业务场景。多线程模型通过分摊网络 I/O 操作,提高了 Redis 的性能和扩展性。在高并发、高吞吐量的应用场景中,Redis 6.0 的多线程模型展示了强大的优势和应用潜力。

希望本文能够帮助读者深入理解 Redis 的线程模型、I/O 模型和多线程机制,为实际项目提供有力支持和指导。

  • 15
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值