本篇博客的视频教程首发于 Youtube:科技小飞哥,加入 电报粉丝群 获得最新视频更新和问题解答。

Redis基础

Redis是什么

Redis是一个基于BSD开源的项目,是一个把结构化的数据放在内存中的一个存储系统。

你可以把它作为数据库,缓存和消息中间件来使用。同时支持stringslistshashessetssorted setsbitmapshyperloglogsgeospatial indexes等数据类型。

它还通过redis sentinel实现高可用,通过redis cluster实现了自动分片。以及事务,发布/订阅,自动故障转移等等。

为什么用Redis

而在后端开发的技术选型中,Redis已经成为了一个不可绕过的解决方案工具。因此Redis成为了后端开发的基本技能之一。当然,也是后端面试中必考的技术栈之一。

Redis的优点,如果只用一个字来解释,那就是:快!

Redis 有多快?官方给出的答案是读写速度 10万/秒,如果说这是在单线程情况下跑出来的成绩,你会不会惊讶?为什么单线程的 Redis 速度这么快?

Redis为什么快

主要有以下几点:

  1. Redis 是基于内存的。 内存的读写速度非常快。当然Redis也存在持久化操作,但是是fork子进程和利用 Linux 系统的页缓存技术来完成,并不会影响Redis的读写性能。
  2. Redis 是单线程的。 避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗。
  3. Redis 使用多路复用技术。 可以处理并发的连接。非阻塞 IO 部实现采用 epoll,采用了 epoll+自己实现的简单的事件框架。epoll 中的读、写、关闭、连接都转化成了事件,然后利用 epoll 的多路复用特性,绝不在 IO 上浪费一点时间。
  4. Redis 中的数据结构是专门进行设计的。 数据结构简单。对数据操作也简单。

Redis是单线程的吗

我们经常听到,Redis是单线程的,这句话对吗?

基本上是对的,但是不准确。

而对于为什么使用单线程,官方有一句解释:

It’s not very frequent that CPU becomes your bottleneck with Redis, as usually Redis is either memory or network bound.

意思就是:
因为 Redis 是基于内存的操作,CPU 不是 Redis 的瓶颈。Redis 的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且 CPU 不会成为瓶颈,那就顺理成章地采用单线程的方案了。

为什么说不准确呢?

我们需要回顾Redis的两个最重要的版本更新:

  1. Redis 4.0 为了防止耗时的命令阻塞线程,导致无法处理后续事件。引入了多线程来处理一些非阻塞命令。有:UNLINKFLUSHALL ASYNCFLUSHDB ASYNC等。
    但是整个网络模型依然是单线程的,所以我们称之为单线程。

  2. Redis 6.0 就真正的在网络模型上加入多线程IO来解决网络IO的性能瓶颈。 此时IO读写是多线程的,执行命令依旧是单线程的。

Redis网络模型

一张图看懂Redis的单线程模型:

redis_io

redis的网络事件处理器是基于Reactor模式,又叫做文件事件处理器。

文件事件处理器使用I/O多路复用来同时监听多个套接字,并根据套接字执行的任务关联到不同的事件处理器。
文件事件以单线程方式运行,但通过使用I/O多路复用程序来监听多个套接字,文件事件处理器实现了高性能的网络通信模型。
Redis 在处理客户端的请求时,包括接收(socket读)、解析执行发送(socket 写) 等都由一个顺序串行的主线程处理,这就是所谓的单线程

Reactor模型

Redis的单线程网络模型,这就是一个经典的Reactor的模型,其本质上是 I/O 多路复用(I/O multiplexing) + 非阻塞 I/O(non-blocking I/O)的模式。

是一种基于事件驱动模型的设计模式。

我们来看一下Reactor里面两种经典的模型。

单线程Reactor模型

Redis的单线程模型就是使用的经典的单线程Reactor模型。

我们先看看单线程的Reactor模型

redis

