Redis是如何作到访问速度很快的

           

对于Redis这种内存数据库来讲,除了访问的是内存以外,Redis访问速度飞快还取决于其余的一些因素,而这些都跟Redis的高可用性有很大关系。下面是衡量Redis的三个纬度:
1.高性能:线程模型、网络I/O模型、数据结构,合理的数据编码2.高可用性:主从复制、哨兵模式、Cluster分片集群和持久化机制3.高拓展性:负载均衡
本篇文章,咱们主要来介绍Redis的高性能特性的几个相关因素。  根据官方数据,Redis的QPS能够达到约100000/秒,横轴表示链接数,纵轴表示QPS。

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

参考资料:https://redis.io/topics/benchmarks
因素1: 线程模型   

Redis4.0以前是单线程模型,缘由是由于在此以前CPU不是瓶颈,网络I/O才是瓶颈,而单线程模型一来避免了多线程模型之间的上下文切换,二来又能够经过多路复用来实现并发,而且代码更容易维护。html

不过在Redis4.0以后,redis新增了一些能够被其余线程异步处理的删除操做,例如:UNLINK、FLUSHALL ASYNC 和 FLUSHDB ASYNC。缘由是有一些超大的键值对占用了很大的内容,例如几十 MB 或者几百 MB 的数据,这些数据的删除在几百毫秒内结束不了,若是是同步的就会阻塞待处理的任务,因此加入了多线程,目的就是为了异步处理这些大的数据。
redis


参考资料为:https://zhuanlan.zhihu.com/p/91490643https://stackoverflow.com/questions/10489298/redis-is-single-threaded-then-how-does-it-do-concurrent-i-ohttp://www.odbms.org/2018/03/the-little-known-feature-of-redis-4-0-that-will-speed-up-your-applications/

因素2:网络I/O模型  

由于Redis是单线程模型的缘由,实现并发操做的话,是须要采用了多路复用机制的,例如:epoll,关于epoll的介绍能够参考个人另一篇文章4.同时管理多个socket的高效方法-epoll
算法

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

参考资料:https://draveness.me/redis-io-multiplexing/https://www.jianshu.com/p/8f2fb61097b8https://baijiahao.baidu.com/s?id=1676709704453688282&wfr=spider&for=pc

因素3:数据结构

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

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=


其次,不一样的数据类型使用不一样的数据结构速度才得以提高,而且每种数据类型都有一种或者多种数据结构来支撑。数组

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=


1)  SDS 简单动态字符SDS与C中的字符串区别以下所示:

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

sds与c字符串相比,优点以下:  
1.Redis 将获取字符串长度所需的复杂度从 O(N) 下降到了 O(1)   , 这确保了获取字符串长度的工做不会成为 Redis 的性能瓶颈。2.  与 C 字符串不一样, SDS 的空间分配策略彻底杜绝了发生缓冲区溢出的可能性。3.空间预分配:  SDS 被修改后,程序不只会为 SDS 分配所须要的必须空间,还会分配额外的未使用空间。分配规则以下:若是对 SDS 修改后,len 的长度小于 1M,那么程序将分配和 len 相同长度的未使用空间。举个例子,若是 len=10,从新分配后,buf 的实际长度会变为 10(已使用空间)+10(额外空间)+1(空字符)=21。若是对 SDS 修改后 len 长度大于 1M,那么程序将分配 1M 的未使用空间。4.惰性释放空间:  当对 SDS 进行缩短操做时,程序并不会回收多余的内存空间,而是使用 free 字段将这些字节数量记录下来不释放,后面若是须要 append 操做,则直接使用 free 中未使用的空间,减小了内存的分配。经过惰性空间释放策略, SDS 避免了缩短字符串时所需的内存重分配操做, 并为未来可能有的增加操做提供了优化。5.二进制安全  :  为了确保 Redis 能够适用于各类不一样的使用场景, SDS 的 API 都是二进制安全的(binary-safe):全部 SDS API 都会以处理二进制的方式来处理 SDS 存放在   buf   数组里的数据, 程序不会对其中的数据作任何限制、过滤、或者假设 —— 数据在写入时是什么样的, 它被读取时就是什么样。

这也是咱们将 SDS 的 buf 属性称为字节数组的缘由 —— Redis 不是用这个数组来保存字符, 而是用它来保存一系列二进制数据。安全

好比说, 使用 SDS 来保存以前提到的特殊数据格式就没有任何问题, 由于 SDS 使用 len 属性的值而不是空字符来判断字符串是否结束, 如图 2-18 所示。网络

参考资料:http://redisbook.com/preview/sds/different_between_sds_and_c_string.html

2)  zipList 压缩列表

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

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

Ziplist 是由一系列特殊编码的内存块构成的列表, 一个 ziplist 能够包含多个节点(entry), 每一个节点能够保存一个长度受限的字符数组(不以 \0 结尾的 char 数组)或者整数。并发


字符数组长度小于等于 63 (26−1)字节的字符数组长度小于等于 16383 (214−1) 字节的字符数组长度小于等于 4294967295 (232−1)字节的字符数组整数4 位长,介于 0 至 12 之间的无符号整数1 字节长,有符号整数3 字节长,有符号整数int16_t 类型整数int32_t 类型整数int64_t 类型整数

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

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

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

