3.redis命令(下)

109 阅读6分钟

3.8benchmark 测试工具

3.8.1 简介

在Redis安装完毕后会自动安装一个redis-benchmark测试工具,其是一个压力测试工具,用于测试 Redis 的性能。

通过 redis-benchmark –help 命令可以查看到其用法: image.png 通过例子来学习常用的 options 的用法

3.8.2 测试1
redis-benchmark -h 127.0.0.1 -p 6379 -c 100 -n 100000 -d 8 

以上命令中选项的意义:

  • -h:指定要测试的 Redis 的 IP,若为本机,则可省略
  • -p:指定要测试的 Redis 的 port,若为 6379,则可省略
  • -c:指定模拟有客户端的数量,默认值为 50
  • -n:指定这些客户端发出的请求的总量,默认值为 100000
  • -d:指定测试 get/set 命令时其操作的 value 的数据长度,单位字节,默认值为 3。

以上命令的意义是,使用 100 个客户端连接该 Redis,这些客户端总共会发起 100000 个请求,set/get 的 value 为 8 字节数据。

该命令会逐个测试所有 Redis 命令,每个命令都会给出一份测试报告,每个测试报告由四部分构成:

  • 测试环境报告 image.png
  • 延迟百分比分布:每完成一次剩余测试量的 50%就给出一个统计数据。 image.png
  • 延迟的累积分布:按时间间隔统计的报告:基本是每 0.1 毫秒统计一次。 image.png
  • 总述报告 image.png
3.8.2 测试2
redis-benchmark -t set,lpush,sadd -c 100 -n 100000 -q
  • -t:指定要测试的命令,多个命令使用逗号分隔,不能有空格
  • -q:指定仅给出总述性报告

3.9 简单动态字符串 SDS

3.9.1 SDS 简介

无论是 Redis 的 Key 还是 Value,其基础数据类型都是字符串。Redis 并没有直接使用 C 语言中传统的字符串表示,而是自定义了一种字符串。这种字符串本身的结构比较简单,但功能却非常强大,称为简单动态字符串, Simple Dynamic String。

注意,Redis 中的所有字符串并不都是 SDS,也会出现 C 字符串。

3.9.2 SDS 结构

SDS 不同于 C 字符串。C 字符串本身是一个以双引号括起来,以空字符’\0’结尾的字符序列。但 SDS 是一个结构体,定义在 Redis 安装目录下的 src/sds.h 中:

struct sdshdr { 
    // 字节数组,用于保存字符串 
    char buf[]; 
    // buf[]中已使用字节数量,称为 SDS 的长度 
    int len; 
    // buf[]中尚未使用的字节数量 
    int free; 
}

例如执行 SET country “China”命令时,键 country 与值”China”都是 SDS 类型的,只不过一个是 SDS 的变量,一个是 SDS 的字面常量。”China”在内存中的结构如下: image.png
通过以上结构可以看出,SDS 的 buf 值实际是一个 C 字符串,包含空字符’\0’共占 6 个字节。但 SDS 的 len 是不包含空字符’\0’的。

3.9.3 SDS 的优势

C 字符串使用 Len+1 长度的字符数组来表示实际长度为 Len 的字符串,字符数组最后以空字符’\0’结尾,表示结束。这种结构简单,但不能满足 Redis 对字符串功能性、安全性及高效性等的要求。

(1) 防止”字符串长度获取”性能瓶颈

要获取 C 字符串长度,必须要遍历整个字符串。而 SDS 结构体中直接就存放着字符串的长度数据(len + free)

(2) 保障二进制安全

C 字符串中只能包含符合某种编码格式的字符,例如 ASCII、UTF-8 等,并且除了字符串末尾外,其它位置不能包含空字符’\0’。而图片、音频、视频、压缩文件、office 文件等二进制数据中以空字符’\0’作为分隔符的情况是很常见的。故而在 C 字符串中是不能保存它们。

但 SDS 是通过 len 属性判断字符串是否结束。

(3) 减少内存再分配次数

SDS 采用了空间预分配策略惰性空间释放策略来避免内存再分配问题。 空间预分配策略指每次 SDS 进行空间扩展时,程序不但为其分配所需的空间,还会为其分配额外的未使用空间,以减少内存再分配次数。而额外分配的未使用空间大小取决于空间扩展后 SDS 的 len 属性值。

  • 如果 len 属性值小于 1M,那么分配的未使用空间 free 的大小与 len 属性值相同。
  • 如果 len 属性值大于等于 1M ,那么分配的未使用空间 free 的大小固定是 1M。

