Redis为什么那么快?


前言

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

基准测试

横轴为连接数,纵轴为QPS。


一、Redis为什么那么快?

Redis为什么那么快?

Redis之所以那么快,有以下五点原因:

  • 基于内存实现
  • 使用I/O多路复用模型
  • 采用单线程模型
  • 高效的数据结构
  • 合理的数据编码

下面将通过对以上五点原因的分析来说明 “Redis为什么那么快?”。


二、原因之一:基于内存实现

内存由CPU控制,也就是CPU内部集成的内存控制器,享受与CPU通信的最优带宽。Redis 将数据存储在内存中,读写操作不会受到磁盘的 IO 速度限制,所以Redis的读写速度会非常的快。


三、原因之二:使用I/O多路复用模型

传统阻塞I/O(BIO)

传统阻塞IO

传统阻塞 IO ,在执行accept 、recv 等网络操作时,如遇到异常情况会一直处于阻塞状态。

同步非阻塞IO(NIO)

同步非阻塞IO

channel(管道)、buffer(缓冲区)和selector(多路复用器)为NIO的核心组件,channel就像是一个火车,用来运输buffer,而buffer中保存了所有的数据,每个channel携带着buffer注册到selector上,selector会对各个管道进行轮询,若有IO事件发生,内核就开启一个对应事件的线程去处理。

相比之下,NIO的效率明显高于BIO,而NIO是I/O多路复用模型的基础。

IO多路复用模型

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

IO多路复用模型

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

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

Redis 线程不会阻塞在某一个特定的客户端请求处理上。正因为此,Redis可以同时和多个客户端连接并处理请求,从而提升了并发性。


四、原因之三:采用单线程模型

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

多线程的弊端

  • 上下文切换消耗资源
    何为上下文切换?
    一个线程被剥夺CPU的使用权而被暂停运行叫做“切出”。
    一个线程被选中占用CPU开始或继续运行叫做“切入”。
    在“切入”、“切出”的过程中,CPU需要保存和恢复相应的进度信息,这个进度信息叫做“上下文”。
    一个工作的线程被另一个线程暂停,而这个线程占用CPU开始执行任务的过程叫做“上下文切换”。
    多线程中,上下文切换相对频繁,也就意味着CPU需要不停地“切入”、“切出”以及不停地保存和恢复相应的进度信息,完成这一系列的操作是非常消耗资源的。
  • 额外的性能开销
    当多线程并行修改共享数据的时候,为了保证数据正确,需要加锁机制,这就会带来额外的性能开销。
  • 增加代码复杂度和调试难度

单线程的优势

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

单线程是否没有充分利用 CPU 资源呢?
因为 Redis 是基于内存的操作,CPU 不是 Redis 的瓶颈,Redis 的瓶颈最有可能是机器内存的大小或者网络带宽


五、原因之四:高效的数据结构

为了追求速度,不同数据类型使用不同的数据结构速度才得以提升。每种数据类型都有一种或者多种数据结构来支撑,Redis数据类型和底层数据结构的关系如下图所示。

Redis数据类型和底层数据结构的关系

HashTable 哈希表

全局哈希表

哈希表本质上是一个数组,每一个元素叫做哈希桶,每个桶中的entry保存着键值指针(*key 和 *value)。
Redis其实就是一个全局哈希表,哈希表的时间复杂度是 O(1),通过key的哈希值和哈希函数,就可以定位对应哈希桶的位置,从而确定桶中entry,找到对应数据。

全局哈希表

哈希冲突

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

链式哈希

Redis采用链式哈希的方式解决冲突,即同一个桶里面的元素使用链表保存。

链式哈希

随着数据量的增加,哈希冲突越来越多,链表也随之越来越长,进而导致查找性能变差。因此,Redis使用了两个全局哈希表,通过rehash操作,增加现有的哈希桶数量,分散单桶元素数量,从而在减少哈希冲突的同时缩短链表长度,提高Redis的查询效率。

rehash的执行过程

  1. 给哈希表 2 分配更大的空间
  2. 将 hash 表 1 的数据重新映射拷贝到 hash 表 2 中
  3. 释放 hash 表 1 的空间

rehash触发条件

扩容时 rehash:当以下条件中的任意一个被满足时, 程序会自动开始对哈希表执行扩容 rehash 操作

  • 服务器目前没有在执行 BGSAVE 命令或者 BGREWRITEAOF 命令,并且哈希表的负载因子大于等于 1;
  • 服务器目前正在执行 BGSAVE 命令或者 BGREWRITEAOF 命令,并且哈希表的负载因子大于等于 5。

收缩时rehash:当哈希表的负载因子小于 0.1 时,程序自动开始对哈希表执行收缩操作

渐进式rehash

