深入理解Redis数据结构与字符串操作优化(阅读笔记、一)

目录

一、简单动态字符串

1.SDS定义

2.杜绝缓冲区溢出

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

        1)增长字符串:空间预分配

        2)缩短字符串:惰性释放空间

4.二进制安全

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

6.总结

二、 链表

三、字典

1.字典的实现

1)哈希表

2)哈希表节点

3)字典

2.哈希算法 

3.解决键冲突

4.rehash

5.渐进式rehash


一、简单动态字符串

Redis没有使用传统的C语言字符串,而是自己构建了一种简单动态字符串(simple dynamic string, SDS)的抽象类型,用作redis默认字符串。

1.SDS定义

struct sdshdr{
    // 记录buf数组中已使用字节的数量
    // 等于SDS所保存字符串的长度
    int len;
    // 记录buf数组中未使用字节的数量
    int free;
    // 字节数组,用于保存字符串
    char buf[];
}

C获取字符串长度:O(n)

SDS获取字符串长度:O(1)

2.杜绝缓冲区溢出

当修改字符串长度时:

SDS先检查空间大小是否满足修改的需求,不满足则通过sdscat扩展。

C不记录自身长度,所以strcat假定用户在执行函数时,已经分配足够的内存。

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

C调整字符串长度时会造成以下问题:

1.增长字符串,若没有进行内存重分配扩展空间,会造成缓冲区溢出

2.缩短字符串,若没有释放不再使用的空间,会造成内存泄漏;

 SDS优化策略:

        1)增长字符串:空间预分配

        检查free大小是否够用,够用直接用,不够用重分配。

        SDS.len < 1M时,free = len;

        SDS.len >= 1M时,free = 1M;

        2)缩短字符串:惰性释放空间

        缩短字符串时程序不会立即重分配回收空间,而是使用free属性记录缩短字节的数量,等待将来使用。

        SDS也提供了API,真正释放SDS未使用的空间,避免释放策略造成内存浪费。

4.二进制安全

C字符串中的字符必须符合某种编码(比如ASCII),除了末尾之外不能有任何空字符。否则读入空字符串就会被误认为是结尾,使得C字符串只能保存文本数据。而不能保存图片、音频、视频、压缩文件等二进制数据。

SDS的API都是二进制安全的,会以处理二进制的方式处理SDS存放到buf数组里的数据。

SDS的buf属性成为“字节数组”的原因:redis不是用这个数组来保存字符,而是用来保存二进制数据。

Redis不仅可以保存文本数据,还可以保存任意格式的二进制数据。

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

 SDS遵循C字符串以空字符结尾的惯例,为了可以使用部分<string.h>库定义的函数。

例如:

        strcasecmp函数,比对SDS和C字符串的值。

        strcat函数,将保存文本数据的SDS作为strcat函数的第二个参数追加到C字符串的后面。

6.总结

C字符串SDS
获取字符串长度的复杂度O(n)获取字符串长度的复杂度O(1)
API是不安全的,会造成缓冲区溢出API是安全的,不会造成缓冲区溢出(预分配、惰性释放)
每次修改字符串长度都要内存重分配修改字符串长度N次,最多执行N次内存重分配
只能保存文本能保存文本、二进制数据
可以使用全部<string.h>库定义的函数可以使用部分<string.h>库定义的函数

二、 链表

typedef struct listNode {

    // 前置节点
    struct listNode *prev;

    // 后置节点
    struct listNode *next;

    // 节点的值
    void *value;

} listNode;

多个listNode可以通过next,prev组成双端链表

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;

dup函数用于复制链表节点所保存的值

free函数用于释放链表节点所有保存的值

match用于对比链表节点所保存的值和另一个输入值是否相等

Redis的链表实现的特性如下:

双端、无环、带表头指针和表尾指针、带链表长度计数器、多态(节点使用void*指针来保存节点值,可以通过list结构的dup、free、match三个属性为节点设置类型特定函数,所以链表用于保存各种不同类型的值)。

三、字典

字典,又称为符号表、关联数组或映射,是一种用于保存键值对的抽象数据。

字典中的每个键都是独一无二的,通过键对值进行管理。

字典在Redis中应用相当广泛,比如Redis的数据库就是使用字典来作为底层实现的,对数据库的增、删、改、查操作也是构建在对字典操作之上的。

