3.8 Benchmark
Redis安装完毕后会自动安装一个redis-benchmark测试工具,其是一个压力测试工具,用于测试 Redis 的性能。 src目录下可找到该工具
通过 redis-benchmark –help 命令可以查看到其用法
3.8.1 测试1
3.9 简单动态字符串SDS
无论是 Redis 的 Key 还是 Value,其基础数据类型都是字符串。
例如,Hash 型 Value 的field 与 value 的类型、List 型、Set 型、ZSet 型 Value 的元素的类型等都是字符串。
虽然 Redis是使用标准 C 语言开发的,但并没有直接使用 C 语言中传统的字符串表示,而是自定义了一种字符串。这种字符串本身的结构比较简单,但功能却非常强大,称为简单动态字符串, Simple Dynamic String,简称 SDS。
注意,Redis 中的所有字符串并不都是 SDS,也会出现 C 字符串。C 字符串只会出现在字符串“字面常量”中,该字符串不可能发生变更。
例如: edisLog(REDIS_WARNNING, “sdfsfsafsafds”);
3.9.1 SDS的结构
SDS 不同于 C 字符串。C 字符串本身是一个以双引号括起来,以空字符’\0’结尾的字符序列。但 SDS 是一个结构体,定义在 Redis 安装目录下的 src/sds.h 中:
struct sdshdr{
// 记录buf 数组中已使用字节的数量
// 等于SDS 所保存字符串的长度
int len;
// 记录buf 数组中未使用字节的数量
int free;
// 字节数组,用于保存字符串
char buf[];
}
例如执行 SET country “China”命令时,键 country 与值”China”都是 SDS 类型的,只不过coutry是 SDS 的变量,china是 SDS 的字面常量。”China”在内存中的结构如下(SDS会预留一部分free空间):
3.9.2 SDS的优势
- SDS 结构中维护了字符串的长度信息,这意味着获取字符串长度是一个 O(1) 的操作,而不需要像 C 字符串那样通过遍历整个字符串来确定长度,大大提高效率。
- 尽管SDS结构的字符串仍然以 ‘\0’ 结尾, 但其依赖于长度确定字符串结束的位置, 因此其可以存储任何二进制数据,包括 ‘\0’, 包括图片音频. 作为对比, C 字符串依赖于空字符’\0’作为终止符。
- 有效减少内存分配次数
3.9.3 SDS减少内存分配次数的方法
- 空间预分配策略
每次 SDS 进行空间扩展时,程序不但为其分配所需的空间,还会为其分配额外的未使用空间,以减少内存再分配次数。而额外分配的未使用空间大小取决于空间扩展后 SDS 的 len 属性值。
- 如果 len 属性值小于 1M,那么分配的未使用空间 free 的大小与 len 属性值相同。
- 如果 len 属性值大于等于 1M ,那么分配的未使用空间 free 的大小固定是 1M。
- 惰性空间释放策略。
SDS 字符串长度如果缩短,那么多出的未使用空间将暂时不释放,而是增加到 free 中。以使后期扩展 SDS 时减少内存再分配次数。
当然也可以使用 sdsRemoveFreeSpace()函数来强制释放未使用空间。
3.10 集合的底层实现
之前我们学习了 hash, Set ,ZSet , List 四种集合(集合是一种泛指, 他们都可以叫集合)
- 对于Set 类型的底层实现,直接采用了 hashTable。
- 对于 Hash、ZSet、List 集合的底层实现进行了特殊的设计,使其保证了 Redis 的高性能。
3.10.1 Hash与 ZSet 集合的底层实现
对于Hash与 ZSet 集合,其底层的实现实际有两种:压缩列表 zipList,与跳跃列表skipList。
这两种实现对于用户来说是透明的,但用户写入不同的数据,系统会自动使用不同的实现。
当且仅当同时满足以配置文件 redis.conf 中相关集合元素数量阈值与元素大小阈值两个条件时,使用的就是压缩列表 zipList,只要有一个条件不满足使用的就是跳跃列表 skipList。
例如,对于Hash 集合中这两个条件如下:
- 集合元素个数小于 redis.conf 中 hash-max-ziplist-entries 属性的值,其默认值为 512字节
- 每个集合元素大小都小于 redis.conf 中 hash-max-ziplist-value 属性的值,其默认值为 64字节
例如,对于ZSet 集合中这两个条件如下:
- 集合元素个数小于 redis.conf 中 zset-max-ziplist-entries 属性的值,其默认值为 128
- 每个集合元素大小都小于 redis.conf 中 zset-max-ziplist-value 属性的值,其默认值为 64字节
3.10.1.1 zipList
zipList,通常称为压缩列表,是一个经过特殊编码的用于存储字符串或整数的双向链表.
其底层数据结构由三部分构成:head、entries 与 end。这三部分在内存上是连续存放的。
- head
- zlbytes:占 4 个字节,整个zipList所占的字节数.
- zltail:占 4 个字节,用于存放 zipList 中最后一个 entry 在整个数据结构中的偏移量(字节)。以便快速定位列表的尾 entry 位置。
- zllen:占 2 字节,用于存放列表包含的 entry 个数。由于其只有 16 位,所以 zipList 最多可以含有的 entry 个数为 216-1 = 65535 个。
- entries
entries 是真正的列表,由很多的列表元素 entry 构成。由于不同的元素类型、数值的不同,从而导致每个 entry 的长度不同。
每个entry由三部分构成
- prevlength:该部分用于记录上一个 entry 的长度,以实现逆序遍历。默认长度为 1 字节,只要上一个 entry 的长度<254 字节,prevlength 就占 1 字节,否则其会自动扩展为 3 字节长度。
- encoding:该部分用于标志后面的 data 的具体类型。如果 data 为整数类型,encoding固定长度为 1 字节。如果 data 为字符串类型,则 根据字符串的长度, encoding 长度可能会是 1 字节、2 字节或 5 字节。
- data:真正存储的数据。数据类型只能是整数类型或字符串类型。不同的数据占用的字节长度不同。
- end
end 只包含一部分,称为 zlend。占 1 个字节,值固定为 255,即二进制位为全 1,表示一个 zipList 列表的结束。
3.10.1.2 listPack(Redis7升级zipList)
ziplist的实现比较复杂.
为了能够逆序遍历,每个 entry 中包含前一个 entry 的长度,这样会导致在 ziplist 中间修改或者插入 entry 时需要进行级联更新。在高并发的写操作场景下会极度降低 Redis 的性能。
在 Redis 7.0 中,重写 zipList 后改名为 listPack,但为了兼容性,在配置中也保留了 zipList 的相关属性。
相同点:
- listPack 也是一个经过特殊编码的用于存储字符串或整数的双向链表。
- 其底层数据结构也由三部分构成:head、entries 与 end,且这三部分在内存上也是连续存放的。
- listPack中表示列表结束的end与 zipList中表示列表结束的 zlend 是相同的,占一个字节,且 8 位全为 1。
不同点:
- head中去掉了在zipList中记录最后一个 entry 偏移量的 zltail。
- 每个entry中去掉了在zipList中记录前一个 entry 长度的 prevlength, 而增加了记录当前entry 长度的 element-total-len。这个改变过后仍然可以实现逆序遍历,但却避免了由于在列表中间修改或插入 entry 时引发的级联更新。
3.10.1.3 skipList
skipList,跳跃列表,简称跳表,是一种随机化的数据结构,基于并联的链表,实现简单,查找效率较高。简单来说跳表也是链表的一种,只不过它在链表的基础上增加了跳跃功能。也正是这个跳跃功能,使得在查找元素时,能够提供较高的效率。
3.10.1.3.1 高层链表的原理
假设有一个 带头结点的有序链表
高层链表存在的问题:
任意插入了新的节点,或删除的原来的某些节点时,只要不是插入到表尾/删除表尾, 那么插入/删除势必会影响到高层指针的指向, 所有被链式影响到的高层指针都要重新安排, 大大影响系统性能.
3.10.1.3.1 skipList对高层链表的优化
3.10.2 List集合的底层实现
quickList,快速列表,quickList 本身是一个双向无循环链表,它的每一个节点都是一个zipList。从 Redis3.2版本开始,对于 List的底层实现,使用 quickList替代了 zipList 和 linkedList。
quickList 本质上是 zipList 和 linkedList 的混合体。其将 linkedList 按段切分,每一段使用 zipList 来紧凑存储若干真正的数据元素,多个 zipList 之间使用双向指针串接起来。当然,对于每个 zipList 中最多可存放多大容量的数据元素,在配置文件中通过 list-max-ziplist-size 属性可以指定。
3.10.2.1 对quickList查找
根据索引index对 List 元素进行查找都。
quickList 由一个个的 zipList 构成,每个 zipList 的 zllen 中记录的就是当前 zipList 中包含的 entry 的个数,即包含的真正数据元素的个数。根据要检索元素的 index,从 quickList 的头节点开始,逐个对 zipList 的 zllen 做 sum求和,直到找到第一个求和后 sum 大于 index 的 zipList,那么要检索的这个元素就在这个zipList 中。
3.10.2.2 对quickList插入
由于 zipList 是有大小限制的(list-max-ziplist-size属性),所以根据插入的元素的大小 insertBytes,与 , 查找到的插入位置所在的 zipList 当前的大小 zlBytes , 插入的操作不同.
最简单的情况下 , 当 insertBytes + zlBytes <= list-max-ziplist-size 时,直接插入到 zipList 中相应位置即可
3.10.2.3 对quickList删除
对于删除操作,只需要注意一点,在相应的 zipList 中删除元素后,该 zipList 中是否还有元素。如果没有其它元素了,则将该 zipList 删除,将其前后两个 zipList 相连接。