REDIS(一)-数据结构与对象

本文深入解析Redis中的数据结构,包括简单动态字符串(SDS)、链表、字典、跳跃表、整数集合、压缩列表及对象编码。探讨各数据结构的特点、应用场景与内部实现机制,如SDS的二进制安全性和链表的高效节点重排。

image-20200802164149419

第2章 简单动态字符串

Redis并没有直接使用C语言传统的字符串表示(以空字符结尾的字符数组),而是自己构件了一种名为简单动态字符串(SDS)的抽象类型,并将SDS用作redis的默认字符串表示

当redis需要的不仅仅是一个字符串字面量,而是一个可以被修改的字符串值时,redis就会使用sds来表示字符串值,比如在redis的数据库里面,包含字符串值的键值对在底层都是由SDS实现的。

除了用来保存数据库中的字符串值之外,SDS还被用作缓冲区(buffer):AOF模块中的AOF缓冲区,以及客户端状态中的输入缓冲区,都是由SDS实现的。

2.1 SDS的定义

SDS的定义:

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

2.2 SDS与C字符串的区别

  1. 常熟复杂度获取字符串长度

  2. 杜绝缓冲区溢出

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

    1. 空间预分配

    2. 惰性空间释放

      减少长度时先用free存储。

  4. 二进制安全

    SDS采用len属性的值而不是空字符来判断字符串是否结束

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

image-20200716213014926

2.3 SDS API

image-20200716213331296

第3章 链表(List)

链表提供了高效的节点重排能力,以及顺序性的节点访问方式,并且可以通过增删节点来灵活地调整链表的长度。

list结构为链表提供了表头指针head,表尾指针tail,以及链表长度计数器len,而dup,free

和match成员则是用于实现多态链表所需的类型特定函数:

  • dup函数用于复制链表节点所保存的值
  • free函数用于释放链表节点所保存的值
  • match函数则用于对比链表节点所保存的值和另一个输入值是否相等

且每个节点都有上个节点的指针和下一个结点的指针

redis的链表实现的特性可以总结如下:

  • 双端:链表节点带有prev和next指针,获取某个节点的前置节点和后置节点的复杂度都是O(1)
  • 无环:表头节点的prev指针和表尾节点的next指针都指向NULL,对链表的访问以NULL结束
  • 带表头指针和表尾指针:通过list结构的head指针和tail指针,程序获取链表的表头和表尾节点的复杂度为O(1)
  • 带链表长度计数器:程序使用list结构的len属性来对list持有的链表节点进行计数,程序获取链表中节点数量的复杂度为O(1)
  • 多态:链表节点使用void*指针来保存节点值,并且可以通过list结构的dup,free,match三个属性为节点值设置类型特定函数,所以链表可以用于保存各种不同类型的值。

第4章 字典

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

字典中的每个键都是独一无二的。

4.1 字典的实现

4.1.1 哈希表

结构定义:

typedef struct dictht{
    // 哈希表数组
    dictEntry **table;
    // 哈希表大小
    unsigned long size;
    // 哈希表大小掩码,用于计算索引值
    // 总是等于size - 1
    unsigned long sizemask;
    // 该哈希表已有节点的数量
    unsigned long used;
}dictht;

table是一个数组,数组中的每个元素都是一个指向dict.h/dictEntry结构的指针,每个dictEntry结构保存着一个键值对。

image-20200717145152415

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

image-20200717145721352

4.1.3 字典

redis中的字典由dict.h/dict结构表示:

typedef struct dict{
    // 类型特定函数
    dictType *type;
    // 私有数据
    void *privdata;
    // 哈希表
    dictht ht[2];
    // rehash索引
    // 当rehash不在进行时,值为-1
    int trehashidx;	/* rehashing not in progress if rehashidx == -1 */
}dict;

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

image-20200717151711481

4.2 哈希算法