例如:创建一个键为“msg”,值为“hello world”的键值对时,这个键值对就是保存在代表数据库的字典里面。

除了用来表示数据库之外,字典还是哈希键的底层实现之一,当一个哈希键包含的键值对比较多,又或者键值对中的元素都是比较长的字符串时,Redis就会使用字典作为哈希键的底层实现。 

1.字典的实现

1)哈希表

typedef trust dictht{

    // 哈希表数组
    dictEntry **table;

    // 哈希表大小
    unsigned long size;

    // 哈希表大小掩码,用于计算索引值
    // 总是等于size-1
    unsigned long sizemask;
    
    // 该哈希表已有节点的数量
    unsigned long used;

} dictht;

 每个dictEntry保存着一个键值对

2)哈希表节点

typedef stuct dictEntry{

    // 键
    void *key;

    // 值
    union{
        void *val;
        unit64_t u64;
        int64_6 s64;
    } v;
    
    // 指向下个哈希表节点,形成链表
    struct dictEntry *next;

} dictEntry;

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

3)字典

typedef trust dict{

    // 类型特定的函数
    dictType *type;

    // 私有数据
    void *privdata;
    
    // 哈希表
    dictht ht[2];

    // rehash索引
    // 当rehash不在进行时,值为-1
    int rehash;

} dict;

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

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

除了ht[1]外,另一个与rehash相关的属性是rehashidx,记录了rehash目前的进度,没有进行rehash的时候,值为-1。

2.哈希算法 

将一个新的键值对添加到字典中时:

        1.根据键计算出哈希值和索引值

        2.将包含新键值对的哈希表节点放到哈希表数组指定的索引上

当字典被用作数据库的底层实现,或者哈希键的底层实现时,Redis使用MurmurHash2算法来计算键的哈希值。优点:即使输入的键是有规律的,也能给出一个很好的随机分布性,且算法的计算速度非常快。

3.解决键冲突

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

链表是由dictEntry组成的,因为dictEntry没有指向表尾的指针,为了速度考虑,程序总是将新节点添加到链表的表头位置(时间复杂度O(1))。

4.rehash

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

扩展和收缩哈希表的工作可以通过执行rehash(重新散列)操作来完成:

1.为ht[1]分配空间,这个哈希表的大小取决于要执行的操作,以及ht[0]当前包含的键值对数量(即ht[0].used属性的值):

        扩展,ht[1]大小为第一个大于等于ht[0].used*2的2^n(2的n次方幂);

        收缩,ht[1]大小为第一个大于等于ht[0].used的2^n(2的n次方幂);

2.将保存在ht[0]中的所有键值对rehash到ht[1]中:rehash指的是重新计算键的哈希值和索引值,然后放到指定的位置上。

3.ht[0]全部迁移到了ht[1]之后,释放ht[0],将ht[1]设置为ht[0],并新建个空白的哈希表ht[1],为rehash准备。

哈希表的扩展和收缩

以下条件满足任意一个,会自动扩展:

1.服务器没有执行BGSAVE命令或者BGREWRITEAOF命令,并且哈希值的负载因子大于等于1。

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

负载因子 = 哈希表已保存节点数量 / 哈希表大小

另外,当哈希表的负载因子小于0.1时,程序将自动开启收缩操作。

5.渐进式rehash

rehash动作并不是一次性完成的,而是分多次、渐进式的完成,详细步骤:

1.为ht[1]分配空间,让字典同时持有ht[1]和ht[0]两个哈希表。

2.在字典中维持一个索引计数器变量rehashidx,并将他的值设置为0,表示rehash开始。

3.在rehash进行期间,每次对字典进行增、删、改、查操作时,程序除了进行指定操作外,还会将ht[0]哈希表在rehashidx索引上的所有键值对rehash至ht[1],当rehash工作完成之后,rehashidx属性值增一。

4.当ht[0]的所有键值对都会被rehash到ht[1],这时程序将rehashidx属性的值设为-1,表示rehash操作完成。

渐进式的好处,避免了集中式rehash带来的庞大计算量。

  • 19
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

陈年小趴菜

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

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

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

打赏作者

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

抵扣说明:

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

余额充值