Redis学习笔记1(SDS,链表,字典)

Redis

简单动态字符串

Redis没有直接使用C语言传统字符串(以空字符结尾的字符数组)表示,而是自己构建了一种名为简单动态字符串(SDS)的抽象类型,并将SDS用作Redis的默认字符串表示

在Redis里面,C字符串只会作为字符串自变量,用在一些无须对字符串值进行修改的地方,比如打印日志

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CJGuRwlz-1637423600117)(C:\Users\VV\AppData\Roaming\Typora\typora-user-images\image-20211119202357287.png)]

当redis需要的不仅仅是一个字符串字面量,而是一个可以被修改的字符串值得时候,Redis就会使用SDS来表示字符串的值,比如在Redis的数据库里面,包含字符串值的键值对在底层都是由SDS实现的。

例如:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZuaKHMhh-1637423600120)(C:\Users\VV\AppData\Roaming\Typora\typora-user-images\image-20211119202831428.png)]

那么Redis将在数据库中创建一个新的键值对,其中:

  • 键值对的键是一个字符串对象,对象的底层实现是一个保存着字符串msg的SDS。
  • 键值对的值也是一个字符串对象,对象的底层是一个保存着字符串hello world的SDS

如果客户端执行命令:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-H1UO2ZCA-1637423600122)(C:\Users\VV\AppData\Roaming\Typora\typora-user-images\image-20211119203023913.png)]

那么Redis将在数据库中创建一个新的键值对,其中:

  • 键值对的键是一个字符串对象,对象的底层实现了一个保存着字符串fruits的SDS
  • 键值对的值是一个列表对象,列表对象包含了三个字符串对象,这三个字符串对象分别由三个SDS实现:第一个SDS保存着字符串apple,第一个SDS保存着字符串banana,第一个SDS保存着字符串cherry

除了用来保存数据库中的字符串值之外,SDS还被用作缓冲区:AOF模块中的AOF缓冲区,以及客户端状态中的输入缓冲区,都是由SDS实现的

SDS的定义

SDS实现:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LBjAjk4b-1637423600123)(C:\Users\VV\AppData\Roaming\Typora\typora-user-images\image-20211119203559100.png)]

如图2-1

  • free属性值为0,表示这个SDS没有分配任何未使用空间
  • len属性值为5,表示这个SDS保存了一个五字节长的字符串
  • buf属性是一个char类型的数组,数组保存了Redis五个字符,而最后一个字节则保存了空字符\0

SDS遵循C字符串以空字符结尾的惯例,保存空字符的1字节空间不计算在SDS的len属性里面,并且为空字符分配额外的1字节空间,以及添加空字符到末尾等操作,都是由SDS函数自动完成的,所以这个空字符对于SDS的使用者来说是完全透明的。

遵循空字符结尾这一惯例的好处是,SDS可以直接重用一部分C字符串函数库里面的函数。

例如:printf("%s",s->buf);可以打印SDS保存的字符串值,而无须为SDS编写专门的打印函数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QnWQPgJi-1637423600125)(C:\Users\VV\AppData\Roaming\Typora\typora-user-images\image-20211119204316988.png)]

图2-2展示了另一个SDS示例。这个SDS和之前展示的SDS一样,都保存了字符串值Redis。这个和之前区别在于,这个SDS为buf数组分配了五字节未使用空间,所以它的free属性的值为5

SDS与C字符串的区别

C语言使用长度为N+1的字符数组来表示长度为N的字符串,而且字符数组的最后一个元素总是空字符\0

C语言使用的这种简单的字符串表示方式,并不能满足Redis对字符串在安全性、效率以及功能方面的要求

常数复杂度获取字符串长度

因为C字符串并不记录自身长度信息,所以为了获取一个C字符串长度,程序必须遍历整个字符串,对遇到的每个字符进行计数,直到遇到代表字符串结尾的空字符位置,这个操作的时间复杂度为O(n)。

而SDS在len属性中记录了长度,所以可以通过O(1)的复杂度来获取长度,设置和更新SDS长度的工作是由SDS的API在执行时自动完成的,使用SDS无需进行任何手动修改长度的工作

