此文参考《Redis设计与实现》做出的总结
底层数据结构
SDS(简单动态字符串)
在Redis也会有一些字符串字面量,也就是不会进行修改的字符串使用C语言的字符串,即以空字符串结尾的字符数组。但为了修改的方便,Redis自行构建了一种数据结构用来保存可修改的字符串。
一个SDS结构体中,有三个变量分别是len用来记录当前保存的字符串长度,free用来记录未使用的字节量,buf字符数组用来保存字符串。这样做的优点是:
-
以O(1)复杂度来获取字符串长度len
-
杜绝缓冲区溢出,每次给字符串后添加字符时会通过free来确定SDS是否有足够的空间容纳,如果不够会自动扩展空间
-
减少内存重分配次数,C字符串每次修改如果空间不够都将导致字符串重新分配内存比较耗时,而SDS通过空间预分配和惰性空间释放来减少内存重分配。空间预分配指当修改后SDS长度小于1MB时,那么程序将多分配一倍的空间预留给SDS,例如修改后len为13那么free也将是13,之后如果再在SDS后进行添加字符大概率free是够用的,大于1MB预留1MB时;惰性空间释放指当字符串修改后长度变短时并不进行内存重分配,而是将多出来的空间预留下来,用free记录预留空间,SDS也提供了相应的API,让我们可以在有需要时,真正地释放SDS的未使用空间,避免内存浪费
-
二进制安全,C字符串中的字符必须符合某种编码且只能末尾为空字符,而SDS可以随意存取,当buf数组中有多个空字符时也不会影响(C字符串碰到空字符便认为字符串结束),因为SDS使用len判断字符串是否结束
-
兼容部分C字符串函数,SDS仍然遵循C字符串以空字符结尾的惯例,为了重用C字符串函数
map(字典)
字典使用哈希表作为底层结构,使用拉链法处理哈希冲突,为了速度总是将新节点加到表头。字典中有两个哈希表,一般情况只使用哈希表1,在rehash时会同时使用两个哈希表。
rehash步骤:
- 为字典的ht[1]哈希表分配空间。如果执行的是扩展操作,那么ht[1]的大小为第一个大于等于ht[0].used*2的2的n次方幂,如果执行的是收缩操作,那么ht[1]的大小为第一个大于等于ht[0].used的2的n次方幂
- 将保存在ht[0]中的所有键值对rehash到ht[1]上面
- 当ht[0]包含的所有键值对都迁移到了ht[1]之后(ht[0]变为空表),释放ht[0],将ht[1]设置为ht[0],并在ht[1]新创建一个空白哈希表,为下一次rehash做准备
Redis的rehash是渐进式rehash,并不是一次性完成所有的rehash操作,这是采用分而治之的思想,为了防止rehash对服务器性能造成影响,将操作均摊到每次对字典的操作上。渐进式rehash操作:
- 在字典中维持一个索引计数器变量rehashidx,并将它的值设置为0,表示rehash工作正式开始。
- 在rehash进行期间,每次对字典执行添加、删除、查找或者更新操作时,程序除了执行指定的操作以外,还会顺带将ht[0]哈希表在rehashidx索引上的所有键值对rehash到ht[1],当rehash工作完成之后,程序将rehashidx属性的值增一。
- 随着字典操作的不断执行,最终在某个时间点上,ht[0]的所有键值对都会被rehash至ht[1],这时程序将rehashidx属性的值设为-1,表示rehash操作已完成。
渐进式rehash期间执行增删改查
- 查找一个键的话,程序会先在ht[0]里面进行查找,如果没找到的话,就会继续到ht[1]里面进行查找,诸如此类。
- 修改和删除一个键由于首先要查找到这个键,故和查找一样
- 添加到字典的键值对一律会被保存到ht[1]里面,而ht[0]则不再进行任何添加操作,这一措施保证了ht[0]包含的键值对数量会只减不增,并随着rehash操作的执行而最终变成空表。
linkedlist(链表)
和数据结构的链表实现方式一样,是一个双向链表
skiplist(跳跃表)
基本介绍
跳跃表是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。
- 跳跃表支持平均O(logN)、最坏O(N)复杂度的节点查找,还可以通过顺序性操作来批量处理节点。
- 在大部分情况下,跳跃表的效率可以和平衡树相媲美,并且因为跳跃表的实现比平衡树要来得更为简单,所以有不少程序都使用跳跃表来代替平衡树。
跳跃表对象结构:
typedef struct zskiplist {
<