服务器端编程经常需要构造高性能的IO模型,常见的IO模型有四种

(1)同步阻塞IO(Blocking IO):即传统的IO模型

老李去火车站买票,排队三天买到一张退票。

耗费:在车站吃喝拉撒睡 3天,其他事一件没干。

(2)同步非阻塞IO(Non-blocking IO):默认创建的socket都是阻塞的,非阻塞IO要求socket被设置为NONBLOCK。注意这里所说的NIO并非Java的NIO(New IO)库

老李去火车站买票,隔12小时去火车站问有没有退票,三天后买到一张票。

耗费:往返车站6次,路上6小时,其他时间做了好多事。

(3)IO多路复用(IO Multiplexing):即经典的Reactor设计模式,有时也称为异步阻塞IO,Linux中的epoll都是这种模型。

  1. select/poll

老李去火车站买票,委托黄牛,然后每隔6小时电话黄牛询问,黄牛三天内买到票,然后老李去火车站交钱领票。

耗费:往返车站2次,路上2小时,黄牛手续费100元,打电话17次

  1. epoll

老李去火车站买票,委托黄牛,黄牛买到后即通知老李去领,然后老李去火车站交钱领票。

耗费:往返车站2次,路上2小时,黄牛手续费100元,无需打电话

  1. 信号驱动I/O模型

老李去火车站买票,给售票员留下电话,有票后,售票员电话通知老李,然后老李去火车站交钱领票。

耗费:往返车站2次,路上2小时,免黄牛费100元,无需打电话

(4)异步IO(Asynchronous IO):即经典的Proactor设计模式,也称为异步非阻塞IO。

  1. 异步I/O模型

老李去火车站买票,给售票员留下电话,有票后,售票员电话通知老李并快递送票上门。

耗费:往返车站1次,路上1小时,免黄牛费100元,无需打电话

单线程 Reactor 模式流程

hiredis 异步方式 subscriber redis 异步io_Redis

单线程 Reactor,工作者线程池

hiredis 异步方式 subscriber redis 异步io_多线程_02

多 Reactor 线程模式

hiredis 异步方式 subscriber redis 异步io_Redis_03

Redis 为什么那么快

  1. 内存操作
  2. 多路复用io阻塞机制
  3. resp协议
  4. 单线程减去了多线程的上下文切换

Redis6.0 之前的版本真的是单线程吗?

Redis 在处理客户端的请求时,包括获取 (socket 读)、解析、执行、内容返 回 (socket 写) 等都由一个顺序串行的主线程处理,这就是所谓的“单线程”。 但如果严格来讲从 Redis4.0 之后并不是单线程,除了主线程外,它也有后台线程 在处理一些较为缓慢的操作,例如清理脏数据、无用连接的释放、大 key 的删 除等等。

Redis6.0 之前为什么一直不使用多线程?

官方曾做过类似问题的回复:使用 Redis 时,几乎不存在 CPU 成为瓶颈的情 况, Redis 主要受限于内存和网络。例如在一个普通的 Linux 系统上,Redis 通过
使用 pipelining 每秒可以处理 100 万个请求,所以如果应用程序主要使用 O(N)或 O(log(N))的命令,它几乎不会占用太多 CPU。

使用了单线程后,可维护性高。多线程模型虽然在某些方面表现优异,但是 它却引入了程序执行顺序的不确定性,带来了并发读写的一系列问题,增加了系 统复杂度、同时可能存在线程切换、甚至加锁解锁、死锁造成的性能损耗。Redis 通过 AE 事件模型以及 IO 多路复用等技术,处理性能非常高,因此没有必要使用 多线程。单线程机制使得 Redis 内部实现的复杂度大大降低,Hash 的惰性 Rehash、Lpush 等等 “线程不安全” 的命令都可以无锁进行。

Redis6.0 为什么要引入多线程呢?