通过使用SDS而不是C字符串,Redis将获取字符串的长度的复杂度降低,确保了获取字符串长度的工作不会造成Redis的性能瓶颈,例如,因为字符串键在底层使用SDS实现,所以即使我们对一个非常长的字符串键反复执行strlen命令,也不会对系统性能造成任何影响。因为strlen命令的复杂度仅为O(1)

杜绝缓冲区溢出

除了获取字符串长度复杂度高之外,C字符串不记录自身长度带来的另一个问题是容易造成缓冲区溢出。举个例子strcat函数可以将src字符串中的内容拼接到dest字符串的末尾

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-D3vaFnoW-1637423600127)(C:\Users\VV\AppData\Roaming\Typora\typora-user-images\image-20211120085336294.png)]

因为C字符串本身不记录自身的长度,所以strcat假定用户在执行这个函数时,已经为dest分配了足够多的内存,可以容纳src字符串的所有内容,而这个假定一旦不成立,就会产生缓冲区溢出。

举个例子,假设程序里由两个内存中紧邻着的C字符串s1和s2,其中s1保存了字符串Redis,而s2则保存了字符串MongoDB

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5HMcRfeL-1637423600128)(C:\Users\VV\AppData\Roaming\Typora\typora-user-images\image-20211120085606598.png)]

如果一个程序员决定通过执行:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-25paUW3s-1637423600129)(C:\Users\VV\AppData\Roaming\Typora\typora-user-images\image-20211120085644598.png)]

将s1的内容修改为Redis Cluster,但粗心的他却忘了在执行strcat之前为s1分配足够的空间,那么在strcat函数执行之后,s1的数据将移除到s2所在的空间中,导致s2保存的内容被意外的修改。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-97sYeJNr-1637423600130)(C:\Users\VV\AppData\Roaming\Typora\typora-user-images\image-20211120085805886.png)]

与C字符串不同,SDS的空间分配策略完全杜绝了发生缓冲区溢出的可能性:当SDS API需要对SDS进行修改时,API会先检查SDS空间是否满足修改要求,如果不满足,API会自动将SDS的空间扩展至执行修改所需的大小,然后才执行实际的修改操作,所以使用SDS既不需要手动修改SDS的空间大小,也不会出现前面所说的缓冲区溢出问题。

SDS的API里面也有一个用于执行拼接操作的sdscat函数,它可以将一个C字符串拼接到给定SDS所保存的字符串后面,但是在执行拼接的操作之前,sdscat会先检查给定SDS的空间是否足够,如果不够的话,sdscat会先扩展SDS的空间然后才进行拼接操作。

减少修改字符串时带来的内存重分配次数

因为C字符串不记录自身的长度,所以对于一个包含了N个字符的C字符串来说,这个C字符串的底层实现总是一个N+1个字符长的数组(额外的一个字符空间保留空字符)。因为C字符串的长度和底层数组的长度之间存在着这种关联性,所以每次增长或者缩短一个C字符串,程序总要对保存这个C字符串进行一次内存重分配操作:

  • 如果程序执行的是增长字符串的操作,比如拼接操作(append),那么在执行这个操作之前,程序需要先通过内存重分配来扩展底层数组的空间大小-----如果忘了这一步就会产生缓冲区溢出
  • 如果程序执行的是缩短字符串的操作,比如截断操作(trim),那么在执行这个操作之后,程序需要通过内存重分配来释放字符串不再使用那部分空间----吐过忘了这一步就会产生内存泄漏

因为内存重分配涉及复杂的算法,并且可能需要执行系统调用,所以它通常是一个比较耗时的操作:

  • 在一般程序中,如果修改字符串长度的情况不太常出现,那么每次修改都执行一次内存重分配是可以接受的
  • 但是Redis作为数据库,经常被用于速度要求苛刻、数据被频繁修改的场合,如果每次修改字符串都要执行一次内存重分配的话,那么光是内存重分配操作就会占去修改字符串所用时间的一大部分,如果这种修改频繁地发生的话,可能还会对性能造成影响

