根据官方数据,Redis 的 QPS 可以达到约 100000(每秒请求数)。

redis qps瓶颈多少 redis的qps_Redis

  1. 基于内存实现
  2. 高效的数据结构
  3. 合理的数据编码
  4. 单线程模型
  5. I/O 多路复用模型



一、 完全基于内存实现

Redis 是用 C 语言实现的,是基于内存的数据库,不论读写操作都是在内存上完成的。跟磁盘数据库相比,完全吊打磁盘的速度。对于磁盘数据库来说,首先要将数据通过 IO 操作读取到内存里。

磁盘调用栈图:

redis qps瓶颈多少 redis的qps_缓存_02

内存操作

内存直接由 CPU 控制,也就是 CPU 内部集成的内存控制器,所以说内存是直接与 CPU 对接,享受与 CPU 通信的最优带宽。

Redis 将数据存储在内存中,读写操作不会因为磁盘的 IO 速度限制,所以速度飞一般的感觉!

一张图量化系统的各种延时时间(部分数据引用 Brendan Gregg

redis qps瓶颈多少 redis的qps_数据库_03




二、 高效的数据结构



MySQL为了提高检索速度使用了 B+ Tree 数据结构,所以 Redis 速度快应该也跟数据结构有关。

数据结构是为了追求速度,不同数据类型使用不同的数据结构速度才得以提升。每种数据类型都有一种或者多种数据结构来支撑,底层数据结构有 6 种。

redis qps瓶颈多少 redis的qps_数据库_04

  • Redis hash 字典
  • SDS 简单动态字符
  • zipList 压缩列表
  • 双端列表
  • skipList 跳跃表
  • 整数数组(intset)

redis qps瓶颈多少 redis的qps_redis_05

注意:这里所说的数据结构并不是 Redis 提供给我们使用的 5 种数据类型:String、List、Hash、Set、SortedSet。

在 Redis 中,常用的 5 种数据类型和应用场景如下:

  • String:缓存、计数器、分布式锁等。
  • List:链表、队列、微博关注人时间轴列表等。
  • Hash:用户信息、Hash 表等。
  • Set:去重、赞、踩、共同好友等。
  • Zset:访问量排行榜、点击量排行榜等。

上面的应该叫做 Redis 支持的数据类型,也就是数据的保存形式。这里要说的是针对这 5 种数据类型,底层都运用了哪些高效的数据结构来支持。





1. Redis hash 字典

Redis 整体就是一个哈希表来保存所有的键值对,无论数据类型是 5 种的任意一种。哈希表,本质就是一个数组,每个元素被叫做哈希桶,不管什么数据类型,每个桶里面的 entry 保存着实际具体值的指针。

redis qps瓶颈多少 redis的qps_缓存_06

整个数据库就是一个全局哈希表,而哈希表的时间复杂度是 O(1),只需要计算每个键的哈希值,便知道对应的哈希桶位置,定位桶里面的 entry 找到对应数据,这个也是 Redis 快的原因之一。

Hash 冲突:

当写入 Redis 的数据越来越多的时候,哈希冲突不可避免,会出现不同的 key 计算出一样的哈希值。

Redis 通过链式哈希解决冲突:也就是同一个 桶里面的元素使用链表保存。但是当链表过长就会导致查找性能变差可能,所以 Redis 为了追求快,使用了两个全局哈希表。用于 rehash 操作,增加现有的哈希桶数量,减少哈希冲突。

开始默认使用 hash 表 1 保存键值对数据,哈希表 2 此刻没有分配空间。当数据越来多触发 rehash 操作,则执行以下操作:

  • 给 hash 表 2 分配更大的空间;
  • 将 hash 表 1 的数据重新映射拷贝到 hash 表 2 中;
  • 释放 hash 表 1 的空间。

值得注意的是,将 hash 表 1 的数据重新映射到 hash 表 2 的过程中并不是一次性的,这样会造成 Redis 阻塞,无法提供服务。

而是采用了渐进式 rehash,每次处理客户端请求的时候,先从 hash 表 1 中第一个索引开始,将这个位置的 所有数据拷贝到 hash 表 2 中,就这样将 rehash 分散到多次请求过程中,避免耗时阻塞。




2. SDS 简单动态字符

字符串结构使用最广泛,通常我们用于缓存登陆后的用户信息,key = userId,value = 用户信息 JSON 序列化成字符串。

C 语言中字符串的获取 「MageByte」的长度,要从头开始遍历,直到 「\0」为止,Redis 作为唯快不破的男人是不能忍受的。

C 语言字符串结构与 SDS 字符串结构对比图如下所示:

redis qps瓶颈多少 redis的qps_数据库_07

SDS 与 C 字符串区别:



1)O(1) 时间复杂度获取字符串长度