3)双端列表

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

双端链表仍是 Redis 列表类型的底层实现之一, 当对列表类型的键进行操做 —— 好比执行 RPUSH 、 LPOP 或 LLEN 等命令时, 程序在底层操做的可能就是双端链表。

双端链表及其节点的性能特性以下:

1.节点带有前驱和后继指针,访问前驱节点和后继节点的复杂度为 O(1)O(1) ,而且对链表的迭代能够在从表头到表尾和从表尾到表头两个方向进行;2.链表带有指向表头和表尾的指针,所以对表头和表尾进行处理的复杂度为 O(1)O(1) ;3.链表带有记录节点数量的属性,因此能够在 O(1)O(1) 复杂度内返回链表的节点数量(长度);

除此以外,Redis为双端链表还实现了一个迭代器, 这个迭代器能够从两个方向对双端链表进行迭代:

1.沿着节点的 next 指针前进,从表头向表尾迭代;2.沿着节点的 prev 指针前进,从表尾向表头迭代;

双端链表的实现:

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

参考资料:http://origin.redisbook.com/internal-datastruct/adlist.html

4)skipList 跳跃表


和字典、链表或者字符串这几种在 Redis 中大量使用的数据结构不一样, 跳跃表在 Redis 的惟一做用, 就是实现有序集数据类型。
跳表简介:跳跃表以有序的方式在层次化的链表中保存元素, 效率和平衡树媲美 —— 查找、删除、添加等操做均可以在对数指望时间下完成, 而且比起平衡树来讲, 跳跃表的实现要简单直观得多。

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

从图中能够看到, 跳跃表主要由如下部分构成:
1.表头(head):负责维护跳跃表的节点指针。2.跳跃表节点:保存着元素值,以及多个层。3.层:保存着指向其余元素的指针。高层的指针越过的元素数量大于等于低层的指针,为了提升查找的效率,程序老是从高层先开始访问,而后随着元素值范围的缩小,慢慢下降层次。4.表尾:所有由 NULL 组成,表示跳跃表的末尾。
参考资料:https://redisbook.readthedocs.io/en/latest/internal-datastruct/skiplist.html

5)整数集合(intset)


整数集合(intset)用于有序、无重复地保存多个整数值, 根据元素的值, 自动选择该用什么长度的整数类型来保存元素。
举个例子, 若是在一个 intset 里面, 最长的元素能够用   int16_t 类型来保存, 那么这个 intset 的全部元素都以   int16_t 类型来保存。
另外一方面, 若是有一个新元素要加入到这个 intset , 而且这个元素不能用   int16_t 类型来保存 —— 好比说, 新元素的长度为   int32_t , 那么这个 intset 就会自动进行“升级”:先将集合中现有的全部元素从   int16_t 类型转换为   int32_t 类型, 接着再将新元素加入到集合中。
根据须要, intset 能够自动从   int16_t 升级到   int32_t 或   int64_t , 或者从   int32_t 升级到   int64_t 。

      Intset 只支持升级,不支持降级。

    Intset 是有序的,程序使用二分查找算法来实现查找操做,复杂度为 O(lgN)     。


Intset 是集合键的底层实现之一,若是一个集合是下面的状况,那么 Redis 就会使用 intset 来保存集合元素。

  1. 只保存着整数元素;

  2. 元素的数量很少;

参考资料:https://redisbook.readthedocs.io/en/latest/compress-datastruct/intset.html


因素4:合理的数据编码


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

typedef struct redisObject{    //类型:包含字符串对象、列表对象、哈希对象、集合对象、有序集合对象。   unsigned type:4;   //编码   unsigned encoding:4;   //指向底层数据结构的指针   void *ptr;    //... }robj;

编码介绍:

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

2)List:List 对象的编码能够是 ziplist 或 linkedlist,字符串长度 < 64 字节且元素个数 < 512 使用 ziplist 编码,不然转化为 linkedlist 编码;

备注:这两个条件是能够修改的,在 redis.conf 中:list-max-ziplist-entries 512list-max-ziplist-value 64

3)Hash:Hash 对象的编码能够是 ziplist 或 hashtable。

当 Hash 对象同时知足如下两个条件时,Hash 对象采用 ziplist 编码,不然就是 hashtable 编码。

1.Hash 对象保存的全部键值对的键和值的字符串长度均小于 64 字节。2. Hash 对象保存的键值对数量小于 512 个。

4)Set:Set 对象的编码能够是 intset 或 hashtable,intset 编码的对象使用整数集合做为底层实现,把全部元素都保存在一个整数集合里面。

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

5)Zset:Zset 对象的编码能够是 ziplist 或 zkiplist,当采用 ziplist 编码存储时,每一个集合元素使用两个紧挨在一块儿的压缩列表来存储。

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

当 Zset 对象同时知足一下两个条件时,采用 ziplist 编码,若是不知足以上条件的任意一个,ziplist 就会转化为 zkiplist 编码。

Zset 保存的元素个数小于 128。Zset 元素的成员长度都小于 64 字节。
备注:这两个条件是能够修改的,在 redis.conf 中:zset-max-ziplist-entries 128zset-max-ziplist-value 64
相关文章
相关标签/搜索