为了避免C字符串这种缺陷,SDS通过未使用空间解除了字符串长度和底层数组长度之间的关系:在SDS中,buf数组的长度不一定就是字符数量加一,数组里面可以包含未使用的字节,而这些字节的数量就由SDS的free属性记录。

  1. 空间预分配

    空间预分配用于优化SDS字符串增长操作:当SDS的API对一个SDS进行修改,并且需要内存扩展的时候,程序不仅会为SDS分配修改时所需要的空间,还会为SDS分配额外未使用的空间

    额外分配的未使用空间数量由以下公式决定:

    • 如果对SDS进行修改后,SDS长度将小于1MB,那么程序分配和len属性同样大小的未使用空间,这时SDS len属性和free属性相同,所以SDS的buf数组实际长度变成2*len+1(空字符)
    • 如果对SDS进行修改后,SDS长度将大于等于1MB,那么程序会分配1MB的未使用空间

    通过空间预分配策略,Redis可以减少连续执行字符串增长操作所需的内存重分配次数

    在扩展SDS空间之前,SDS API会先检查未使用空间是否足够,如果足够的话,API就会直接使用未使用空间,而无须执行内存重分配

    通过这种预分配策略,SDS将连续增长N次字符串所需的内存重分配次数小于等于N次

  2. 惰性空间释放

    惰性空间释放用于优化SDS的字符串缩短操作:当SDS的API需要缩短SDS保存的字符串时,程序并不立即使用内存重分配来回收缩短后多出来的字节,而是用free属性将这些字节的数量记录起来,并等待来使用

    举个例子,sdstrim函数接受一个SDS和一个C字符串作为参数,移除SDS中所有在C字符串中出现过的字符

    比如对图2-14所示的SDS值s来说,执行:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-e0vLs5fr-1637423600131)(C:\Users\VV\AppData\Roaming\Typora\typora-user-images\image-20211120125914234.png)]

    会被修改成图2-15所示的样子。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-n7hCBilS-1637423600133)(C:\Users\VV\AppData\Roaming\Typora\typora-user-images\image-20211120130014822.png)]

    SDS并没有将多出来的8字节空间释放出来,而是作为未使用空间保留在了SDS里面,如果将来要对SDS进行增长操作的话,这些未使用空间就可能会派上用场

    通过惰性空间释放策略,SDS避免了缩短字符串时所需的内存重分配操作,并为将来可能有的增长操作提供了优化

    与此同时,SDS也提供了相应的API,让我们可以在有需要时,真正地释放SDS的未使用空间,所以不用担心惰性空间释放策略会造成空间的浪费

    二进制安全

    C字符串中的字符必须符合某种编码(比如ASCII),并且除了字符串的末尾之外,字符串里面不能包含空字符,否则最先被程序读入的空字符将被误认为是字符串的结尾,这些限制使得C字符串只能保存文本数据,而不能保存像图片、音频、视频、压缩文件这样的二进制数据

    虽然数据库一般用于保存文本数据,但使用数据库来保存二进制数据不少见,因此为了确保Redis可以适用于各种不同的,SDS的API都是二进制安全的,所有SDS API都会以处理二进制的方式来处理SDS存放在buf数组里的数据,程序不会对其中的数据做任何限制、过滤、或者假设,数据在写入时和读取时一致

    这也是我们将SDS的buf属性称为字节数组的原因----Redis不是用这个数组来保存字符,而是用它来保存一系列二进制数据

    SDS使用len属性的值来判断字符串是否结束

    通过使用二进制安全的SDS,而不是C字符串,使得Redis不仅可以保存文本数据,还可以保存任意格式的二进制数据

    兼容部分C字符串函数

    SDS一样会遵循C字符串以空字符结尾的惯例:

    这些API总会将SDS保存的数据的末尾设置为空字符,并且总会在为buf数组分配空间时多分配一个字节来容纳这个空字符,这是为了让那些保存文本数据的SDS可以重用一部分<string.h>库定义的函数

    通过遵循C字符串以空字符结尾的惯例,可以避免了不必要的代码重复

    总结
    C字符串SDS
    获取字符串长度的复杂度为O(n)获取字符串长度的复杂度为O(1)
    API是不安全的,可能会造成缓冲区溢出API是安全的,不会造成缓冲区溢出
    修改字符串长度N次必然需要执行N次内存重分配修改字符串长度N次最多需要执行N次内存重分配
    只能保存文本数据可以保存文本或者二进制数据
    可以使用所有<string.h>库中的函数可以使用一部分<string.h>库中的函数
    SDS API

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F2oy3pJ8-1637423600134)(C:\Users\VV\AppData\Roaming\Typora\typora-user-images\image-20211120145732879.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZVvn8ZYW-1637423600135)(C:\Users\VV\AppData\Roaming\Typora\typora-user-images\image-20211120145738440.png)]