惰性空间释放策略:SDS 字符串长度如果缩短,那么多出的未使用空间将暂时不释放,而是增加到 free 中。以使后期扩展 SDS 时减少内存再分配次数。

3.9.4 常用的 SDS 操作函数

image.png image.png

3.10 集合的底层实现原理

Redis 中对于 Set 类型的底层实现,直接采用了 hashTable。但对于 Hash、ZSet、List 集合的底层实现进行了特殊的设计,使其保证了 Redis 的高性能。

3.10.1 两种实现的选择

对于Hash与ZSet集合,其底层的实现实际有两种:压缩列表zipList,与跳跃列表skipList。 这两种实现对于用户来说是透明的,但用户写入不同的数据,系统会自动使用不同的实现。 只有同时满足以配置文件 redis.conf 中相关集合元素数量阈值与元素大小阈值两个条件,使用的就是压缩列表 zipList,只要有一个条件不满足使用的就是跳跃列表 skipList。例如,对于 ZSet 集合中这两个条件如下:

  • 集合元素个数小于 redis.conf 中 zset-max-ziplist-entries 属性的值,其默认值为 128
3.10.2 zipList

image.png (1) 什么是 zipList

zipList 通常称为压缩列表,是一个经过特殊编码的用于存储字符串或整数的双向链表。其底层数据结构由三部分构成:head、entries 与 end。这三部分在内存上是连续存放的。

(2) head

  • zlbytes:存放 zipList 列表整体数据结构所占字节数。
  • zltail:存放 zipList 中最后一个 entry 在整个数据结构中的偏移量(字节)。该数据的存在可以快速定位列表的尾 entry 位置,方便操作。
  • zllen:存放列表包含的 entry 个数。zipList 最多可以有 65535 个 entry。

(3) entries

  • prevlength:记录上一个 entry 的长度,以实现逆序遍历。默认长度为 1 字节, 只要上一个 entry 的长度<254 字节,prevlength 就占 1 字节,否则其会自动扩展为 3 字节。
  • encoding:标志后面的 data 的具体类型。如果 data 为整数类型,长度为 1 字节。如果 data 为字符串类型,则 encoding 长度可能会是 1 字节、2 字节或 5 字节。data 字符串不同的长度,对应着不同的 encoding 长度。
  • data:真正存储的数据。数据类型只能是整数类型或字符串类型。

(4) end

  • zlend:值固定为 255,即二进制位为全 1,表示 一个 zipList 列表的结束。
3.10.3 listPack

ziplist,实现复杂,为了逆序遍历,每个 entry 中包含前一个 entry 的长度,这样导致在 ziplist 中间修改或者插入 entry 时需要进行级联更新。在高并发的写操作场景下会极度降低 Redis 的性能。为了实现更紧凑、更快的解析,更简单的实现,重写实现了 ziplist, 并命名为 listPack。

在 Redis 7.0 中,已经将 zipList 全部替换为了 listPack,但为了兼容性,在配置中也保留了 zipList 的相关属性。 image.png (1) 什么是 listPack

listPack 也是一个经过特殊编码的用于存储字符串或整数的双向链表。其底层数据结构也由 head、entries 与 end构成,在内存上也是连续存放的。

listPack与zipList的重大区别在head与每个entry的结构上,表示列表结束的end与zipList 的相同

(2) head

  • totalBytes:存放 listPack 列表整体数据结构所占的字节数。
  • elemNum:存放列表包含的 entry 个数。

(3) entries

  • encoding:标志后面的 data 的具体类型。如果 data 为整数类型,encoding 长度可能会是 1、2、3、4、5 或 9 字节。不同的字节长度,其标识位不同。如果 data 为字符串类型,则 encoding 长度可能会是 1、2 或 5 字节。data 字符串不同的长度,对应着不同的 encoding 长度。
  • data:真正存储的数据。数据类型只能是整数类型或字符串类型。
  • element-total-len:记录当前 entry 的长度,用于实现逆序遍历。由于其特殊的记录方式,使其本身占有的字节数据可能会是 1、2、3、4 或 5 字节。
3.10.4 skipList

(1) 什么是 skipList

skipList,跳跃列表,简称跳表,是一种随机化的数据结构,基于并联的链表,实现简单,查找效率较高。跳表也是链表的一种,只不过它在链表的基础上增加了跳跃功能。 也正是这个跳跃功能,使得在查找元素时,能够提供较高的效率。

(2) skipList 原理

假设有一个带头尾结点的有序链表。

image.png

在该链表中,如果要查找某个数据,需要从头开始逐个进行比较。同样,当我们要插入新数据·时,也要经历同样的查找过程,从而确定插入 位置。