C 语言字符串不记录长度信息,需要遍历整个字符串时间复杂度为 O(n),C 字符串遍历时遇到 ‘\0’ 时结束。

SDS 中 len 保存字符串的长度,O(1) 时间复杂度。



2)空间预分配

SDS 被修改后,程序不仅会为 SDS 分配所需要的必须空间,还会分配额外的未使用空间。

分配规则如下:如果对 SDS 修改后,len 的长度小于 1M,那么程序将分配和 len 相同长度的未使用空间。举个例子,如果 len=10,重新分配后,buf 的实际长度会变为 10(已使用空间)+10(额外空间)+1(空字符)=21。如果对 SDS 修改后 len 长度大于 1M,那么程序将分配 1M 的未使用空间。



3)惰性空间释放

当对 SDS 进行缩短操作时,程序并不会回收多余的内存空间,而是使用 free 字段将这些字节数量记录下来不释放,后面如果需要 append 操作,则直接使用 free 中未使用的空间,减少了内存的分配。



4)二进制安全

在 Redis 中不仅可以存储 String 类型的数据,也可能存储一些二进制数据。

二进制数据并不是规则的字符串格式,其中会包含一些特殊的字符如 ‘\0’,在 C 中遇到 ‘\0’ 则表示字符串的结束,但在 SDS 中,标志字符串结束的是 len 属性。




3. zipList 压缩列表

压缩列表是 List 、hash、 sorted Set 三种数据类型底层实现之一。

当一个列表只有少量数据的时候,并且每个列表项要么就是小整数值,要么就是长度比较短的字符串,那么 Redis 就会使用压缩列表来做列表键的底层实现。

ziplist 是由一系列特殊编码的连续内存块组成的顺序型的数据结构,ziplist 中可以包含多个 entry 节点,每个节点可以存放整数或者字符串。

ziplist 在表头有三个字段 zlbytes、zltail 和 zllen,分别表示列表占用字节数、列表尾的偏移量和列表中的 entry 个数;压缩列表在表尾还有一个 zlend,表示列表结束。

struct ziplist<T> {

int32 zlbytes; // 整个压缩列表占用字节数

int32 zltail_offset; // 最后一个元素距离压缩列表起始位置的偏移量,用于快速定位到最后一个节点

int16 zllength; // 元素个数

T[] entries; // 元素内容列表,挨个挨个紧凑存储

int8 zlend; // 标志压缩列表的结束,值恒为 0xFF

}

redis qps瓶颈多少 redis的qps_redis_08


如果我们要查找定位第一个元素和最后一个元素,可以通过表头三个字段的长度直接定位,复杂度是 O(1)。而查找其他元素时,就没有这么高效了,只能逐个查找,此时的复杂度就是 O(N)。



4. 双端列表

Redis List 数据类型通常被用于队列、微博关注人时间轴列表等场景。不管是先进先出的队列,还是先进后出的栈,双端列表都很好的支持这些特性。