链表

每个链表节点使用一个adlist.h/listNode结构来表示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ep5CIKbO-1637423600136)(C:\Users\VV\AppData\Roaming\Typora\typora-user-images\image-20211120154008135.png)]

多个listNode可以通过prev和next指针组成双端链表,如图3-1所示

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MpKoHyLs-1637423600137)(C:\Users\VV\AppData\Roaming\Typora\typora-user-images\image-20211120154038159.png)]

虽然仅仅使用多个listNode结构就可以组成链表,但使用adlist.h/list来持有链表的话,操作起来会更方便:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1PEBzX7Y-1637423600138)(C:\Users\VV\AppData\Roaming\Typora\typora-user-images\image-20211120154128896.png)]

list结构为链表提供了表头指针head,表尾指针tail,以及链表长度计数器len,而dup、free和match成员适用于实现多态链表所需的类型特定函数:

  • dup函数用于复制链表节点所保存的值
  • free函数用于释放链表节点所保存的值
  • match函数则用于对比链表节点所保存的值和另一个输入值是否相等

图3-2是由一个list结构和三个listNode结构组成的链表

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jG9tACMQ-1637423600139)(C:\Users\VV\AppData\Roaming\Typora\typora-user-images\image-20211120154403542.png)]

Redis的链表实现的特性可以总结如下:

  • 双端:链表节点带有prev和next指针,获取某个节点的前置节点和后置节点的复杂度都是O(1)
  • 无环:表头节点的prev指针和表尾节点的next指针都指向NULL,对链表的访问以NULL为终点
  • 带表头指针和表尾指针:通过list结构的head指针和tail指针,程序获取链表的表头节点和表尾节点的复杂度为O(1)
  • 带链表长度计数器:使用list结构的len属性对list持有的链表节点进行计数,获取长度的复杂度为O(1)
  • 多态:链表节点使用void*指针来保存节点值,可以通过list结构的dup、free、match三个属性为节点值设置类型特定函数,所以链表可以用于保存各种不同类型的值
链表和链表节点的API

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-N1WWXhFn-1637423600140)(C:\Users\VV\AppData\Roaming\Typora\typora-user-images\image-20211120225107146.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gOauEBis-1637423600141)(C:\Users\VV\AppData\Roaming\Typora\typora-user-images\image-20211120225117303.png)]

重点回顾
  • 链表被广泛用于实现Redis的各种功能,比如列表键,发布与订阅、慢查询、监视器等
  • 每个链表节点由一个listNode结构来表示,每个节点都有一个指向前置节点和后置节点的指针
  • 每个链表使用一个list结构来表示,这个结构带有表头节点指针、表尾节点指针,以及链表长度等信息
  • 因为链表表头节点的前置节点和表尾节点的后置节点都指向NULL,所以Redis的链表实现是无环链表
  • 通过为链表设置不同的类型特定函数,Redis的链表可以用于保存各种不同类型的值

字典

字典的实现

Redis的字典使用哈希表为底层实现,一个哈希表里面可以有多个节点,而每个哈希表节点就保存了字典中的一个键值对

哈希表

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-54kS3WR3-1637423600142)(C:\Users\VV\AppData\Roaming\Typora\typora-user-images\image-20211120231456360.png)]

table属性是一个数组,数组中每个元素指向dictEntry结构的指针,每个dictEntry结构保存一个键值对

size属性记录了哈希表的大小,即table数组的大小

used属性则记录了哈希表目前已有节点(键值对)的数量

sizemask属性的值总是等于size-1,这个属性和哈希值一起决定一个键应该被放到table数组的哪个索引上面。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vkANTvcW-1637423600143)(C:\Users\VV\AppData\Roaming\Typora\typora-user-images\image-20211120232207836.png)]

如上是一个大小为4的空的哈希表

哈希表节点

哈希表节点使用dictEntry结构表示,每个dictEntry结构都保存着一个键值对:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Zx1P6FO6-1637423600144)(C:\Users\VV\AppData\Roaming\Typora\typora-user-images\image-20211120232325002.png)]