当要将一个新的键值对添加到字典里面时,程序需要先根据键值对的键计算出哈希值和索引值,然后再根据索引值,将包含新键值对的哈希表节点放到哈希表数组的指定索引上面。

计算哈希的方法:hash = dict->type->hashFunction(k0);

计算索引的方法:index = hash & ht[0].sizemark;

4.3 解决键冲突

当有两个或以上数量的键被分配到了哈希表数组的同一个索引上面时,我们称这些键发生了冲突。

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

4.4 rehash

扩展和收缩哈希表的工作可以通过执行rehash操作来完成,redis对字典的哈希表执行rehash的步骤如下:

  1. 为字典的ht[1]哈希表分配空间,这个哈希表的空间大小取决于要执行的操作,以及ht[0]当前包含的键值对数量
    1. 如果执行的是扩展操作,那么ht[1]的大小为第一个大于等于ht[0].used * 2的2^n
    2. 如果执行的是收缩操作,那么ht[1]的大小为第一个大于等于ht[0].used 的 2^n
  2. 将保存在ht[0]中的所有键值对放置到ht[1]上面,rehahsh指的是重新计算键的哈希值和索引值
  3. 将ht[0]包含的所有键值对都迁移到了ht[1]之后(ht[0]变为空表),释放ht[0],将ht[1]设置为ht[0],并在ht[1]新创建一个空白哈希表,为下一次rehahs做准备

4.5 渐进式rehash

  1. 为ht[1]分配空间,让字典同时持有ht[0]和ht[1]两个哈希表。
  2. 在字典中维持一个索引计数器变量rehashidx,并将它的值设置为0,表示rehash工作正式开始。
  3. 在rehash进行期间,每次对字典执行添加、删除、查找或者更新操作时,程序除了执行指定的操作以外,还会顺带将ht[0]哈希表在rehashidx索引上的所有键值对rehash到ht[1],当rehash工作完成之后,程序将rehashidx属性的值增1。
  4. 随着字典操作的不断执行,最终在某个时间点上,ht[0] 的所有键值对都会被rehash至ht[1],这时程序将rehashidx属性的值设为-1,表示rehash操作已完成。

第5章 跳跃表

跳跃表是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的

5.1 跳跃表的实现

image-20200717192633617

header:指向跳跃表的表头节点

tail:指向跳跃表的表尾节点

level:记录目前跳跃表内,层数最大的那个节点的层数

length:记录跳跃表的长度,也即是,跳跃表目前包含节点的数量

5.1.1 跳跃表节点

定义:

typedef struct zskiplistNode{
	// 层
	struct zskipListLevel{
        // 前进指针
    	struct zskiplistNode *forward;
        // 跨度
    	unsigned int span;
	}level[];

    // 后退指针
	struct zskiplistNode *backward;
    // 分值
	double score;
	// 成员对象
    robj *obj;
}
  1. 跳跃节点的level数组可以包含多个元素,每个元素都包含一个指向其他节点的指针,程序可以通过这些层来加快访问其他节点的速度,一般来说,层的数量越多,访问其他节点的速度就越快。

    每次创建一个新跳跃节点的时候,程序都根据幂次定律随机生成一个介于1和32之间的值作为level数组的大小,这个大小就是层的高度。

  2. 前进指针

    每个层都有一个指向表尾方向的前进指针,用于从表头向表尾方向访问节点。

  3. 跨度

    层的跨度用于记录两个节点之间的距离:

    • 两个节点之间的跨度越大,他们相距的就越远
    • 指向NULL的所有前进指针的跨度都为0,因为他们没有连向任何节点
  4. 后退指针

    节点的后退指针用于从表尾向表头方向访问节点:跟可以一次通过多个节点的前进指针不同,因为每个节点只有一个后退指针,所以每次只能后退至前一个节点。

  5. 分值和成员

    节点的分支是一个double类型的浮点数,跳跃表中的所有节点按分值从小到大来排序

    节点的成员对象是一个指针,它指向一个字符串对象,而字符串对象则保存着一个SDS值

