Redis(五)线程模型

一、Redis 有多快?

Redis 是基于内存运行的高性能 K-V 数据库,官方提供的测试报告是单机可以支持约 10w/s 的 QPS
image.png

二、Redis 为什么这么快?

(1)完全基于内存,数据存在内存中,绝大部分请求是纯粹的内存操作,非常快速,跟传统的磁盘文件数据存储相比,避免了通过磁盘 IO 读取到内存这部分的开销。
(2)数据结构简单,对数据操作也简单。Redis 中的数据结构是专门进行设计的,每种数据结构都有一种或多种数据结构来支持。Redis 正是依赖这些灵活的数据结构,来提升读取和写入的性能。
(3)采用单线程,省去了很多上下文切换的时间以及 CPU 消耗,不存在竞争条件,不用去考虑各种锁的问题,不存在加锁释放锁操作,也不会出现死锁而导致的性能消耗。
(4)使用基于 IO 多路复用机制的线程模型,可以处理并发的链接。
Redis 基于 Reactor 模式开发了自己的网络事件处理器,这个处理器被称为文件事件处理器 file event handler。由于这个文件事件处理器是单线程的,所以 Redis 才叫做单线程的模型,但是它采用 IO 多路复用机制同时监听多个 Socket,并根据 Socket 上的事件来选择对应的事件处理器进行处理。文件事件处理器的结构包含 4 个部分,线程模型如下图:

多个 Socket
IO 多路复用程序
文件事件分派器
事件处理器(命令请求处理器、命令回复处理器、连接应答处理器)

image.png
image.png
多个 Socket 可能会产生不同的操作,每个操作对应不同的文件事件,但是 IO 多路复用程序会监听多个 Socket,将 Socket 产生的事件放入队列中排队,事件分派器每次从队列中取出一个事件,把该事件交给对应的事件处理器进行处理。
Redis 客户端对服务端的每次调用都经历了发送命令,执行命令,返回结果三个过程。其中执行命令阶段,由于 Redis 是单线程来处理命令的,所有每一条到达服务端的命令不会立刻执行,所有的命令都会进入一个队列中,然后逐个被执行。并且多个客户端发送的命令的执行顺序是不确定的。但是可以确定的是不会有两条命令被同时执行,不会产生并发问题,这就是 Redis 的单线程基本模型。

多路 I/O 复用模型是利用 select、poll、epoll 可以同时监察多个流的 I/O 事件的能力,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有 I/O 事件时,就从阻塞态中唤醒,然后程序就会轮询一遍所有的流(epoll 是只轮询那些真正发出了事件的流),并且依次顺序处理就绪的流,这种做法就避免了大量的无用操作。
这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。采用多路 I/O 复用技术可以让单个线程高效的处理多个客户端的网络 IO 连接请求(尽量减少网络 IO 的时间消耗)

(5)Redis 直接自己构建了 VM 机制 ,避免调用系统函数的时候,浪费时间去移动和请求

三、为什么 Redis 是单线程?

这里我们强调的单线程,指的是网络请求模块使用一个线程来处理,即一个线程处理所有网络请求,其他模块仍用了多个线程。
那为什么使用单线程呢?官方答案是:因为 CPU 不是 Redis 的瓶颈,Redis 的瓶颈最有可能是机器内存或者网络带宽。既然单线程容易实现,而且 CPU 不会成为瓶颈,那就顺理成章地采用单线程的方案了。
但是,我们使用单线程的方式是无法发挥多核 CPU 性能,不过我们可以通过在单机开多个 Redis 实例来解决这个问题

四、Redis6.0 的多线程?

1、Redis6.0 之前为什么一直不使用多线程?
Redis 使用单线程的可维护性高。多线程模型虽然在某些方面表现优异,但是它却引入了程序执行顺序的不确定性,带来了并发读写的一系列问题,增加了系统复杂度、同时可能存在线程切换、甚至加锁解锁、死锁造成的性能损耗。
2、Redis6.0 为什么要引入多线程呢?
因为 Redis 的瓶颈不在内存,而是在网络 I/O 模块带来 CPU 的耗时,所以 Redis6.0 的多线程是用来处理网络 I/O 这部分,充分利用 CPU 资源,减少网络 I/O 阻塞带来的性能损耗。
3、Redis6.0 如何开启多线程?
默认情况下 Redis 是关闭多线程的,可以在 conf 文件进行配置开启:
io-threads-do-reads yes
io-threads 线程数
“##”官方建议的线程数设置:4 核的机器建议设置为 2 或 3 个线程,8 核的建议设置为 6 个线程,线程数一定要小于机器核数,尽量不超过 8 个。
4、多线程模式下,是否存在线程并发安全问题?
如图,一次 redis 请求,要建立连接,然后获取操作的命令,然后执行命令,最后将响应的结果写到 socket 上。
image.png
在 redis 的多线程模式下,获取、解析命令,以及输出结果两个过程,可以配置成多线程执行的,因为它毕竟是我们定位到的主要耗时点,但是命令的执行,也就是内存操作,依然是单线程运行的。所以,Redis 的多线程部分只是用来处理网络数据的读写和协议解析,执行命令仍然是单线程顺序执行,也就不存在并发安全问题。