key属性保存着键值对中的键,v属性则保存着键值对中的值,其中键值对的值可以是一个指针,或者一个uint64_t证书,又或者是一个int64_t整数

next指向另一个哈希表节点的指针,这个指针可以将多个哈希值相同的键值对连接在一起,来解决键冲突问题

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tOuDNmox-1637423600144)(C:\Users\VV\AppData\Roaming\Typora\typora-user-images\image-20211120232754882.png)]

字典

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RMkdCmCZ-1637423600145)(C:\Users\VV\AppData\Roaming\Typora\typora-user-images\image-20211120233723914.png)]

type属性和privdata属性是针对不同类型的键值对,为创建多态字典而设置的

  • type属性是指向一个dictType结构的指针,每个dictType结构保存了一簇用于操作特定类型键值对的函数,Redis会为用途不同的字典设置不同的类型特定函数
  • privdata属性保存了需要传给那些类型特定函数的可选参数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jA8C8cKV-1637423600146)(C:\Users\VV\AppData\Roaming\Typora\typora-user-images\image-20211120234421890.png)]

ht属性是一个包含两个项的数组,数组中的每个项都是一个dictht哈希表,一般情况下,字典只使用ht[0]哈希表,ht[1]哈希表只会在对ht[0]哈希表进行rehash时使用

除了ht[1]之外,另一个和rehash有关的属性就是rehashidx,它记录了rehash目前的进度,如果没有进行rehash,则它的值为-1

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Neei9zdV-1637423600147)(C:\Users\VV\AppData\Roaming\Typora\typora-user-images\image-20211120234843335.png)]

哈希算法

当要将一个新的键值对添加到字典里面时,先根据键值对的键计算出哈希值和索引值,然后再根据索引值,将包含新键值对的哈希表节点放到哈希表数组的指定索引上面

Redis计算哈希值和索引值的方法如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yaKUzIJG-1637498227312)(C:\Users\VV\AppData\Roaming\Typora\typora-user-images\image-20211121103133931.png)]

如果要将一个键值对k0和v0添加到字典里面,那么程序会先使用语句:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fF97hRv4-1637498227316)(C:\Users\VV\AppData\Roaming\Typora\typora-user-images\image-20211121103523518.png)]

计算键k0的哈希值。

假设计算得出的哈希值为8,那么程序会继续执行:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xkM3L2NW-1637498227318)(C:\Users\VV\AppData\Roaming\Typora\typora-user-images\image-20211121103632245.png)]

计算出键k0的索引值0,这表示包含键值对k0和v0的节点应该被放置到哈希表数组的索引0位置上,如图4-5所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tcMzeUBR-1637498227320)(C:\Users\VV\AppData\Roaming\Typora\typora-user-images\image-20211121104609165.png)]

解决键的冲突

当有两个或以上数量的键被分配到了哈希表数组的同一个索引上面时,我们称这些键发生了冲突

Redis的哈希表使用链地址法来解决键冲突,每个哈希表节点都有一个next指针,多个哈希表节点可以用next指针构成一个单向链表,被分配到同一个索引上的多个节点可以用这个单向链表连接起来,这样就解决了键冲突

举个例子,假设程序要将键值对k2和v2添加到图4-6所示的哈希表里面,并且计算得出k2的索引值为2,那么键k1和k2将产生冲突,而解决冲突的办法就是使用next 指针将键k2和k1所在的节点连接起来,如图4-7所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lbWLiCvC-1637498227322)(C:\Users\VV\AppData\Roaming\Typora\typora-user-images\image-20211121131453226.png)]

rehash

随着操作不断执行,哈希表保存的键值对会逐渐地增多或者减少,为了让哈希表的负载因子维持在一个合理的范围内,当哈希表保存的键值对数量太多或者太少时,程序需要对哈希表的大小进行相应的扩展和收缩