因为 Redis 是单线程,当数据量过大时,会造成严重阻塞,所以采用的是渐进式 rehash。所谓渐进式rehash,就是每处理一个请求时,从哈希表 1 中依次将索引位置上的所有 entry 拷贝到哈希表 2 中,将rehash 分散到多次请求过程中,避免耗时阻塞。

渐进式rehash

SDS 简单动态字符串

SDS是String的底层实现结构。

SDS 简单动态字符串

Redis 中定义动态字符串的结构

struct sdshdr {       
    int len; // buf中已使用空间的长度      
    int free; // buf中剩余可用空间的长度    
    char buf[]; // 存储的实际内容 
};

SDS的特性

  • 低时间复杂度
    SDS的len保存了已使用空间的长度,获取字符串长度的时间复杂度为O(1)
  • 空间预分配
    SDS被修改后,会被分配所需要的必须空间以及额外的未使用空间。
    分配规则:如果SDS被修改后,len的长度小于1M,那么SDS将被分配和len相同长度的未使用空间。
    举个例子,如果 len=10,重新分配后,buf的实际长度会变为10(已使用空间)+10(额外空间)+1(空字符)=21。如果SDS被修改后,len长度大于1M,那么SDS将分配1M的未使用空间。

拼接实例:

原字符串

在这里插入图片描述

调用拼接函数拼接字符串"JIYI",Redis根据以上规则分配13byte的free。

在这里插入图片描述

继续拼接字符串"LOVE"

在这里插入图片描述

  • 惰性空间释放
    在执行完一个字符串缩短的操作后,Redis并不会马上回收free的空间,是为了预防后续拼接的操作,这样可以减少重新分配空间带来的消耗,但是再次操作之后还是没用到free的空间,Redis就会收回free的空间,防止内存的浪费。
  • 二进制安全
    以空字符也就是’\0’举例,C语言是根据’\0’去判断一个字符串的长度,但是有些数据中会有’\0’穿插在中间,比如图片,音频,视频,压缩文件的二进制数据。以下图为例,C语言只能识别第一个’\0’前面的部分字符"TINGQIU"。

在这里插入图片描述

ZipList 压缩列表

ZipList是List 、hash、sorted Set三种数据类型底层实现之一。

ZipList的特性

  • ZipList是由一系列特殊编码的连续内存块组成的顺序型的数据结构,不容易产生内存碎片,内存利用率高。
  • 适用情景:一个列表只有少量数据,并且每个列表项是小整数或短字符串
    ZipList的结构。
  • 查找第一个元素和最后一个元素,可以通过表头三个字段的长度直接定位,时间复杂度是O(1)。而查找其他元素时,只能逐个查找,此时的时间复杂度是O(N)。

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

ZipList的结构

struct ziplist<T> {
    int32 zlbytes; // 整个压缩列表占用字节数
    int32 zltail_offset; // 最后一个元素距离压缩列表起始位置的偏移量,用于快速定位到最后一个节点
    int16 zllength; // 元素个数
    T[] entries; // 元素内容列表,挨个挨个紧凑存储
    int8 zlend; // 标志压缩列表的结束,值恒为0xFF
}

ZipList 压缩列表

ZipList的缺点

插入和删除操作需要频繁的申请和释放内存,同时会发生内存拷贝,数据量大时内存拷贝开销较大。

LinkedList 双向链表

LinkedList是List的底层实现结构之一。

LinkedList的特性

  • 双端
    带有prev和next指针,定义前后节点的时间复杂度为O(1)。

双端

  • 无环
    表头节点的prev指针和表尾节点的next指针都指向NULL,对链表的访问以NULL 为终点。

无环

  • 表头指针和表尾指针
    通过 list 结构的 head 指针和 tail 指针,获取链表的表头节点和表尾节点的时间复杂度为O(1)。
  • 链表长度计数器
    使用list结构的len属性来对list持有的链表节点进行计数,获取链表中节点数量的时间复杂度为O(1)。
  • 多态
    链表节点使用void*指针来保存节点值,并且可以通过 list 结构的dup、free、match 三个属性为节点值设置类型特定函数,所以链表可以用于保存各种不同类型的值。
typedef struct list {
    listNode *head; // 头结点
    listNode *tail; // 尾结点
    void *(*dup)(void *ptr); // 节点复制函数
    void (*free)(void *ptr); // 节点释放函数
    int (*match)(void *ptr, void *key); // 节点对比函数
    unsigned long len;  // 链表所包含的节点数量
} list;

LinkedList的缺点

除保存数据外还需要保存prev、next两个指针,内存利用率低,LinkedList的各个节点是单独的内存块,地址不连续,节点多了容易产生内存碎片。

QuickList

