Redis之线程模型

Redis 官方在 2020 年 5 月正式推出 6.0 版本,提供很多振奋人心的新特性,所以备受关注。

码老湿,提供了啥特性呀?知道了我能加薪么?

主要特性如下:

  1. 多线程处理网络 IO;
  2. 客户端缓存;
  3. 细粒度权限控制(ACL);
  4. RESP3 协议的使用;
  5. 用于复制的 RDB 文件不在有用,将立刻被删除;
  6. RDB 文件加载速度更快;

其中备受关注的就是「多线程模型 + 客户端缓存」,我们只有掌握了新特性原理,才能判断什么时候使用 6.0 版本,如何用的更好更快,不踩坑。

本篇先从 Redis 多线程模型开始,至于客户端缓存、等且听下回分解。

最后,点击下方卡片关注「码哥字节」能加薪。

码老湿,Redis 6.0 之前为什么不使用多线程?

官方答复:

  • 使用 Redis 时,几乎不存在 CPU 成为瓶颈的情况, Redis 主要受限于内存和网络。

  • 在一个普通的 Linux 系统上,Redis 通过使用pipelining 每秒可以处理 100 万个请求,所以如果应用程序主要使用 O(N) 或O(log(N)) 的命令,它几乎不会占用太多 CPU。

  • 使用了单线程后,可维护性高。多线程模型虽然在某些方面表现优异,但是它却引入了程序执行顺序的不确定性,带来了并发读写的一系列问题,增加了系统复杂度、同时可能存在线程切换、甚至加锁解锁、死锁造成的性能损耗。

Redis 通过 AE 事件模型以及 IO 多路复用等技术,处理性能非常高,因此没有必要使用多线程。

单线程机制让 Redis 内部实现的复杂度大大降低,Hash 的惰性 Rehash、Lpush 等等『线程不安全』的命令都可以无锁进行

在《Redis 为什么这么快?》码哥有详细介绍快的原理

Redis 6.0 之前单线程指的是 Redis 只有一个线程干活么?

非也,Redis 在处理客户端的请求时,包括获取 (socket 读)、解析、执行、内容返回 (socket 写) 等都由一个顺序串行的主线程处理,这就是所谓的「单线程」

其中执行命令阶段,由于 Redis 是单线程来处理命令的,所有每一条到达服务端的命令不会立刻执行,所有的命令都会进入一个 Socket 队列中,当 socket 可读则交给单线程事件分发器逐个被执行。

此外,有些命令操作可以用后台线程或子进程执行(比如数据删除、快照生成、AOF 重写)。

码老湿,那 Redis 6.0 为啥要引入多线程呀?

随着硬件性能提升,Redis 的性能瓶颈可能出现网络 IO 的读写,也就是:单个线程处理网络读写的速度跟不上底层网络硬件的速度

读写网络的 read/write 系统调用占用了Redis 执行期间大部分CPU 时间,瓶颈主要在于网络的 IO 消耗, 优化主要有两个方向:

  • 提高网络 IO 性能,典型的实现比如使用 DPDK 来替代内核网络栈的方式。
  • 使用多线程充分利用多核,提高网络请求读写的并行度,典型的实现比如 Memcached

添加对用户态网络协议栈的支持,需要修改 Redis 源码中和网络相关的部分(例如修改所有的网络收发请求函数),这会带来很多开发工作量。

而且新增代码还可能引入新 Bug,导致系统不稳定。

所以,Redis 采用多个 IO 线程来处理网络请求,提高网络请求处理的并行度。

需要注意的是,Redis 多 IO 线程模型只用来处理网络读写请求,对于 Redis 的读写命令,依然是单线程处理

这是因为,网络处理经常是瓶颈,通过多线程并行处理可提高性能。

而继续使用单线程执行读写命令,不需要为了保证 Lua 脚本、事务、等开发多线程安全机制,实现更简单。

架构图如下

主线程与 IO 多线程是如何实现协作呢?

如下图:

主要流程

  1. 主线程负责接收建立连接请求,获取 socket 放入全局等待读处理队列;
  2. 主线程通过轮询将可读 socket 分配给 IO 线程;
  3. 主线程阻塞等待 IO 线程读取 socket 完成;
  4. 主线程执行 IO 线程读取和解析出来的 Redis 请求命令;
  5. 主线程阻塞等待 IO 线程将指令执行结果回写回 socket完毕;
  6. 主线程清空全局队列,等待客户端后续的请求。

思路:将主线程 IO 读写任务拆分出来给一组独立的线程处理,使得多个 socket 读写可以并行化,但是 Redis 命令还是主线程串行执行。

如何开启多线程呢?

Redis 6.0 的多线程默认是禁用的,只使用主线程。如需开启需要修改 redis.conf 配置文件:io-threads-do-reads yes

码老湿,线程数是不是越多越好?

当然不是,关于线程数的设置,官方有一个建议:4 核的机器建议设置为 2 或 3 个线程,8核的建议设置为 6 个线程,线程数一定要小于机器核数。

线程数并不是越大越好,官方认为超过了 8 个基本就没什么意义了。

另外,开启多线程后,还需要设置线程数,否则是不生效的

io-threads 4

总结与思考

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

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

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

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

模型缺陷