Redis 将所有数据放在内存中,内存的响应时长大约为 100 纳秒,对于小数 据包,Redis 服务器可以处理 80,000 到 100,000 QPS,这也是 Redis 处理的极限了, 对于 80%的公司来说,单线程的 Redis 已经足够使用了。
但随着越来越复杂的业务场景,有些公司动不动就上亿的交易量,因此需要 更大的QPS。常见的解决方案是在分布式架构中对数据进行分区并采用多个服务 器,但该方案有非常大的缺点,例如要管理的 Redis 服务器太多,维护代价大; 某些适用于单个 Redis 服务器的命令不适用于数据分区;数据分区无法解决热点 读/写问题;数据偏斜,重新分配和放大/缩小变得更加复杂等等。

从 Redis 自身角度来说,因为读写网络的 read/write 系统调用占用了 Redis 执行期间大部分 CPU 时间,瓶颈主要在于网络的 IO 消耗, 优化主要有两个方向:
• 提高网络 IO 性能,典型的实现比如使用 DPDK 来替代内核网络栈的方 式
• 使用多线程充分利用多核,典型的实现比如 Memcached。 协议栈优化的这种方式跟 Redis 关系不大,支持多线程是一种最有效最便捷的操作方式。

所以总结起来,redis 支持多线程主要就是两个原因:
• 可以充分利用服务器 CPU 资源,目前主线程只能利用一个核
• 多线程任务可以分摊 Redis 同步 IO 读写负荷

Redis6.0 默认是否开启了多线程?

Redis6.0 的多线程默认是禁用的,只使用主线程。如需开启需要修改 redis.conf 配置文件:
io-threads-do-reads yesio-threads 4 开启多线程后,还需要设置线程数,否则是不生效的。
关于线程数的设置,官方有一个建议:4 核的机器建议设置为 2 或 3 个线程, 8 核的建议设置为 6 个线程,线程数一定要小于机器核数。还需要注意的是,线 程数并不是越大越好,官方认为超过了 8 个基本就没什么意义了。

Redis6.0 采用多线程后,性能的提升效果如何?

Redis 作者 antirez 在 RedisConf 2019 分享时曾提到:Redis 6 引入的多线程 IO 特性对性能提升至少是一倍以上。国内也有大牛曾使用 unstable 版本在阿里 云 esc 进行过测试,GET/SET 命令在 4 线程 IO 时性能相比单线程是几乎是翻倍 了。如果开启多线程,至少要 4 核的机器,且 Redis 实例已经占用相当大的 CPU 耗时的时候才建议采用,否则使用多线程没有意义。

Redis6.0 多线程的实现机制?

流程简述如下:
1、主线程负责接收建立连接请求,获取 socket 放入全局等待读处理队列
2、主线程处理完读事件之后,通过 RR(Round Robin) 将这些连接分配给这
些 IO 线程
3、主线程阻塞等待 IO 线程读取 socket 完毕
4、主线程通过单线程的方式执行请求命令,请求数据读取并解析完成,但并不执行回写 socket
5、主线程阻塞等待 IO 线程将数据回写 socket 完毕
6、解除绑定,清空等待队列

该设计有如下特点:
1、IO 线程要么同时在读 socket,要么同时在写,不会同时读或写
2、IO 线程只负责读写 socket 解析命令,不负责命令处理

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

从上面的实现机制可以看出,Redis 的多线程部分只是用来处理网络数据的 读写和协议解析,执行命令仍然是单线程顺序执行。所以我们不需要去考虑控制 key、lua、事务,LPUSH/LPOP 等等的并发及线程安全问题。

Redis6.0 的多线程和 Memcached 多线程模型进行对比

Memcached 服务器采用 master-woker 模式进行工作,服务端采用 socket 与客户端通讯。主线程、工作线程 采用 pipe 管道进行通讯。主线程采用 libevent 监听 listen、accept 的读事件,事件响应后将连接信息的数据结构封装起来,根 据算法选择合适的工作线程,将连接任务携带连接信息分发出去,相应的线程利 用连接描述符建立与客户端的 socket 连接 并进行后续的存取数据操作。

相同点:都采用了 master 线程-worker 线程的模型

不同点:Memcached 执行主逻辑也是在 worker 线程里,模型更加简单, 实现了真正的线程隔离,符合我们对线程隔离的常规理解。而 Redis 把处理逻 辑交还给 master 线程,虽然一定程度上增加了模型复杂度,但也解决了线程并 发安全等问题