5.1.2 跳跃表

结构

typedef struct zskiplist{
    // 表头节点和表尾节点
    struct zskiplistNode *header, *tail;
    // 表中节点的数量
    unsigned long length;
    // 表中层数最大的节点的层数
    int level;
}zskiplist;

第6章 整数集合

整数集合(intset)是集合键的底层实现之一,当一个集合只包含整数值元素,并且这个集合的元素数量不多时,Redis就会使用整数集合作为集合键的底层实现

6.1 整数集合的实现

整数集合是redis用于保存整数值的集合抽象数据结构,它可以保存类型为int16_t,int32_t或int64_t的整数值,并且保证集合中不会出现重复元素

定义:

typedef struct intset{
    // 编码方式
    uint32_t encoding;
    // 集合包含的元素数量
    uint32_t length;
    // 保存元素的数组
    int8_t contents[];
}intset;

contents数组是整数集合的底层实现:整数集合的每个元素都是contents数组的一个数组项(item),各个项在数组中按值的大小从小到大有序地排列,并且数组中不包含任何重复项

虽然contents属性声明为int8_t类型的数组,但实际上contents数组并不保存任何int8_t类型的值,contents数组的真正类型取决于encoding属性的值

6.2 升级

每当我们要将一个新元素添加到整数集合里面,并且新元素的类型比整数集合现有所有元素的类型都要长时,整数集合需要先进行升级,然后才能将新元素添加到整数集合里面

分三步:

  1. 根据新元素的类型,扩展整数集合底层数组的空间大小,并为新元素分配空间
  2. 将底层数组现有的所有元素都转换成语新元素相同的类型,并将类型转换后的元素放置到正确的位上,而且在放置元素的过程中,需要维持底层数组的有序性质不变
  3. 将新元素添加到底层数组里面

6.3 升级的好处

  1. 提升整数集合的灵活性
  2. 尽可能地节约内存
6.3.1 提升灵活性

C语言是静态类型语言,为了避免类型错误,我们通常不会把两种不同类型的值放在同一个数据结构里面

6.3.2 节约内存

6.4 降级

整数集合不支持降级操作。即时删除了最高级别的元素,依旧保持最高级别

第7章 压缩列表

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

7.1 压缩列表的构成

image-20200718160333032

image-20200718160419102

7.2 压缩列表节点的构成

每个压缩列表节点可以保存一个字节数组或者一个整数值,其中,字节数组可以是以下三种长度的其中一种:

  • 长度小于等于63字节的字节数组
  • 长度小于等于16383字节的字节数组
  • 长度小于等于4294967295字节的字节数组

而整数值则可以是以下六种长度的其中一种:

  • 4位长
  • 1字节长的有符号整数
  • 3字节长的有符号整数
  • int16_t类型整数
  • int32_t类型整数
  • int64_t类型整数

image-20200718183434712

7.2.1 previous_entry_length

节点的previous_entry_length属性以字节为单位,记录了压缩列表中的前一个节点的长度。previous_entry_length属性的长度可以是1字节或者5字节。

  • 如果前一个节点的长度小于254字节,那么previous_entry_length属性的长度为1字节:前一节点的长度就保存在这一个字节里面
  • 如果前一节点的长度大于等于254字节,那么previous_entry_length属性的长度为5字节,其中属性的第一字节会被设置为0xFE,而之后的四个字节则用于保存前一节点的长度

因为节点的previous_entry_length属性记录了前一个节点的长度,所以程序可以通过指针运算,根据当前节点的起始地址来计算出前一个节点的起始地址

7.2.2 encoding

