2.redis学习笔记-简单动态字符串&链表

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/jun8148/article/details/82388329

1. 简单字符串

1.1. 简介

redis中没有C语言中的传统字符串,而是自己构建了一个简单动态字符串(SDS)。在redis中,C字符串只是字符串字面量用于无须修改的字符串。可修改的就要使用到SDS了。

1.2. SDS

sds.h/sdshdr结构:

struct sdshdr{
    // 记录buf数组中已使用字节的数量
    // 等于SDS所保存字符串的长度
    int len;
    // 记录buf数组中未使用字节的数量
    int free;
    // 字节数组,用于保存字符串
    char buf[];
};
  • free属性值为0,表示这个SDS没有分配任何未使用空间。
  • len属性的值为多少,表示这个SDS保存多少字节的字符串。
  • buf是char类型数组,保存着字符串,SDS遵循C字符串以空字符串结尾的惯例,结尾是‘\0’
  • SDS:里面的buf,就是我们在C中学的字符串。

1.3. SDS和C字符串的区别

C语言中字符串使用的是长度为N+1的字符数组表示一个长度为N的字符串,而且最后的字符永远都是‘\0’

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

​ C字符串不会记录本身的长度信息,所以我们为了获取字符串长度,只能遍历整个字符串,进行计数,时间复杂度是O(N).

SDS中有一个len属性记录了SDS本身的长度,时间复杂度是O(1)。

1.3.2. 杜绝缓冲区溢出

因为C字符串不记录本身的长度,所以C字符串容易造成缓冲区溢出,不要问我具体的,学过C语言的应该都知道。

SDS的空间分配策略杜绝了缓冲区溢出,当SDS API对SDS进行修改的时候,API会首先检查SDS的空间是否满足修改所需的要求,如果不满足,API会自动扩展SDS的空间至能满足的大小,再执行实际的修改操作。所以,SDS既不需要人为的修改SDS的空间大小,也不会出现缓冲区溢出的问题。

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

C字符串中,字符串的长度和底层的数组的长度之间存在着关联性,如果我们增长或者缩短一个C字符串,程序都会对这个C字符串进行一次内存分配的操作。

SDS用过未使用空间解除了字符串长度和底层数组长度之间的关联:在SDS中,buf数组的长度不一定就是字符数量加一,数组中可以包含未使用的字节,这些字节由free属性记录,而SDS就通过这些未使用的空间,实现了空间预分配和惰性空间释放两种优化策略。

1.3.3.1. 空间预分配-用于扩展

当SDS API对一个SDS进行修改并且要对SDS进行空间扩展的时候,程序不仅会为SDS分配修改所需的必须空间,还会分配一些额外的未使用空间。

额外未使用的空间数量:

  • 如果SDS进行修改之后,SDS的长度(len值)小于1MB,那么分配与len属性一样大小的未使用空间。即free = len.
  • 如果SDS进行修改之后,SDS的长度(len值)大于等于1MB,那么分配1MB的未使用空间。

    通过这种方法,当我们再次需要扩展的时候,就会优先考虑未使用的空间,无须执行内存重分配。

1.3.3.2. 惰性空间释放-用于缩短

当我们需要缩短SDS保存的字符串时,程序不会立即使用内存重分配来回收缩短后多出来的字节,而是使用free属性将这些字节的数量记录下来,并等待将来使用。

当然SDS API提供了方法真正释放SDS的未使用空间,所以不需要担心会造成内存浪费。

1.3.4. 二进制安全

C字符串中的字符要符合编码,并且除字符串末尾,字符串内不能包含空字符(‘\0’)。C字符把空字符作为字符串结尾的唯一标识,读到了控制符就结束了。所以C字符串,只能保存文本数据,而不能保存很多的二进制数据,例如:图像、音频之类的。

SDS API以处理二进制的方式处理SDS存放在buf数组中的数据,程序不会对其中的数据做任何限制、过滤或者假设,数据写入的是什么,读出来就是什么。SDS通过len属性来确定字符串结束,而不是空字符。

1.3.5. 兼容部分C字符串函数

SDS遵循了C字符串以空字符结尾的惯例,所以保存文本数据的SDS可以重用部分C字符串库中的函数<string.h>

1.3.6. 总结区别

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

1.4. SDS的主要API

