Redis 专题一: 数据结构


简单的动态字符串

  • redis没有直接使用C语言传统的字符串表示,而自己构建了一个动态字符串SDS,当redis需要的不仅仅是一个字符串字面量,而是一个可以被秀噶ide字符串值时,redis就会使用sds来表示字符串值,比如在redis的数据库里,包含字符串值的键值对在底层都是由SDS实现的。
redis  > set name "bugall"
ok
1.键值对的键是一个字符串对象,对象的底层实现是一个保存着字符串"name"的SDS
2.键值对的值也是一个字符串对象,对象的底层实现是一个保存这字符串"bugall"的SDS
  • 除了用来保存数据库中的字符串值之外,SDS还被用作缓冲区,AOF模块中的AOF缓冲区,以及客户端状态中的输入缓冲区。
    这里写图片描述
struct sdshdr{
    //记录buf数组中已使用字节的数量
    //等于SDS所保存字符串的长度
    int len;
    //记录buf数组中未使用字节的数量
    int free;
    //字节数组,用于保存字符串
    char buf[];
}
1. free属性的值为0,表示这个SDS没有分配任何未使用空间。
2. len属性的值为5,表示这个SDS保存了一个五字节长的字符串
3. buf属性是一个char类型数组,数组的前5个字节分别保存了‘r','e','d','i','s'五个字符,而最后一个字节则保存了空字符'\0'

注意:保存孔子福的1字节空间不计算在SDS的len属性里面,并且为空字符分配额外的一字节空间。
  • 与c字符串不同,SDS的空间分配策略完全杜绝了发生缓冲区溢出的可能性,当SDS api 需要对SDS进行修改时,API 会先检查SDS的空间是否满足修改所需要的要求,如果不满足的话,API会自动将SDS的空间拓展至执行修改所需要的大小,然后才执行实际的修改操作,所以使用SDS即不需要手动修改SDS的空间大小,也不会出现的缓冲区溢出的情况。

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

  • 空间预分配:
    空间预分配用于优化SDS的字符串增长操作:当SDS的API对一个SDS进行修改,而且需要对SDS进行空间拓展的时候,程序不仅会为SDS分配修改所必须要的空间,还会为SDS分配额外的未使用空间。通过空间的预分配策略,redis可以减少连续执行字符串增长操作所需的内存重分配次数。

  • 惰性空间释放
    惰性空间释放用于优化SDS字符串缩短操作:当SDS的API需要缩短字符时,程序并不是立即使用内存重分配来回收缩短后多出来的字节,而是将这些字节的数量记录起来(free),并等待将来使用

  • SDS还是二进制安全的,支持C字符串函数


链表
  • 链表提供了高效的节点重排能力,以及顺序性的节点访问方式,并且可以通过增删节点来灵活的调整链表的长度。因为Redis使用的C语言并没有内置这种数据结构,所以Redis构建了自己的链表实现。链表在Redis中的应用非常广泛,比如列表键的底层实现之一就是链表,当一个列表键包含了数量比较多的元素,又或者列表中包含的元素都是比较长的字符串时,Redis就会使用链表作为列表键的底层实现。

  • integers列表键的底层实现就是一个链表,链表中的每个节点都保存了一个整数值。除了链表键之外,发布与订阅,慢查询,监视器等功能也用到了链表,Redis服务器本身还是用链表来保存多个客户端的状态信息,以及使用链表来构建客户端的输出缓冲区。

这里写图片描述

typeof struct list {
    //表头借点
    listNode *head
    //表尾节点
    listNode *tail
    //链表所包含的节点数量
    unsigned long len
    //节点值复制函数
    void *(*dup)(void *ptr);
    //节点值释放函数
    void (*free)(void *ptr);
    //节点值对比函数
    void (*match)(void *ptr,void *key)
} list

字典

  • 字典,又称为符号表,关联数组或映射,是一种用于保存键值对的抽象数据结构,Redis的数据库就是用使用字典作为底层实现的。除了用来表示数据库之外,字典还是哈希键的底层实现之一,当一个哈希键包含的键值对比较多,又或者键值对中的元素都是比较长的字符串时。Redis就会使用字典作为哈希键的底层实现。举个例子:
a={}
a{
    users:{name:'bugall',name:'yifang'}
}
其中'users'可以理解为上稳重的哈希键,键值对就是'users'对应的内容。
  • Redis的字典使用哈希表作为底层实现,一个哈希表里面可以有多个哈希表节点,而每个哈希表节点就保存了字典中的一个键值对。
    这里写图片描述