Redis 为什么用单线程

Redis 单线程指的什么

Redis 是单线程,主要是指 Redis 的网络 IO 和键值对读写是由一个线程来完成的,这也是 Redis 对外提供键值存储服务的主要流程。 但 Redis 的其他功能,比如持久化、异步删除、集群数据同步等,其实是由额外的线程执 行的。

多线程提高系统吞吐率

日常写程序时,我们经常会听到一种说法:“使用多线程,可以增加系统吞吐率,或是可 以增加系统扩展性。”的确,对于一个多线程的系统来说,在有合理的资源分配的情况 下,可以增加系统中处理请求操作的资源实体,进而提升系统能够同时处理的请求数,即 吞吐率。下面的左图是我们采用多线程时所期待的结果。
但是,请你注意,通常情况下,在我们采用多线程后,如果没有良好的系统设计,实际得 到的结果,其实是右图所展示的那样。我们刚开始增加线程数时,系统吞吐率会增加,但 是,再进一步增加线程时,系统吞吐率就增长迟缓了,有时甚至还会出现下降的情况
image.png

多线程共享资源

为什么会出现这种情况呢?一个关键的瓶颈在于,系统中通常会存在被多线程同时访问的 共享资源,比如一个共享的数据结构。当有多个线程要修改这个共享资源时,为了保证共 享资源的正确性,就需要有额外的机制进行保证,而这个额外的机制,就会带来额外的开 销。

举个栗子

拿 Redis 来说,Redis 有 List 的数据类型,并提供出队(LPOP) 和入队(LPUSH)操作。假设 Redis 采用多线程设计,如下图所示,现在有两个线程 A 和 B,线程 A 对一个 List 做 LPUSH 操作,并对队列长度加 1。同时,线程 B 对该 List 执行 LPOP 操作,并对队列长度减 1。为了保证队列长度的正确性,Redis 需要让线程 A 和 B 的 LPUSH 和 LPOP 串行执行,这样一来,Redis 可以无误地记录它们对 List 长度的修 改。否则,我们可能就会得到错误的长度结果。这就是多线程编程模式面临的共享资源的 并发访问控制问题。
image.png
并发访问控制一直是多线程开发中的一个难点问题,如果没有精细的设计,比如说,只是 简单地采用一个粗粒度互斥锁,就会出现不理想的结果:即使增加了线程,大部分线程也 在等待获取访问共享资源的互斥锁,并行变串行,系统吞吐率并没有随着线程的增加而增 加。甚至减少,因为多线程会存在多线程频繁切换开销
采用多线程开发一般会引入同步原语来保护共享资源的并发访问,这也会降低系统 代码的易调试性和可维护性。为了避免这些问题,Redis 直接采用了单线程模式。

Redis 使用单线程小结

  • 使用多线程,可以增加系统吞吐率,如果没有良好的系统设计,实际得 到的结果我们刚开始增加线程数时,系统吞吐率会增加,但 是,再进一步增加线程时,系统吞吐率就增长迟缓了,有时甚至还会出现下降的情况
  • 出现上述情况的原因是 redis 在使用多线程时同样会存在多线程同时访问的 共享资源的问题,为了保证共 享资源的正确性,就需要有额外的机制进行保证。
  • 在没有良好的系统设计,这个额外的机制,就会带来额外的开 销。这种开销有时不但无法增加系统吞吐率,反而会降低系统吞吐率
  • 多线程开发一般会引入同步原语来保护共享资源的并发访问,这也会降低系统 代码的易调试性和可维护性

单线程 Redis 为什么快

多路复用 IO

https://alicharles.oss-cn-hangzhou.aliyuncs.com/static/images/mp_qrcode.jpg
文章目录
  1. 一、Redis 有多快?
  2. 二、Redis 为什么这么快?
  3. 三、为什么 Redis 是单线程?
  4. 四、Redis6.0 的多线程?
    1. Redis 为什么用单线程
      1. Redis 单线程指的什么
      2. 多线程提高系统吞吐率
      3. 多线程共享资源
        1. 举个栗子
      4. Redis 使用单线程小结
    2. 单线程 Redis 为什么快
      1. 多路复用 IO