Redis 的链表实现的特性可以总结如下:

  • 双端:链表节点带有 prev 和 next 指针,获取某个节点的前置节点和后置节点的复杂度都是 O(1)。
  • 无环:表头节点的 prev 指针和表尾节点的 next 指针都指向 NULL,对链表的访问以 NULL 为终点。
  • 带表头指针和表尾指针:通过 list 结构的 head 指针和 tail 指针,程序获取链表的表头节点和表尾节点的复杂度为 O(1)。
  • 带链表长度计数器:程序使用 list 结构的 len 属性来对 list 持有的链表节点进行计数,程序获取链表中节点数量的复杂度为 O(1)。
  • 多态:链表节点使用 void* 指针来保存节点值,并且可以通过 list 结构的 dup、free、match 三个属性为节点值设置类型特定函数,所以链表可以用于保存各种不同类型的值。

后续版本对列表数据结构进行了改造,使用 quicklist 代替了 ziplist 和 linkedlist。

quicklist 是 ziplist 和 linkedlist 的混合体,它将 linkedlist 按段切分,每一段使用 ziplist 来紧凑存储,多个 ziplist 之间使用双向指针串接起来。

redis qps瓶颈多少 redis的qps_redis_09

这也是为何 Redis 快的原因,不放过任何一个可以提升性能的细节。




5. skipList 跳跃表

sorted set 类型的排序功能便是通过「跳跃列表」数据结构来实现。

跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。

跳跃表支持平均 O(logN)、最坏 O(N)复杂度的节点查找,还可以通过顺序性操作来批量处理节点。

跳表在链表的基础上,增加了多层级索引,通过索引位置的几个跳转,实现数据的快速定位,如下图所示:

redis qps瓶颈多少 redis的qps_Redis_10

当需要查找 40 这个元素需要经历 三次查找。




6. 整数数组(intset)

当一个集合只包含整数值元素,并且这个集合的元素数量不多时,Redis 就会使用整数集合作为集合键的底层实现。结构如下:

typedef struct intset{

//编码方式

uint32_t encoding;

//集合包含的元素数量

uint32_t length;

//保存元素的数组

int8_t contents[];

}intset;

contents 数组是整数集合的底层实现:整数集合的每个元素都是 contents 数组的一个数组项(item),各个项在数组中按值的大小从小到大有序地排列,并且数组中不包含任何重复项。length 属性记录了整数集合包含的元素数量,也即是 contents 数组的长度。




三、 合理的数据编码



Redis 使用对象(redisObject)来表示数据库中的键值,当我们在 Redis 中创建一个键值对时,至少创建两个对象,一个对象是用做键值对的键对象,另一个是键值对的值对象。

例如我们执行 SET MSG XXX 时,键值对的键是一个包含了字符串“MSG“的对象,键值对的值对象是包含字符串"XXX"的对象。

redisObject

typedef struct redisObject{

//类型

unsigned type:4;

//编码

unsigned encoding:4;

//指向底层数据结构的指针

void *ptr;

//...

}robj;

其中 type 字段记录了对象的类型,包含字符串对象、列表对象、哈希对象、集合对象、有序集合对象。

对于每一种数据类型来说,底层的支持可能是多种数据结构,什么时候使用哪种数据结构,这就涉及到了编码转化的问题。

那我们就来看看,不同的数据类型是如何进行编码转化的:

String:存储数字的话,采用 int 类型的编码,如果是非数字的话,采用 raw 编码;

List:List 对象的编码可以是 ziplist 或 linkedlist,字符串长度 < 64 字节且元素个数 < 512 使用 ziplist 编码,否则转化为 linkedlist 编码;

注意:这两个条件是可以修改的,在 redis.conf 中:

list-max-ziplist-entries 512

list-max-ziplist-value 64

Hash:Hash 对象的编码可以是 ziplist 或 hashtable。

当 Hash 对象同时满足以下两个条件时,Hash 对象采用 ziplist 编码:

  • Hash 对象保存的所有键值对的键和值的字符串长度均小于 64 字节。
  • Hash 对象保存的键值对数量小于 512 个。

否则就是 hashtable 编码。