节点的encoding属性记录了节点的content属性所保存数据的类型以及长度

  • 一字节,两字节或者五字节长,值的最高位为00,01或者10的是字节数组编码:这种编码表示节点的content属性保存着字节数组,数组的长度由编码除去最高两位之后的其他位记录
  • 一字节长,值的最高位以11开头的是整数编码,这种编码表示节点的content属性保存着整数值,整数值的类型和长度由编码除去最高两位之后的其他位记录

image-20200718190400759

7.2.3 content

节点的content属性负责保存节点的值,节点值可以是一个字节数组或者整数,值的类型和长度由节点的encoding属性决定.

image-20200718190614790

image-20200718190626702

7.3 连锁更新

第8章 对象

8.1 对象的类型与编码

redis中的每个对象都由一个redisObject结构表示:

typedef struct redisObject{
    // 类型
    unsigned type:4;
    // 编码
    unsigned encoding:4;
    // 指向底层实现数据结构的指针
    void *ptr;
    //***
}robj;
8.1.1 类型

对象的type属性记录了对象的类型,这个属性的值可以是表中列出的常量中的其中一个

对于redis数据库保存的键值对来说,键总是一个字符串对象,而值则可以是字符串对象,列表对象,哈希对象,集合对象或者有序集合对象的其中一种,

image-20200718191827121

使用type指令查看类型:type [键]

8.1.2 编码和底层实现

对象的ptr指针指向对象的底层实现数据结构,而这些数据结构由对象的encoding属性决定

encoding属性记录了对象所使用的编码,也即是说这个对象使用了什么数据结构作为对象的底层实现,这个属性的值可以是表中的其中一个:

image-20200718192029080

使用指令查看编码:OBJECT ENCODING [键]

通过encoding属性来设定对象所使用的编码,而不是为特定类型的对象关联一种固定的编码,极大地提升了redis的灵活性和效率,因为redis可以根据不同的使用场景来为一个对象设置不同的编码,从而优化对象在某一场景下的效率

8.2 字符串对象

字符串对象的编码可以是int,raw或者embstr

如果一个字符串对象保存的是整数值,并且这个整数值可以用long类型来表示,那么字符串对象会将整数值保存在字符串对象结构的ptr属性里面,并将字符串对象的编码设置为int

如果字符串对象保存的是一个字符串值,并且这个字符串值的长度大于32字节,那么字符串对象将使用一个简单动态字符串(SDS)来保存这个字符串值,并将对象的编码设置为raw

如果字符串对象保存的是一个字符串值,并且这个字符串值的长度小于等于32字节,那么字符串对象将使用embstr编码的方式来保存这个字符串值

在redis中,long double类型表示的浮点数也是作为字符串值来保存的,如果我们要保存一个浮点数到字符串对象里面,那么程序会先将这个浮点数转换成字符串值,然后再保存转换所得的字符串值

8.2.1 编码的转换

对于int编码的字符串对象来说,如果我们向对象执行了一些命令,使得这个对象保存的不再是整数值,而是一个字符串值,那么字符串对象的编码将从int变为raw

因为redis没有为embstr编码的字符串对象编写任何相应的修改程序,所以embstr编码的字符串对象实际上是只读的,当我们对embstr编码的字符串对象执行任何修改命令时,程序会先将对象的编码从embstr转换成raw,然后再执行修改命令。

8.2.2 字符串命令的实现

因为字符串键的值为字符串对象,所以用于字符串键的所有命令都是针对字符串对象来构建的

image-20200718195010822

8.3 列表对象

列表对象的编码可以是ziplist或者是linkedlist

ziplist编码的列表对象使用压缩列表作为底层实现,每个压缩列表节点保存了一个列表元素

image-20200718230817524

另一方面,linkedlist编码的列表对象使用双端链表作为底层实现,每个双端链表节点都保存了一个字符串对象,而每个字符串对象都保存了一个列表元素

image-20200718230926738

8.3.1 编码转换

当列表对象可以同时满足以下两个条件时,列表对象使用ziplist编码:

  • 列表对象保存的所有字符串元素的长度都小于64字节
  • 列表对象保存的元素数量小于512个