在Redis 3.2版本中,新增了QuickList数据结构,用于替代ZipList和LinkedList。
官网对QuickList的解释为"A doubly linked list of ziplists",意为由ZipList组成的LinkedList,结合了ZipList和LinkedList的优点。

QuickList结构

typedef struct quicklist {
    quicklistNode *head;        /* 头结点 */
    quicklistNode *tail;        /* 尾结点 */
    unsigned long count;        /* 在所有的ziplist中的entry总数 */
    unsigned long len;          /* quicklist节点总数 */
    int fill : QL_FILL_BITS;              /* 16位,每个节点的最大容量 */
    unsigned int compress : QL_COMP_BITS; /* 16位,quicklist的压缩深度,0表示所有节点都不压缩,否则就表示从两端开始有多少个节点不压缩 */
    unsigned int bookmark_count: QL_BM_BITS;  /*4位,bookmarks数组的大小,bookmarks是一个可选字段,用来quicklist重新分配内存空间时使用,不使用时不占用空间*/
    quicklistBookmark bookmarks[];
} quicklist;

QuickList节点结构

typedef struct quicklistNode {
    struct quicklistNode *prev;
    struct quicklistNode *next;
    unsigned char *zl;           /* quicklist节点对应的ziplist */
    unsigned int sz;             /* ziplist的字节数 */
    unsigned int count : 16;     /* ziplist的item数*/
    unsigned int encoding : 2;   /* 数据类型,RAW==1 or LZF==2 */
    unsigned int container : 2;  /* NONE==1 or ZIPLIST==2 */
    unsigned int recompress : 1; /* 这个节点以前压缩过吗? */
    unsigned int attempted_compress : 1; /* node can't compress; too small */
    unsigned int extra : 10; /* 未使用到的10位 */
} quicklistNode;

QuickList 快表

SkipList 跳表

SkipList是Sorted Set的底层实现结构之一。

SkipList的特性

  • SkipList是一种有序数据结构,它通过在每个节点中存储着多个指向其他节点的指针,从而达到快速访问节点的目的。
  • SkipList支持平均O(logN)、最坏O(N)时间复杂度的节点查找,还可以通过顺序性操作来批量处理节点。
  • SkipList在LinkedList的基础上,增加了多层级索引,通过索引位置的几个跳转,实现数据的快速定位。

SkipList 跳表

IntSet 整数数组

IntSet是Set的底层实现结构之一。

Intset的特性

  • 适用情景:一个集合只包含整数值元素,并且这个集合的元素数量不多。
  • Inset的每个元素都是contents数组的一个数组项(item),各个项在数组中按值的大小从小到大有序地排列,并且数组中不包含任何重复项。
typedef struct intset{
     //编码方式
     uint32_t encoding;
     //集合包含的元素数量
     uint32_t length;
     //保存元素的数组
     int8_t contents[];
}intset;

六、原因之五:合理的数据编码

Redis使用对象(RedisObject)来表示键值对,在创建Redis键值对时,将创建两个对象,一个对象是用做键值对的键对象,另一个是键值对的值对象。
例如:创建键值对“set tingqiu jiyi"时,键值对的键对象是一个包含了字符串“tingqiu“的对象,键值对的值对象是包含字符串"jiyi"的对象。

typedef struct redisObject{
    //类型
   unsigned type:4;
   //编码
   unsigned encoding:4;
   //指向底层数据结构的指针
   void *ptr;
    //...
 }robj;

由上文可知,不同的数据类型,其底层支持可能有多种数据结构,在不同的时候选择不同的底层数据结构,就涉及到编码转化的问题。

String

底层结构为SDS,若是数字,采用int类型的编码;若是字符,采用raw编码。

List

List 对象的编码包括ZipListLinkedList(Redis 3.2后改由QuickList实现),若字符串长度 < 64 字节且元素个数 < 512 (默认)则使用ZipList编码,否则使用LinkedList 编码。
上述选择ZipList编码的条件为Redis默认值,具体数值可在redis.conf文件中修改。

list-max-ziplist-entries 512
list-max-ziplist-value 64

Hash

Hash对象的编码包括ZipList(Redis 6.0后被ListPack取代)和HashTable,与List类似,若键/值字符串长度 < 64 字节且元素个数 < 512 (默认)则使用 ZipList编码,否则使用HashTable编码。

Soeted Set

Soeted Set对象的编码包括ZipList(Redis 6.0后被listpack取代)和SkipList,与List类似,若字符串长度 < 64 字节且元素个数 < 512 (默认)则使用ZipList编码,否则使用HashTable编码。

Set

Set对象的编码包括IntSetHashTable,若元素为整数且元素个数小于一定范围使用IntSet编码,否则使用 hashtable 编码。

  • 6
    点赞
  • 56
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

久违の欢喜

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

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

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

打赏作者

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

抵扣说明:

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

余额充值