2.redis设计与实现学习笔记-简单动态字符串&链表

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字符串的SDSO(N),N为给定字符串的长度
sdsempty创建一个不包含任何内容的空SDSO(1)
sdsfree释放给定的SDSO(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(N2),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返回给定节点的后置节点xtnext属性获得, 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问链表长度
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值