Set:Set 对象的编码可以是 intset 或 hashtable,intset 编码的对象使用整数集合作为底层实现,把所有元素都保存在一个整数集合里面。

保存元素为整数且元素个数小于一定范围使用 intset 编码,任意条件不满足,则使用 hashtable 编码;

Zset:Zset 对象的编码可以是 ziplist 或 skiplist,当采用 ziplist 编码存储时,每个集合元素使用两个紧挨在一起的压缩列表来存储。

Ziplist 压缩列表第一个节点存储元素的成员,第二个节点存储元素的分值,并且按分值大小从小到大有序排列。

redis qps瓶颈多少 redis的qps_数据库_11

当 Zset 对象同时满足以下两个条件时,采用 ziplist 编码:

  • Zset 保存的元素个数小于 128。
  • Zset 元素的成员长度都小于 64 字节。
    如果不满足以上条件的任意一个,ziplist 就会转化为 skiplist 编码。注意:这两个条件是可以修改的,在 redis.conf 中:
zset-max-ziplist-entries 128

zset-max-ziplist-value 64




四、 单线程模型



Redis 的单线程指的是 Redis 的网络 IO 以及键值对指令读写是由一个线程来执行的。 对于 Redis 的持久化、集群数据同步、异步删除等都是其他线程执行。

至于为啥用单线程,我们先了解多线程有什么缺点。



多线程的弊端

使用多线程,通常可以增加系统吞吐量,充分利用 CPU 资源。

但是,使用多线程后,没有良好的系统设计,可能会出现如下图所示的场景,增加了线程数量,前期吞吐量会增加,再进一步新增线程的时候,系统吞吐量几乎不再新增,甚至会下降!

redis qps瓶颈多少 redis的qps_redis qps瓶颈多少_12


在运行每个任务之前,CPU 需要知道任务在何处加载并开始运行。也就是说,系统需要帮助它预先设置 CPU 寄存器和程序计数器,这称为 CPU 上下文。

这些保存的上下文存储在系统内核中,并在重新计划任务时再次加载。这样,任务的原始状态将不会受到影响,并且该任务将看起来正在连续运行。

切换上下文时,我们需要完成一系列工作,这是非常消耗资源的操作。

另外,当多线程并行修改共享数据的时候,为了保证数据正确,需要加锁机制就会带来额外的性能开销,面临的共享资源的并发访问控制问题。

引入多线程开发,就需要使用同步语句来保护共享资源的并发读写,增加代码复杂度和调试难度。



单线程的好处
  • 不会因为线程创建导致的性能消耗;
  • 避免上下文切换引起的 CPU 消耗,没有多线程切换的开销;
  • 避免了线程之间的竞争问题,比如添加锁、释放锁、死锁等,不需要考虑各种锁问题。
  • 代码更清晰,处理逻辑简单。

单线程是否没有充分利用 CPU 资源呢?

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

可以参考原文地址:https://redis.io/topics/faq




五、 I/O 多路复用模型



Redis 采用 I/O 多路复用技术,并发处理连接。采用了 epoll + 自己实现的简单的事件框架。epoll 中的读、写、关闭、连接都转化成了事件,然后利用 epoll 的多路复用特性,绝不在 IO 上浪费一点时间。

在解释 IO 多虑复用之前我们先了解下基本 IO 操作会经历什么。



基本 IO 模型

一个基本的网络 IO 模型,当处理 get 请求,会经历以下过程:

  • 与客户端建立 accept;
  • 从 socket 中读取请求 recv;
  • 解析客户端发送的请求 parse;
  • 执行 get 指令;
  • 响应客户端数据,也就是 向 socket 写回数据。

其中,bind/listen、accept、recv、parse 和 send 属于网络 IO 处理,而 get 属于键值数据操作。既然 Redis 是单线程,那么,最基本的一种实现是在一个线程中依次执行上面说的这些操作。