typedef struct dictht{
    //哈希表数组
    dictEntry **table;
    //哈希表大小
    unsigned long size;
    //哈希表大小掩码,用于计算索引值
    //总是等于size-1
    unsigned long sizemaske;
    //该哈希表已有节点的数量
    unsigned long used;
} dictht;

哈希表节点:
typedef struct dictEntry{
    //键
    void *key
    //值
    union{
        void *val;
        uint64_tu64;
        int64_ts64; 
    } v;
    struct dictEntry *next;
} dictEntry;

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

字典:
typedef struct dict {
    //类型特定函数
    dictType *type;
    //私有数据
    void *privdata;
    //哈希表
    dictht ht[2];
    //rehash索引
    in trehashidx;
} dict;

整数集合

  • 整数集合是集合键的底层实现之一,当一个集合只包含整数数值元素,并且这个集合的元素数量不多时,Redis就会使用整数集合作为集合键的底层实现。举个例子,如果我们创建一个只包含5个元素的集合键,并且集合中的所有元素都是整数值,那么这个集合键的底层实现就会是整数集合
typedef struct intset{
    //编码方式
    uint32_t encoding;
    //集合包含的元素数量
    uint32_t length;
    //保存元素的数组
    int8_t contents[];
} intset;
contents数组是整数集合的底层实现:整数集合的每个元素都是contents数组中的任意一个item(元素),各个项在数组中按值的大小从小到大有序的排列
length属性记录了整数集合包含的元素数量,也是contents数组的长度。
虽然intset结构将contents属性声明为int8_t类型的数则,但实际上contents数组并不保存任何int8_t类型的值,contents数组的真正类型取决于encoding属性的值。这也是整数集合的特点之一,动态的更改contents的大小
  • 升级
    每当我们要将一个新元素添加到整数集合里面,并且新元素的类型比整数集合现有所有元素 的类型要长时,整数集合需要先进行升级,然后才能将新元素添加到整数集合里面
    1.根据新元素的类型,扩展整数集合底层数组的空间大小,并为新元素分配空间。
    2.将底层数组现有的所有元素都转换成与新元素相同的类型,并将类型转换后的元素放置到正确的位置上,而且在放置元素的过程中,需要继续维持底层数组的有序性质不变。
    3.将新元素放到底层数组里面

注意整数集合不支持降级操作


压缩列表

  • 压缩列表是列表键和哈希键的底层实现之一。当一个列表键只包含少量列表项,并且每个列表项要么就是小整数值,要么就是长度比较短的字符串,那么Redis就会使用压缩列表来做列表键的底层实现。

  • 另外,当一个哈希键只包含少量键值对,并且每个键值对的键和值要么就是小整数值,要么就是长度比较短的字符串,那么Redis就会使用压缩列表来做哈希键的底层实现。

  • 压缩列表是Redis为了节约内存而开发的,是由一系列特殊编码的连续内存块组成的顺序型数据结构,一个压缩列表可以包含任意多个节点,每个节点可以保存一个字节数组或者一个整数值

  • 每个压缩列节点可以保存一个字节数组或是一个整数值,每个压缩列表节点都由previous_entry_length,econding,content三个部分组成
    1.previous_entry_length记录了压缩列表中前一个节点的长度。因为节点的previous_entry_length属性记录了前一个节点的长度,所以程序可以通过指针运算,根据当前节点的起始地址来计算出前一个节点的起始地址。
    2.encoding属性记录了节点的content属性所保存数据的类型以及长度
    3.content属性负责保存节点的值,节点值可以是一个字节数组或者整数,值的类型和长度由节点的encoding属性决定

属性类型字节长度用途
zlbytesuint32_t4记录整个压缩列表占用的内存字节数:在对压缩列表进行内存重分配,或者计算zlend的位置时使用
zltailuint32_t4记录压缩列表尾节点距离压缩列表的起始地址有多少字节:通过这个偏移量,程序无需遍历真个压缩列表就可以确定表尾节点的地址
zllenuint16_t2记录了压缩列表包含的节点数量:当这个属性的值小雨uint16_max时,这个属性的值就是压缩列表包含节点的数量;当这个值等于uint16_max时,节点的真实数量需要遍历整个压缩列表才能计算得出
entryX列表节点不定压缩列表包含的各个节点,节点的长度由节点保存的内容决定
zlenduint8_t1特殊值0xFF(十进制255),用于标记压缩列表的末端
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值