扩展和收缩哈希表的工作可以通过执行rehash(重新散列)操作来完成,Redis对字典的哈希表执行rehash步骤如下:

  1. 为字典ht[1]哈希表分配空间,这个哈希表的空间大小取决于要执行的操作,以及ht[0]当前包含的键值对数量(也就是ht[0].used属性的值)
    • 如果执行的是扩展操作,那么ht[1]的大小为第一个大于等于ht[0].used*2的 2 n 2^n 2n
    • 如果执行的是收缩操作,那么ht[1]的大小为第一个大于等于ht[0].used的 2 n 2^n 2n
  2. 将保存在ht[0]中的所有键值对rehash到ht[1]上面:rehash指的是重新计算键的哈希值和索引值,然后将键值对放置到ht[1]哈希表的指定位置上
  3. 当ht[0]包含的所有键值对都迁移到了ht[1]之后(ht[0]变为空表),释放ht[0],将ht[1]设置为ht[0],并在ht[1]新创建一个空白哈希表,为下一次rehash做准备。
哈希表的扩展与收缩

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

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

其中哈希表的负载因子可以通过公式: l o a d _ f a c t o r y = h t [ 0 ] . u s e d / h t [ 0 ] . s i z e load\_factory = ht[0].used/ht[0].size load_factory=ht[0].used/ht[0].size计算得出

根据BGSAVE命令或BGREWRITEAOF命令是否在运行,服务器执行扩展操作所需的负载因子不同,这是因为在执行BGSAVE命令或BGREWRITEAOF命令的过程中,Redis需要创建当前服务器进程的子进程,而大多数操作系统都采用写时复制(copy-on-write)技术来优化子进程的使用效率,所以在子进程存在期间,服务器会提高执行扩展操作所需的负载因子,从而尽可能地避免在子进程存在期间进行哈希表扩展操作,来避免不必要的内存写入操作,最大限度地节约内存

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

渐进式rehash

扩展或收缩哈希表需要将ht[0]里面所有键值对rehash到ht[1]里面,但是这个rehash动作不是一次性、集中式地完成的,而是分多次、渐进式地完成的

这样做的原因在于当哈希表里保存的键值对数量很多的时候rehash会导致服务器在一段时间内停止服务

因此服务器是分多次、渐进式地将ht[0]里面的键值对慢慢地rehash到ht[1]

步骤:

  1. 为ht[1]分配空间,让字典同时持有ht[0]和ht[1]两个哈希表
  2. 在字典中维持一个索引计数器变量rehashidx,并将它的值设置为0,表示rehash工作开始
  3. 在rehash进行期间,每次对字典执行添加、删除、查找或者更新操作时,程序除了执行指定的操作以外,还会顺利将ht[0]哈希表在rehashidx索引上的所有键值对rehash到ht[1],当rehash工作完成之后,程序将rehashidx属性的值增一
  4. 随着字典操作的不断进行,最终在某个时间点上,ht[0]的所有键值对都会被rehash至ht[1],这时程序将rehashidx属性的值设为-1 ,表示rehash操作已完成

渐进式rehash的好处在于它采取分而治之的方式,将rehash键值对所需的计算工作均摊到对字典的每个添加、删除、查找和更新操作上,从而避免了集中式rehash而带来的庞大计算量

渐进式rehash执行期间的哈希表操作

因为在进行渐进式rehash的过程中,字典会同时使用ht[0]和ht[1]两个哈希表,所以在渐进式rehash进行期间,字典的删除、查找、更新等操作会在两个哈希表上进行。例如,要在字典里面查找一个键的话,程序会现在ht[0]里面进行查找,如果没找到的话,就会继续在ht[1]里面进行查找,诸如此类。

另外,在渐进式rehash执行期间,新添加到字典的键值对一律会被保存到ht[1]里面,而ht[0]则不再进行任何操作,这一措施保证了ht[0]包含的键值对数量只减不增,并随着rehash的执行而最终变成空表

重点回顾
  • 字典被广泛用于实现Redis的各种功能,其中包括数据库和哈希键
  • Redis中的字典使用哈希表作为底层实现,每个字典由两个哈希表,一个平时使用,另一个仅在进行rehash时使用
  • 哈希表使用链地址法来解决键冲突,被分配到同一个索引上的多个键值对会连接成一个单向链表
  • 在对哈希表进行扩展或者收缩操作时,程序需要将现有哈希表包含的所有键值对rehash到新哈希表里面,并且这个rehash过程并不是一次性完成的,而是渐进式完成的
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

独一无二的VV

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

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

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

打赏作者

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

抵扣说明:

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

余额充值