消息处理流程:

  1. Reactor对象通过select/poll/epoll等IO多路复用监控连接事件,收到事件后通过dispatcher事件分发器进行转发。
  2. 如果是连接建立的事件,则由acceptor接受连接,并创建Handler处理后续事件。
  3. 如果不是建立连接事件,则Reactor会分发调用Handler来响应。
  4. Handler会完成read->解析->执行->send的完整业务流程。

优点:

  • 单线程运行,串行操作,不需要加锁,逻辑简单。

缺点:

  • 仅用一个线程处理请求,对于多核资源机器来说是有点浪费的。
  • 当处理读写任务的线程负载比较重,将会阻塞后续的事件处理,导致整体延迟变大。

应用:

  • Redis网络模型。(6.0版本以前)

Master-Worker Reactor模型

redis

比起单线程模型,它是将Reactor分成两部分:

  • mainReactor 负责监听server socket,用来处理网络IO连接建立操作,将建立的socketChannel指定注册给subReactor。 (只负责监听)
  • subReactor 主要做和建立起来的socket做数据交互和事件业务处理操作。通常,subReactor个数上可与CPU个数等同。一般是多个,这样的话,就可以充分利用多核的优势。 (负责IO读写和命令的执行)

区别于单线程Reactor模式,这种模式不再是单线程的事件循环,而是有多个线程subReactors各自维护一个独立的事件循环,由 mainReactor 负责接收新连接并分发给 subReactors 去独立处理,最后 subReactors 回写响应给客户端。

优点:

  • 响应快,不必为单个同步时间所阻塞,虽然Reactor本身依然是同步的;
  • 可扩展性,可以方便地通过增加Reactor实例个数来充分利用CPU资源;

缺点:

  • 如果多个线程可能操作同一份数据,就涉及到底层数据同步的问题,则必然会引入某些同步机制,比如锁。增加了代码复杂度,同时增加了同步机制的开销。

应用:

  • Nginx, Netty, Swoole, Memcached就是使用的这个模型

Redis 6.0的多线程网络模型

Redis 6.0版本之后,Redis 正式在核心网络模型中引入了多线程,也就是所谓的 I/O threading,至此 Redis 真正拥有了多线程模型。 但是Redis的多线程模型却并非标准的Master-Worker Reactor模型。他的多线程 只负责IO读写,不负责具体的执行。

为什么Redis 6.0 要使用多线程

之前说了,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存大小和网络带宽。 从Redis自身角度来说,因为读写网络的read/write系统调用占用了Redis执行期间大部分CPU时间,瓶颈主要在于网络的 IO 消耗, 所以选择多线程IO来实现读写。主线程来执行Redis命令。

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

Redis 6.0 网络模型

redis

为什么这么设计呢?

  1. 前面提到 Redis 最初选择单线程网络模型的理由是:CPU 通常不会成为性能瓶颈,瓶颈往往是内存和网络,因此单线程足够了。那么为什么现在 Redis 又要引入多线程呢?很简单,就是 Redis 的网络 I/O 瓶颈已经越来越明显了。所以这个多线程是为了解决IO的瓶颈的。
  2. 如果多线程包括了IO读写,解析和执行的整个过程,那么多线程需要面临线程安全的问题,Redis 6.0版本之前是没有考虑线程安全的,如果使用多线程来处理命令的执行,需要大量的改动来保证多线程的安全机制,实现更复杂。为了避免了不必要的上下文切换和竞争条件,多线程导致的切换而消耗 CPU,也不用考虑各种锁的问题,就让执行这一步只使用主线程。

Redis 6.0和Memcached多线程模型对比

相同点:

  • 都采用了 Master-Worker 的线程的模型

不同点:

  • Memcached 执行主逻辑也是在 Worker 线程里,模型更加简单,实现了真正的线程隔离,通过各种锁机制来保证数据的线程安全。
  • 而 Redis 把执行逻辑交还给 Master 线程,虽然一定程度上增加了模型复杂度,但也解决了数据的线程安全问题。

总结

让我们来回顾一下 Redis 多线程网络模型的设计方案:

  • 使用 I/O 线程实现网络 I/O 多线程化,I/O 线程只负责网络 I/O 和命令解析,不执行具体的命令。

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

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

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

<全文完>