为了提升查找效率,在偶数结点上增加一个指针,让其指向下一个偶数结点。 image.png 这样所有偶数结点就连成了一个新的链表(简称高层链表)。此时再想查找某个数据时,先沿着高层链表进行查找。当遇到第一个比待查数据大的节点时,立即从该大节点的前一个节点回到原链表中进行查找。该方式可以减少比较次数,提高查找效率。如果链表元素较多,为了进一步提升查找效率,可以将原链表构建为三层链表,或再高层级链表。 image.png

(3) 存在的问题

对链表分层级的方式从原理上看确实提升了查找效率,但在实际操作时就出现了问题:由于固定序号的元素拥有固定层级,所以列表元素出现增加或删除的情况下,会导致列表整体元素层级大调整,这样势必会大大降低系统性能。

(4) 算法优化

为了避免前面的问题,skipList 采用了随机分配层级方式。即在确定了总层级后,每添加一个新的元素时会自动为其随机分配一个层级。这种随机性就解决了节点序号与层级间的固定关系问题。 image.png
从这个 skiplist 的创建和插入过程可以看出,每一个节点的层级数都是随机分配的,而且新插入一个节点不会影响到其它节点的层级数。只需要修改插入节点前后的指针。降低了插入操作的复杂度。

skipList 指的就是除了最下面第 1 层链表之外,它会产生若干层稀疏的链表,这些链表里面的指针跳过了一些节点,并且越高层级的链表跳过的节点越多。在查找数据的时先在高层级链表中进行查找,然后逐层降低来精确地确定数据位置。 在这个过程中由于跳过了一些节点,从而加快了查找速度。

3.10.5 quickList

image.png

(1) 什么是 quickList

quickList,快速列表,本身是一个双向无循环链表,每一个节点都是一个 zipList。从Redis3.2开始,对于List的底层实现,使用quickList替代了zipList 和 linkedList。

zipList 与 linkedList 都存在有明显不足,而 quickList 则对它们进行了改进。

quickList 本质上是 zipList 和 linkedList 的混合体。其将 linkedList 按段切分,每一段使用 zipList 来紧凑存储若干真正的数据元素,多个 zipList 之间使用双向指针串接起来。对于每个 zipList 中最多可存放多大容量的数据元素,在配置文件中通过 list-max-ziplist-size 属性可以指定。

(2) 检索操作

对于 List 元素的检索,都是以其索引 index 为依据的。quickList 由一个个的 zipList 构成, 每个 zipList 的 zllen 中记录当前 zipList 中包含的 entry 的个数。根据要检索元素的 index,从 quickList 的头节点开始,逐个对 zipList 的 zllen 做 sum 求和,直到找到第一个求和后 sum 大于 index 的 zipList,那么要检索的这个元素就在这个 zipList 中。

(3) 插入操作 由于 zipList 有大小限制,所以在 quickList 中插入一个元素在逻辑上相对比较复杂。假设要插入的元素的大小为 insertBytes,而查找到的插入位置所在的 zipList 当前的大小为 zlBytes,那么具体可分为下面几种情况:

  • 情况一:当 insertBytes + zlBytes <= list-max-ziplist-size 时,直接插入到 zipList 中相应位置
  • 情况二:当 insertBytes + zlBytes > list-max-ziplist-size,且插入的位置位于该 zipList 的首 部位置,此时需要查看该 zipList 的前一个 zipList 的大小 prev_zlBytes。
    • 若 insertBytes + prev_zlBytes<= list-max-ziplist-size 时,将元素插入到前一个 zipList 的尾部位置
    • 若大于,直接将元素自己构建为一个新的 zipList,并连入 quickList
  • 情况三:当 insertBytes + zlBytes > list-max-ziplist-size,且插入的位置位于该 zipList 的尾 部位置,此时需要查看该 zipList 的后一个 zipList 的大小 next_zlBytes(和情况二类似)
  • 情况四:当 insertBytes + zlBytes > list-max-ziplist-size,且插入的位置位于该 zipList 的中间位置,则将当前 zipList 分割为两个 zipList 连接入 quickList 中,然后将元素插入到分割后的前面 zipList 的尾部位置

(4) 删除操作

在相应的 zipList 中删除元素后,若该 zipList 中没有元素,则删除,将其前后两个 zipList 相连接。

3.10.6 key 与 value 中元素的数量
  • Redis 最多可以处理 2的32次方个 key(约 42 亿),并且在实践中经过测试,每个 Redis 实例至少可以处理 2.5 亿个 key。
  • 每个 Hash、List、Set、ZSet 集合都可以包含 2的32次方个元素。