函数 作用 时间复杂度
sdsnew 创建一个包含给定C字符串的SDS O(N),N为给定字符串的长度
sdsempty 创建一个不包含任何内容的空SDS O(1)
sdsfree 释放给定的SDS O(N),N为被释放的SDS长度
sdslen 返回SDS的已使用空间字节数 O(1),读len
sdsavail 返回SDS的未使用字节数 O(1),读free
sdsdup 创建一个给定SDS的副本(copy) O(N),N为给定SDS的长度
sdsclear 清空SDS保存的字符串内容 O(1),惰性空间释放策略
sdscat 将给定C字符串拼接到SDS字符串末尾 O(N),N为C字符串长度
sdscarsds 将给定sds字符串拼接到另一个sds字符串末尾 O(N),N为被拼接的SDS字符串的长度
sdscpy 将给定的C字符串复制到SDS里面,覆盖SDS原有的字符串 O(N),N为被复制的C字符串长度
sdsgrowzero 用空字符将SDS扩展到指定长度 O(N),N为扩展新增的字节数
sdstrim 接收一个SDS和一个C字符串作为参数,从SDS中移除所有在C字符串中出现过的字符 O(N^2^),N为给定C字符串的长度
sdsrange 保留SDS给定区间的数据,不在区间的数据被覆盖或者清除 O(N),N为被保留数据的长度
sdscmp 对比两个SDS字符串是否相同 O(N),N为两个SDS中较短的那个SDS的长度

2. 链表

链表是什么,我懒得介绍了,应该都是学过的。
链表在Redis中的应用非常广泛,什么列表键啊、保存多个客户端的状态信息、构建客户端输出缓冲区啊之类的。反正用途非常广泛。

2.1. 数据结构设计

链表节点-adlist.h/listNode结构:(双向链表)

typedef struct listNode {
    // 前置节点
    struct listNode *prev;
    // 后置节点
    struct listNode *next;
    // 节点的数据
    void *value;
}listNode;

链表-list:

typedef struct list {
    // 表头节点
    listNode *head;
    // 表尾节点
    listNode *tail;
    // 链表所包含的节点数
    unsigned long len;
    // 节点值复制函数,实现复制链表节点所保存的值
    void *(*dup)(void *ptr);
    // 节点值释放函数,实现释放链表节点所保存的值
    void (*free)(void *ptr);
    // 节点值对比函数,实现对比链表节点的值与另一个输入值是否相等
    int (*match)(void *ptr,void *key);

}list;

链表list提供了表头指针head、表尾指针tail、以及链表长度len、其余的成员可由注释直接清晰看出。

Redis 的链表实现的特性:

  • 双端:采用双向链表的设计方式,含prev和next指针,获取某个节点的前置节点和后置节点的复杂度是O(1)。
  • 无环:表头节点的prev和表尾节点的next都指向NULL,以NULL作为终点。
  • 带表头指针和表尾指针:获取list的head和tail指针,获取头尾节点的复杂度O(1).
  • 带链表长度计数器len,获取链表中节点数的复杂度O(1)。
  • 多态:链表节点使用void*指针来保存节点,并且可以通过dupfreematch三个属性为节点值设置类型特定函数,所以链表能用来保存不同类型的值。

2.2. 链表和链表节点的API

函数 作用 时间复杂度
listSetDupMethod 将给定的函数设置为链表的节点值复制函数 复制函数可以直接通过链表的dup属性直接获得,O(1)
listGetDupMethod 返回链表当前正在使用节点值的复制函数 O(1)
listSetFreeMethod 将给定的函数设置为链表的节点值释放函数 释放函数可以直接通过链表的free属性直接获得,O(1)
listGetFree 返回链表当前正在使用节点值的释放函数 O(1)
listSetMatchMethod 将给定的函数设置为链表的节点值对比函数 对比函数可以直接通过链表的match属性直接获得,O(1)
listGetMatchMethod 返回链表当前正在使用的节点值对比函数 O(1)
listLength 返回链表的长度(包含了多少个节点) 链表长度可以通过链表的len属性直接获得,O(1)
listFirst 返回链表的表头节点 head属性获得,O(1)
listLast 返回链表的表尾节点 tail属性获得,O(1)
listPrevNode 返回给定节点的前置节点 prev属性获得, O(1)
listNextNode 返回给定节点的后置节点xt next属性获得, O(1)
listNodeValue 返回给定节点目前正在保存的值 value属性获得, O(1)
listCreate 创建一个不包含任何节点的新链表 O(1)
listAddNodeHead 将一个包含给定值的新节点添加到给定链表的表头 O(1)
listAddNodeTail 将一个包含给定值的新节点添加到给定链表的表尾 O(1)
listInsertNode 将一个包含给定值的新节点添加到给定节点的之前或者之后 O(1)
listSearchKey 查找并返回链表中包含给定值的节点 O(N),N为链表长度
listIndex 返回链表在给定=索引上的节点 O(N),N问链表长度
listDelNode 从链表中删除节点 O(N),N问链表长度
listRotate 将链表的表尾节点弹出,然后被弹出的节点查到链表的表头,成为新的表头节点 O(1)
listDup 复制一个给定链表的副本 O(N),N问链表长度
listRelease 释放给定链表,以及链表中的所有节点 O(N),N问链表长度

猜你喜欢

转载自blog.csdn.net/jun8148/article/details/82388329