不能满足这两个条件的列表对象需要使用linkedlist编码

8.3.2 列表命令的实现

8.4 哈希对象(HSET)

哈希对象编码可以是ziplist或者hashtable

ziplist编码的哈希对象使用压缩列表作为底层实现,每当有新的键值对要加入到哈希对象时,程序会先将保存了键的压缩列表节点推入到压缩列表表尾,然后再将保存了值的压缩列表节点推入到压缩列表表尾。

因此:

  • 保存了同一键值对的两个节点总是紧挨在一起,保存键的节点在前,保存值的节点在后
  • 先添加到哈希对象中的键值对会被放在压缩列表的表头方向,而后来添加到哈希对象中的键值对会被放在压缩列表的表尾方向

image-20200718232416829

image-20200718232422555

另一方面,hashtable编码的哈希对象使用字典作为底层实现,哈希对象中的每个键值对都使用一个字典键值对来保存

  • 字典中的每个键都是一个字符串对象,对象中保存了键值对的键
  • 字典中的每个键都是一个字符串对象,对象中保存了键值对的值

image-20200718232530412

8.4.1 编码转换

当哈希对象可以同时满足以下两个条件时,哈希对象使用ziplist编码

  • 哈希对象保存的所有键值对的键和值的字符串长度都小于64字节
  • 哈希对象保存的键值对数量小于512个

不能满足这两个条件的哈希对象需要使用hashtable编码

8.5 集合对象

集合对象的编码可以是intset或者是hashtable

intset编码的集合对象使用整数集合作为底层实现,集合对象包含的所有元素都被保存在整数集合里面

hashtable编码的集合对象使用字典作为底层实现,字典的每个键都是一个字符串对象,每个字符串对象包含了一个集合元素,而字典的值则全部被设置为NULL

image-20200719102239735

8.5.1 编码的转换

当集合对象可以同时满足以下两个条件时,对象使用intset编码:

  • 集合对象保存的所有元素都是整数值
  • 集合对象保存的元素数量不超过512个

不能满足这两个条件的集合对象需要使用hashtable编码

8.6 有序集合对象

有序集合对象的编码可以是ziplist或者skiplist

ziplist编码的压缩列表对象使用压缩列表作为底层实现,每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员(member),而第二个元素则保存元素的分值(score)

压缩列表内的集合元素按分值从小到大进行排序,分值较小的元素被放置到靠近表头的方向,而分值较大的元素则被放置到靠近表尾的方向

image-20200719102727606

image-20200719102830326

skiplist编码的有序集合对象使用zset结构作为底层实现,一个zset结构同时包含一个字典和一个跳跃表

typedef struct zset{
    zskiplist *zsl;
    dict *dict;
}zset;

zset结构中的zsl跳跃表按分值从小到大保存了所有集合元素,每个跳跃表节点都是保存了一个集合元素:跳跃表节点的object属性保存了元素的成员,而跳跃表节点的score属性则保存了元素的分值,通过这个跳跃表,程序可以对有序集合进行范围型操作。

zset结构中的dict字典为有序集合创建了一个从成员到分值的映射,字典中的每个键值对都保存了一个集合元素:字典的键保存了元素的成员,而字典的值则保存了元素的分值。

有序集合每个元素的成员都是一个字符串对象,而每个元素的分值都是一个double类型的浮点数,虽然zset结构同时使用跳跃表和字典来保存有序集合元素,但这两种数据结构都会通过指针来共享相同元素的成员和分值,所以同时使用跳跃表和字典来保存集合元素不会产生任何重复成员或者分值。

8.6.1 编码的转换

当有序集合对象可以同时满足以下两个条件时,对象使用ziplist编码:

  • 有序结合保存的元素数量小于128个
  • 有序集合保存的所有元素成员的长度都小于64字节

8.7 类型检查与命令多态

评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值