最近在写一个业务的,本来几个接口就完事了,但是因为元旦假期,PM、DE都请假了(羡慕),没法推进了,闲着也是闲着,索性再跟着书复习一遍Redis中的数据结构。
SDS(简单动态字符串)
图示
SDS是Redis重新设计的一种字符串,中文名是简单动态字符串,如上图所示,它会有几个属性:
- len: 记录SDS的实际已经使用的长度,也就是字符串中buf数组中使用的字节的数量。
- free: 记录buf数组中未使用字节的数量。
- buf: 字节数组,用来保存字符串。
之前以为SDS里面完全没用到C语言中的字符串呢,其实buf指向的还是C语言原生的字符串。
要点
SDS的用处
SDS用处很多,Redis中大部分字符串都是用SDS存的,比如一句 set name 'zhangsan'
这里的key(name)和value(zhangsan)都是用SDS进行存储。
SDS和C语言原生字符串的区别
SDS里面也是含有C语言原生字符串的(buf指向的那个部分),这样的好处是可以方便的使用C语言原生字符串的一些函数而不用自己重新写了(比如printf(%s,s->buf)
)。但是也可以总结一下二者的区别,也就是面试官常问的"Redis为什么要重新设计一个SDS?"。
- 取字符串长度的时间复杂度不同
- SDS结构体中,有一个单独的len来记录字符串的长度,是常数级复杂度。
- C语言并不会记录字符串的长度,想要获取时需要遍历整个字符串,对遇见的每个字符进行计数,直到遇到代表字符串结尾的空字符串('/0')为止,复杂度为O(N)。
- 想想看其实是一种空间换时间的思想。
- 杜绝缓冲区溢出
- 上图展示的就是使用C语言的strcat()函数导致字符串S1溢出到S2的部分了。
- 而SDS在进行修改前会检查空间是否满足要求,如果不满足会自动将SDS的空间扩展至执行修改所需要的大小,然后再进行修改。
- 为了避免频繁的发生字符串长度修改,SDS引入了free属性解除了字符串长度和底层数组长度之间的关联。
- 如果对SDS进行修改之后,SDS的长度小于1MB,那么程序分配和len属性相同大小的未使用空间。2n+1(额外的一字节用于保存空字符)
- 如果对SDS进行修改之后,SDS的长度大于等于1MB,那么程序会分配1MB的未使用空间。n+1MB+1
- 当需要缩短SDS时,不会立马回收空间,而是使用free来记录空闲的空间。
- 二进制安全
- C语言字符串是二进制不安全的,所以不能用来存放音频、图片类型的数据。
- SDS不用空字符串来判断字符串的结尾,用len来决定字符串的长短(buf数组中的长度),所以是二进制安全的。
List(链表)
图示
list是由一个一个的listNode构成的双向链表,其中head指向头节点,tail指向尾节点,len记录链表的长度,dup函数用于复制链表节点所保存的值,free函数用于释放链表节点所保存的值,match函数用于对比链表节点所保存的值和另一个输入值是否相等。
要点
- list是一个双端无环链表
- 它被广泛的用于列表键、发布订阅、慢查询、监视器等地方。
Dict(字典)
图示
字典用来存放键值对,每个键都是唯一的,作为一个常用的数据结构,C语言没有内置,所以Redis自己实现了。
-
哈希表(dictht)中的table是一个数组(类型为dictEntry),每个dictEntry结构保存着一个键值对。size记录了哈希表的大小。used属性记录了哈希表目前已经有键值对的数量。sizemask属性的值总是=size-1,等价于取模运算。
-
dictEntry中的v属性可以是一个指针,或者是一个uint64_t整数或者int64_t整数。next属性是指向两一个dictEntry的指针,这个指针可以将多个哈希值相同的键值对链接在一起,解决哈希冲突的问题。
-
字典(dict)结构体中有一个dictht的数组,大小为2,一般情况下只会使用ht[0],ht[1]只会在对ht[0]进行rehash的时候使用。rehashidx属性用来记录rehash进度,如果目前没有在进行rehash,那么它的值为-1.
要点
字典的用处
- Redis中有一个字典用来代表数据库,比如
set msg "hello world"
就是一个键为 msg 值为hello world的键值对,放在了字典表示的数据库中。 - 字典还是哈希键的底层实现之一,当一个哈希键包含的键值对比较多,Redis就会使用字典作为哈希键的底层实现。
解决哈希冲突
- Redis的哈希表使用拉链法来解决哈希冲突,每个哈希表的节点都有一个next指针,使得多个路由到同一个索引上的节点可以使用链表串起来。这里采用的是头插法(因为redis是单线程,也就不需要考虑java中成环的问题)
rehash
当哈希表中的键值对不断的增加或者减少的时候,查询需要对哈希表的大小进行相应的扩展或者收缩。
-
1)为字典的ht[1]哈希表分配空间,这个大小取决于要执行的操作和ht[0]当前包含的键值对数量(ht[0].used)。如果执行的扩展操作,ht[1]的大小为第一个大于等于ht[0].used*2的2的幂;如果执行的是收缩操作,ht[1]的大小为第一个大于等于ht[0].used的2的幂。
-
2)将保存在ht[0]的所有键值对rehash到ht[1]上面。
-
3)当ht[0]包含的所有键值对都迁移到ht[1]上之后,释放ht[0],将ht[1]设置为ht[0],并新创建一个空白哈希表,为下一次rehash做准备。
什么时候进行rehash?
- 服务器目前没有正在执行BGSAVE或者BGREWRITEAOF,并且哈希表的复杂因子大于等于1.
- 服务器目前正在执行BGSAVE或者BGREWRITEAOF,并且哈希表的负载因子大于等于5.
- 负载因子 = ht[0].used/ht[0].size
- 上面的是进行扩展的,当负载因子小于0.1的时候进行收缩。
渐进式rehash
- rehash的动作不是一次性完成的,因为哈希表中的数据量可能是庞大的,如果一次性将所有的键值对都rehash到ht[1]的话,可能会导致服务器在一段时间内停止服务。所以需要分多次、渐进式的完成。
- 1.一开始让字典同时持有ht[0]和ht[1]两个哈希表;2.将rehashidx由-1改为0表示rehash工作正式开始;3.在rehash进行期间,每次对字典执行添加、删除、查找、更新操作的时候,会顺带将ht[0]哈希表在rehashidx索引上的所有键值对rehash到ht[1]上,并把rehashidx的值+1;4.随着字典操作的不断执行,最终在某个时间点上ht[0]的所有的键值对都会被rehash到ht[1]上,这时将rehashidx的值设为-1。
- 在进行rehash期间,字典的删除、查找、更新操作会现在ht[0]进行,如果没找到才去ht[1]中找。而新添加操作则直接在ht[1]中进行。