Redis 的多线程网络模型实际上并不是一个标准的 Multi-Reactors/Master-Workers 模型,Redis 的多线程方案中,I/O 线程任务仅仅是通过 socket 读取客户端请求命令并解析,却没有真正去执行命令。

所有客户端命令最后还需要回到主线程去执行,因此对多核的利用率并不算高,而且每次主线程都必须在分配完任务之后忙轮询等待所有 I/O 线程完成任务之后才能继续执行其他逻辑。

在我看来,Redis 目前的多线程方案更像是一个折中的选择:既保持了原系统的兼容性,又能利用多核提升 I/O 性能。

6.0版本之前 Redis 为什么选择使用单线程来执行请求

Redis 为什么会选择使用单线程呢?这是因为 CPU 成为 Redis 瓶颈的情况并不常见,成为 Redis 瓶颈的通常是内存或网络带宽。例如,在一个普通的 Linux 系统上使用 pipelining 命令,Redis 可以每秒完成 100 万个请求,所以如果我们的应用程序主要使用 O(N) 或 O(log(N)) 复杂度的命令,它几乎不会使用太多的 CPU。

那么既然 CPU 不会成为瓶颈,理所当然的就没必要去使用多线程来执行命令,我们需要明确的一个问题就是多线程一定比单线程快吗?答案是不一定。因为多线程也是有代价的,最直接的两个代价就是线程的创建和销毁线程(当然可以通过线程池来一定程度的减少频繁的创建线程和销毁线程)以及线程的上下文切换。

在我们的日常系统中,主要可以区分为两种:CPU 密集型 和 IO 密集型。

CPU 密集型:这种系统就说明 CPU 的利用率很高,那么使用多线程反而会增加上下文切换而带来额外的开销,所以使用多线程效率可能会不升反降。举个例子:假如你现在在干活,你一直不停的在做一件事,需要 1 分钟可以做完,但是你中途总是被人打断,需要花 1 秒钟时间步行到旁边去做另一件事,假如这件事也需要 1 分钟,那么你因为反复切换做两件事,每切换一次就要花 1 秒钟,最后做完这 2 件事的时间肯定大于 2 分钟(取决于中途切换的次数),但是如果中途不被打断,你做完一件事再去做另一件事,那么你最多只需要切换 1 次,也就是 2 分 1 秒就能做完。

IO 密集型:IO 操作也可以分为磁盘 IO 和网络 IO 等操作。大部分 IO 操作的特点是比较耗时且 CPU 利用率不高,所以 Redis 6.0 版本网络 IO 会改进为多线程。至于磁盘 IO,因为 Redis 中的数据都存储在内存(也可以持久化),所以并不会过多的涉及到磁盘操作。举个例子:假如你现在给树苗浇水,你每浇完一次水之后就需要等别人给你加水之后你才能继续浇,那么假如这个等待过程需要 5 秒钟,也就是说你浇完一次水就可以休息 5 秒钟,而你切换去做另一件事来回只需要 2 秒,那么你完全可以先去做另一件事,做完之后再回来,这样就可以充分利用你空闲的 5 秒钟时间,从而提升了效率。

使用多线程还会带来一个问题就是数据的安全性,所以多线程编程都会涉及到锁竞争,由此也会带来额外的开销。


Redis保证高效的原因

1)数据结构

数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的,底层数据结构包括简单动态字符串(SDS)、链表、字典、跳跃表、整数集合、压缩列表。Redis并没有直接使用这些数据结构来构建键值对数据库,而是基于这些数据结构创建了一个对象系统,对象设置有多种不同的数据结构实现,从而优化对象在不同场景下的使用效率。

2)纯内存操作

Redis 将所有数据放在内存中,内存的响应时长大约为 100 纳秒,这是 redis 的 QPS 过万的重要基础。

3)基于非阻塞的IO多路复用机制

有了非阻塞 IO 意味着线程在读写 IO 时可以不必再阻塞了,读写可以瞬间完成然后线程可以继续干别的事了。

redis 需要处理多个 IO 请求,同时把每个请求的结果返回给客户端。由于 redis 是单线程模型,同一时间只能处理一个 IO 事件,于是 redis 需要在合适的时间暂停对某个 IO 事件的处理,转而去处理另一个 IO 事件,这就需要用到IO多路复用技术了, 就好比一个管理者,能够管理个socket的IO事件,当选择了哪个socket,就处理哪个socket上的 IO 事件,其他 IO 事件就暂停处理了。

4)单线程反而避免了多线程的频繁上下文切换带来的性能问题

单线程避免了线程切换和竞态产生的消耗,对于服务端开发来说,锁和线程切换通常是性能杀手。

单线程的问题:对于每个命令的执行时间是有要求的。如果某个命令执行过长,会造成其他命令的阻塞,所以 redis 适用于那些需要快速执行的场景。

5)主线程不处理低级别的事件

Redis为了高效的处理客户端的事件,并没有将持久化文件放在主线程里面进行处理,而是Redis在适当的时机fork子进程来异步的处理这种任务,Redis会fork子进程进行处理持久化文件操作(将数据写到RDB 文件中)。Redis还有一组异步任务处理线程,用于处理不需要主线程同步处理的工作,即处理一些低级别的事件(AOF文件重写)。

  • 0
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

猎户星座。

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值