图文主要参考小林Coding的图解redis数据结构
除了它是内存数据库,使得所有的操作都在内存上进⾏之外,还有⼀个重要因素,它实现的数据结构,使
得我们对数据进⾏增删查改操作时,Redis 能⾼效的处理。
:::tips
redisDb 结构,表示 Redis 数据库的结构,结构体⾥存放了指向了 dict 结构的指针;
dict 结构,结构体⾥存放了 2 个哈希表,正常情况下都是⽤「哈希表1」,「哈希表2」只有在rehash 的时候才⽤,具体什么是 rehash,我在本⽂的哈希表数据结构会讲;
ditctht 结构,表示哈希表的结构,结构⾥存放了哈希表数组,数组中的每个元素都是指向⼀个哈希表节点结构(dictEntry)的指针;
dictEntry 结构,表示哈希表节点的结构,结构⾥存放了 void * key 和 void * value 指针, *key 指向的是 String 对象,⽽ *value 则可以指向 String 对象,也可以指向集合类型的对象,⽐如 List 对象、Hash 对象、Set 对象和 Zset 对象
:::
并不是指 String(字符串)对象、List(列表)对象、Hash(哈希)对象、Set(集合)对象和 Zset(有序集合)对象,因为这些是 Redis 键值对中值的数据类型,也就是数据的保存形式,这些对象的底层实现的⽅式就⽤到了数据结构。
简单动态字符串(simple dynamic string,SDS)
char * 指针只是指向字符数组的起始位置,⽽字符数组的结尾位置就⽤“\0”表示,意思是指字符串的结束
获取字符串⻓度的函数strlen ,就是通过字符数组中的每⼀个字符,并进⾏计数,等遇到字符为 “\0” 后,就会停⽌遍历
这个限制使得 C 语⾔的字符串只能保存⽂本数据,不能保存像图⽚、⾳频、视频⽂化这样的⼆进制数据
:::tips
1)获取字符串⻓度的时间复杂度为 O(N);
2)字符串的结尾是以 “\0” 字符标识,字符串⾥⾯不能包含有 “\0” 字符,因此不能保存⼆进制数据;
3)字符串操作函数不⾼效且不安全,⽐如有缓冲区溢出的⻛险,有可能会造成程序运⾏终⽌;
:::
:::tips
1)len,记录了字符串⻓度。这样获取字符串⻓度的时候,只需要返回这个成员变量值就⾏,时间复杂度只需要 O(1)
2)alloc,分配给字符数组的空间⻓度。这样在修改字符串的时候,可以通过 alloc - len 计算出剩余的空间⼤⼩,可以⽤来判断空间是否满⾜修改需求,如果不满⾜的话,就会⾃动将 SDS 的空间扩展⾄执⾏修改所需的⼤⼩,然后才执⾏实际的修改操作,所以使⽤ SDS 既不需要⼿动修改 SDS 的空间⼤⼩,也不会出现前⾯所说的缓冲区溢出的问题。
3)flags,⽤来表示不同类型的 SDS。⼀共设计了 5 种类型,分别是 sdshdr5、sdshdr8、sdshdr16、sdshdr32 和 sdshdr64,后⾯在说明区别之处。
4)buf[],字符数组,⽤来保存实际数据。不仅可以保存字符串,也可以保存⼆进制数据。
:::
Redis 的 List 对象的底层实现之⼀就是链表
:::tips
链表优点:
listNode 链表节点的结构⾥带有 prev 和 next 指针,获取某个节点的前置节点或后置节点的时间复杂度只需O(1),⽽且这两个指针都可以指向 NULL,所以链表是⽆环链表;
list 结构因为提供了表头指针 head 和表尾节点 tail,所以获取链表的表头节点和表尾节点的时间复杂度只需O(1);
list 结构因为提供了链表节点数量 len,所以获取链表中的节点数量的时间复杂度只需O(1);
listNode 链表节使⽤ void* 指针保存节点值,并且可以通过 list 结构的 dup、free、match 函数指针为节点设置该节点类型特定的函数,因此链表节点可以保存各种不同类型的值;
链表缺陷:
压缩列表的最⼤特点,就是它被设计成⼀种内存紧凑型的数据结构,占⽤⼀块连续的内存空间,不仅可以利⽤ CPU 缓存,⽽且会针对不同⻓度的数据,进⾏相应编码,这种⽅法可以有效地节省内存开销。
:::tips
压缩列表的缺陷也是有的:
因此,Redis 对象(List 对象、Hash 对象、Zset 对象)包含的元素数量较少,或者元素值不⼤的情况才会使⽤压缩列表作为底层数据结构。
:::
:::tips
压缩列表entry节点包含三部分内容:
:::
:::tips
压缩列表新增某个元素或修改某个元素时,如果空间不不够,压缩列表占⽤的内存空间就需要重新分配。⽽当新插⼊的元素较⼤时,可能会导致后续元素的 prevlen 占⽤空间都发⽣变化,从⽽引起「连锁更新」问题,导致每个元素的空间都要重新分配,造成访问压缩列表性能的下降
:::
:::tips
连锁更新⼀旦发⽣,就会导致压缩列表占⽤的内存空间要多次重新分配,这就会直接影响到压缩列表的访问性能,,压缩列表只会⽤于保存的节点数量不多的场景.
:::
quicklist 是「双向链表 + 压缩列表」组合,因为⼀个 quicklist 就是⼀个链表,⽽链表中的每个元素⼜是⼀个压缩列表
:::tips
压缩列表会有「连锁更新」的⻛险,⼀旦发⽣,会造成性能下降。
quicklist 解决办法,通过控制每个链表节点中的压缩列表的⼤⼩或者元素个数,来规避连锁更新的问题。
因为压缩列表元素越少或越⼩,连锁更新带来的影响就越⼩,从⽽提供了更好的访问性能。
:::
:::tips
listpack 头包含两个属性,分别记录了 listpack 总字节数和元素数量,然后 listpack 末尾也有个结尾标
识。图中的 listpack entry 就是 listpack 的节点了。
主要包含三个⽅⾯内容:
:::tips
哈希表中的每⼀个 key 都是独⼀⽆⼆的,程序可以根据 key 查找到与之关联的 value,或者通过 key 来更新 value,⼜或者根据 key 来删除整个 key-value等等。
在讲压缩列表的时候,提到过 Redis 的 Hash 对象的底层实现之⼀是压缩列表(最新 Redis 代码已将压缩列表替换成 listpack)。Hash 对象的另外⼀个底层实现就是哈希表。
哈希表优点在于,它能以 O(1) 的复杂度快速查询数据,Redis 采⽤了「链式哈希」来解决哈希冲突
:::
:::tips
在正常服务请求阶段,插⼊的数据,都会写⼊到「哈希表 1」,此时的「哈希表 2 」 并没有被分配空间。
随着数据逐步增多,触发了 rehash 操作,这个过程分为三步:
为了避免 rehash 在数据迁移过程中,因拷⻉数据的耗时,影响 Redis 性能的情况,所以 Redis 采⽤了渐
进式 rehash,也就是将数据的迁移的⼯作不再是⼀次性迁移完成,⽽是分多次迁移。
rehash 的触发条件跟负载因⼦(load factor)有关系
整数集合是 Set 对象的底层实现之⼀,Set 对象只包含整数值元素
:::tips
可以看到,保存元素的容器是⼀个 contents 数组,虽然 contents 被声明为 int8_t 类型的数组,但是实际上 contents 数组并不保存任何 int8_t 类型的元素,contents 数组的真正类型取决于 intset 结构体⾥的encoding 属性的值。⽐如:
:::tips
当我们将⼀个新元素加⼊到整数集合⾥⾯,如果新元素的类型(int32_t)⽐整数集合现有所有元素的类型(int16_t)都要⻓时,整数集合需要先进⾏升级,也就是按新元素的类型(int32_t)扩展 contents 数组的空间⼤⼩,然后才能将新元素加⼊到整数集合⾥,当然升级的过程中,也要维持整数集合的有序性。
:::
1)假设有3个类型int16_t的元素
2)现在要加入一个新元素65535,需要使用int32_t来保存
contents数组腰扩容,4*32-3-16=80
:::tips
:::tips
Redis 只有在 Zset 对象的底层实现⽤到了跳表,跳表的优势是能⽀持平O(logN) 复杂度的节点查找
跳表是在链表基础上改进过来的,实现了⼀种「多层」的有序链表
:::tips
图中头节点有 L0~L2 三个头指针,分别指向了不同层级的节点,然后每个层级的节点都通过指针连接起来:
如果我们要在链表中查找节点 4 这个元素,只能从头开始遍历链表,需要查找 4 次,⽽使⽤了跳表后,只需要查找 2 次就能定位到节点 4,因为可以在头节点直接从 L2 层级跳到节点 3,然后再往前遍历找到节点4。
可以看到,这个查找过程就是在多个层级上跳来跳去,最后定位到元素。当数据量很⼤时,跳表的查找复杂度就是 O(logN)。
:::
:::tips
跨度实际上是为了计算这个节点在跳表中的排位
:::
:::tips
查找⼀个跳表节点的过程时,跳表会从头节点的最⾼层开始,逐⼀遍历每⼀层。在遍历某⼀层的跳表节点时,会⽤跳表节点中的 SDS 类型的元素和元素的权重来进⾏判断,共有两个判断条件:
如果上⾯两个条件都不满⾜,或者下⼀个节点为空时,跳表就会使⽤⽬前遍历到的节点的 level 数组⾥的下⼀层指针,然后沿着下⼀层指针继续查找,这就相当于跳到了下⼀层接着查找。
:::
:::tips
如果要查找「元素:abcd,权重:4」的节点,查找的过程是这样的:
:::tips
跳表的相邻两层的节点数量最理想的⽐例是 2:1,查找复杂度可以降低到 O(logN)。
:::
怎么维持?
:::tips
Redis 则采⽤⼀种巧妙的⽅法是,跳表在创建节点的时候,随机⽣成每个节点的层数,并没有严格维持相邻两层的节点数量⽐例为 2 : 1 的情况。
具体的做法是,跳表在创建节点时候,会⽣成范围为[0-1]的⼀个随机数,如果这个随机数⼩于 0.25(相当于概率 25%),那么层数就增加 1 层,然后继续⽣成下⼀个随机数,直到随机数的结果⼤于 0.25 结束,最终确定该节点的层数。
这样的做法,相当于每增加⼀层的概率不超过 25%,层数越⾼,概率越低,层⾼最⼤限制是 64。
:::