关键点就是 accept 和 recv 会出现阻塞,当 Redis 监听到一个客户端有连接请求,但一直未能成功建立起连接时,会阻塞在 accept() 函数这里,导致其他客户端无法和 Redis 建立连接。

类似的,当 Redis 通过 recv() 从一个客户端读取数据时,如果数据一直没有到达,Redis 也会一直阻塞在 recv()。

redis qps瓶颈多少 redis的qps_缓存_13

阻塞的原因由于使用传统阻塞 IO ,也就是在执行 read、accept 、recv 等网络操作会一直阻塞等待。如下图所示:

redis qps瓶颈多少 redis的qps_redis_14



NIO非阻塞式io

面向与缓冲区
基于通道实现非阻塞式io
多路io复用实现(选择器)

redis qps瓶颈多少 redis的qps_缓存_15

  • 通道(Channel)----TCP链接道路
    通常我们nio所有的操作都是通过通道开始的,所有的通道都会注册到统一个选择器(Selector)上实现管理,在通过选择器将数据统一写入到 buffer中。
  • 缓冲区(Buffer)------加快数据的读取
    Buffer本质上就是一块内存区,可以用来读取数据,也就先将数据写入到缓冲区中、在统一的写入到硬盘上。
  • 选择器(Selector)------通道的管理,实现多路IO复用机制—一个线程处理多个TCP请求。
  • Selector可以称做为选择器,也可以把它叫做多路复用器,可以在单线程的情况下可以去维护多个Channel,也可以去维护多个连接;
  • 线程–完成具体的业务



IO 多路复用

I/O :网络 I/O;多路:多个 TCP 连接;复用:共用一个线程或进程。

多路指的是多个 socket 连接,复用指的是复用一个线程。多路复用主要有三种技术:select,poll,epoll。epoll 是最新的也是目前最好的多路复用技术。

redis qps瓶颈多少 redis的qps_redis qps瓶颈多少_16

它的基本原理是,内核不是监视应用程序本身的连接,而是监视应用程序的文件描述符。

当客户端运行时,它将生成具有不同事件类型的套接字。在服务器端,应对大量的请求,Redis 中使用I / O 多路复用程序(I / O 多路复用模块)同时监听多个套接字,并将这些事件推送到一个队列里(也就是 下图的 I/O 多路复用程序的 socket 队列),然后通过文件事件分派器将其转发到不同的事件处理器逐个被执行。最终将结果返回给客户端。

简单来说:Redis 单线程情况下,内核会一直监听 socket 上的连接请求或者数据请求,一旦有请求到达就交给 Redis 线程处理,这就实现了一个 Redis 线程处理多个 IO 流的效果。

select/epoll 提供了基于事件的回调机制,即针对不同事件的发生,调用相应的事件处理器。所以 Redis 一直在处理事件,提升 Redis 的响应性能。

redis qps瓶颈多少 redis的qps_redis qps瓶颈多少_17

Redis 线程不会阻塞在某一个特定的监听或已连接套接字上,也就是说,不会阻塞在某一个特定的客户端请求处理上。正因为此,Redis 可以同时和多个客户端连接并处理请求,从而提升并发性。



单线程模型

顺便提一下,为什么 Redis 是单线程的。Redis 中使用了 Reactor 单线程模型,你可能对它并不熟悉。没关系,只需要大概了解一下即可。

redis qps瓶颈多少 redis的qps_缓存_18

这张图里,接收到用户的请求后,全部推送到一个队列里,然后交给文件事件分派器,而它是单线程的工作方式。Redis 又是基于它工作的,所以说 Redis 是单线程的。




Redis 单线程与多线程



Redis是单线程的,这话搁以前,是横着走的,谁都知道的真理。现在不一样,Redis 变了。再说这句话,多少得有质疑的语气来跟你辩驳一番。意志不坚定的,可能就缴械投降,顺着别人走了。

到底是什么样的,各位看官请一起往下看:

redis qps瓶颈多少 redis的qps_Redis_19

①传统阻塞 IO 模型

在讲反应器模式前,这里有必要提一下传统阻塞 IO 模型的处理方式。

在传统阻塞 IO 模型中,由一个独立的 Acceptor 线程来监听客户端的连接,每当有客户端请求过来时,它就会为客户端分配一个新的线程来进行处理。

当同时有多个请求过来,服务端对应的就会分配相应数量的线程。这就会导致 CPU 频繁切换,浪费资源。

有的连接请求过来不做任何事情,但服务端还会分配对应的线程,这样就会造成不必要的线程开销。

这就好比你去餐厅吃饭,你拿着菜单看了半天发现真他娘的贵,然后你就走人了。

这段时间等你点菜的服务员就相当于一个对应的线程,你要点菜可以看作一个连接请求。

redis qps瓶颈多少 redis的qps_redis qps瓶颈多少_20

同时,每次建立连接后,当线程调用读写方法时,线程会被阻塞,直到有数据可读可写,在此期间线程不能做其它事情。

还是上边餐厅吃饭的例子,你出去转了一圈发现还是这家性价比最高。回到这家餐厅又拿着菜单看了半天,服务员也在旁边等你点完菜为止。

这个过程中服务员什么也不能做,只能这么干等着,这个过程相当于阻塞。

redis qps瓶颈多少 redis的qps_redis qps瓶颈多少_21

你看这样的方式,每来一个请求就要分配一个线程,并且还得阻塞地等线程处理完。

有的请求还只是过来连接下,什么操作也不干,还得为它分配一个线程,对服务器资源要求那得多高啊。

遇到高并发场景,不敢想象。对于连接数目比较小的的固定架构倒是可以考虑。



②伪异步 IO 模型

你可能了解过一种通过线程池优化的解决方案,采用线程池和任务队列的方式。这种被称作伪异步 IO 模型。

当有客户端接入时,将客户端的请求封装成一个 task 投递到后端线程池中来处理。线程池维护一个消息队列和多个活跃线程,对消息队列中的任务进行处理。

redis qps瓶颈多少 redis的qps_redis_22


这种解决方案,避免了为每个请求创建一个线程导致的线程资源耗尽问题。但是底层仍然是同步阻塞模型。

如果线程池内的所有线程都阻塞了,那么对于更多请求就无法响应了。因此这种模式会限制最大连接数,并不能从根本上解决问题。

我们继续用上边的餐厅来举例,餐厅老板在经营了一段时间后,顾客多了起来,原本店里的 5 个服务员一对一服务的话根本对付不过来。

于是老板采用 5 个人线程池的方式。服务员服务完一个客人后立刻去服务另一个。

这时问题出现了,有的客人点菜特别慢,服务员就得等待很长时间,直到客人点完为止。

如果 5 个客人都点的特别慢的话,这 5 个服务员就得一直等下去,就会导致其余的顾客没有人服务的状态。这就是我们上边所说的线程池所有线程都被阻塞的情况。

那么这种问题该如何解决呢?别急, Reactor 模式就要出场了。



③Reactor 设计模式(反应器模式)

Reactor 模式的基本设计思想是基于 I/O 复用模型来实现的。

这里说下 I/O 复用模型。和传统 IO 多线程阻塞不同,I/O 复用模型中多个连接共用一个阻塞对象,应用程序只需要在一个阻塞对象等待。

当某个连接有新的数据可以处理时,操作系统通知应用程序,线程从阻塞状态返回,开始进行业务处理。

什么意思呢?餐厅老板也发现了顾客点餐慢的问题,于是他采用了一种大胆的方式,只留了一个服务员。

当客人点餐的时候,这个服务员就去招待别的客人,客人点好餐后直接喊服务员来进行服务。

这里的顾客和服务员可以分别看作多个连接和一个线程。服务员阻塞在一个顾客那里,当有别的顾客点好餐后,她就立刻去服务其他的顾客。

了解了 Reactor 的设计思想后,我们再来看下今天的主角单 Reactor 单线程的实现方案:

redis qps瓶颈多少 redis的qps_Redis_23


Reactor 通过 I/O 复用程序监控客户端请求事件,收到事件后通过任务分派器进行分发。

针对建立连接请求事件,通过 Acceptor 处理,并建立对应的 handler 负责后续业务处理。

针对非连接事件,Reactor 会调用对应的 handler 完成 read→业务处理→write 处理流程,并将结果返回给客户端。

整个过程都在一个线程里完成:

redis qps瓶颈多少 redis的qps_redis_24



单线程时代

了解了 Reactor 模式后,你可能会有一个疑问,这个和我们今天的主题有什么关系呢。可能你不知道的是,Redis 是基于 Reactor 单线程模式来实现的。

IO多路复用程序接收到用户的请求后,全部推送到一个队列里,交给文件分派器。

对于后续的操作,和在 Reactor 单线程实现方案里看到的一样,整个过程都在一个线程里完成,因此 Redis 被称为是单线程的操作。

redis qps瓶颈多少 redis的qps_redis_25

对于单线程的 Redis 来说,基于内存,且命令操作时间复杂度低,因此读写速率是非常快的。



多线程时代

Redis6 版本中引入了多线程。上边已经提到过 Redis 单线程处理有着很快的速度,那为什么还要引入多线程呢?单线程的瓶颈在什么地方?

我们先来看第二个问题,在 Redis 中,单线程的性能瓶颈主要在网络IO操作上。

也就是在读写网络 read/write 系统调用执行期间会占用大部分 CPU 时间。如果你要对一些大的键值对进行删除操作的话,在短时间内是删不完的,那么对于单线程来说就会阻塞后边的操作。

回想下上边讲得 Reactor 模式中单线程的处理方式。针对非连接事件,Reactor 会调用对应的 handler 完成 read→业务处理→write 处理流程,也就是说这一步会造成性能上的瓶颈。

Redis 在设计上采用将网络数据读写和协议解析通过多线程的方式来处理,对于命令执行来说,仍然使用单线程操作。





唯快不破的原理总结

  • 纯内存操作,一般都是简单的存取操作,线程占用的时间很多,时间的花费主要集中在 IO 上,所以读取速度快。
  • 整个 Redis 就是一个全局 哈希表,他的时间复杂度是 O(1),而且为了防止哈希冲突导致链表过长,Redis 会执行 rehash 操作,扩充 哈希桶数量,减少哈希冲突。并且防止一次性 重新映射数据过大导致线程阻塞,采用 渐进式 rehash。巧妙地将一次性拷贝分摊到多次请求过程中,避免阻塞。
  • Redis 使用的是非阻塞 IO:IO 多路复用,使用了单线程来轮询描述符,将数据库的开、关、读、写都转换成了事件,Redis 采用自己实现的事件分离器,效率比较高。
  • 采用单线程模型,保证了每个操作的原子性,也减少了线程的上下文切换和竞争。
  • Redis 全程使用 hash 结构,读取速度快,还有一些特殊的数据结构,对数据存储进行了优化,如压缩表,对短数据进行压缩存储,再如,跳表,使用有序的数据结构加快读取的速度。
  • 根据实际存储的数据类型选择不同编码。

Reactor 模式

  • 传统阻塞 IO 模型客户端与服务端线程 1:1 分配,不利于进行扩展。
  • 伪异步 IO 模型采用线程池方式,但是底层仍然使用同步阻塞方式,限制了最大连接数。
  • Reactor 通过 I/O 复用程序监控客户端请求事件,通过任务分派器进行分发。

单线程时代

  • 基于 Reactor 单线程模式实现,通过 IO 多路复用程序接收到用户的请求后,全部推送到一个队列里,交给文件分派器进行处理。

多线程时代

  • 单线程性能瓶颈主要在网络 IO 上。
  • 将网络数据读写和协议解析通过多线程的方式来处理 ,对于命令执行来说,仍然使用单线程操作。