Redis底层数据结构.md

1.Redis 概述

Redis 数据库里面的每个键值对(key-value) 都是由对象(object)组成的:
数据库键总是一个字符串对象(string object);
数据库的值则可以是字符串对象、列表对象(list)、哈希对象(hash)、集合对象(set)、有序集合(sort set)对象这五种对象中的其中一种。

2.Redis 底层数据结构

有以下数据类型:
简单动态字符串(SDS), 链表, 字典, 跳跃表, 整数集合, 压缩列表

2.1 SDS

Redis构建了一种名为简单动态字符串的抽象类型,作为默认字符串表示
结构:

/*  
 * 保存字符串对象的结构  
 */  
struct sdshdr {  
    // buf 中已占用空间的长度  
    int len;  
  
    // buf 中剩余可用空间的长度  
    int free;  
  
    // 数据空间  
    char buf[];  
};

SDS 与C字符串的区别:

1.获取字符串长度(SDS O(1)/C 字符串 O(n))
传统的C 字符串 使用长度为N+1 的字符串数组来表示长度为N 的字符串,所以为了获取一个长度为C字符串的长度,必须遍历整个字符串。
和C 字符串不同,SDS 的数据结构中,有专门用于保存字符串长度的变量,我们可以通过获取len 属性的值,直接知道字符串长度

2.杜绝缓冲区溢出
C 字符串 不记录字符串长度,除了获取的时候复杂度高以外,还容易导致缓冲区溢出。
假设程序中有两个在内存中紧邻着的 字符串 s1 和 s2,其中s1 保存了字符串“redis”,二s2 则保存了字符串“MongoDb”。将s1 的内容修改为redis cluster,但是又忘了重新为s1 分配足够的空间。那么s2原来的内容会被覆盖掉。
Redis 中SDS 的空间分配策略完全杜绝了发生缓冲区溢出的可能性:
当我们需要对一个SDS 进行修改的时候,redis 会在执行拼接操作之前,预先检查给定SDS 空间是否足够,如果不够,会先拓展SDS 的空间,然后再执行拼接操作。

3.减少修改字符串时带来的内存重分配次数   
C语言字符串在进行字符串的扩充和收缩的时候,都会面临着内存空间的重新分配问题。
SDS在拓展时会进行预分配策略, 通过这种预分配策略,SDS将连续增长N次字符串所需的内存重分配次数从必定N次降低为最多N次。

4.惰性释放空间
SDS有free属性可以记录剩余空间的,当对字符串进行收缩的时候,redis只记录free的值,避免下次修改时,对字符串空间进行拓展。
SDS提供了相应的API,在需要的时候,自行释放SDS的空余空间。

5.二进制安全
C 字符串中的字符必须符合某种编码,并且除了字符串的末尾之外,字符串里面不能包含空字符,否则最先被程序读入的空字符将被误认为是字符串结尾,这些限制使得C字符串只能保存文本数据,而不能保存想图片,音频,视频,压缩文件这样的二进制数据。
但是在Redis中,不是靠空字符来判断字符串的结束的,而是通过len这个属性。那么,即便是中间出现了空字符对于SDS来说,读取该字符仍然是可以的。

3.链表

链表提供了高效的节点重排能力,以及顺序性的节点访问方式,并且可以通过增删节点来灵活地调整链表的长度。
链表在Redis 中的应用非常广泛,比如列表键的底层实现之一就是链表。当一个列表键包含了数量较多的元素,又或者列表中包含的元素都是比较长的字符串时,Redis 就会使用链表作为列表键的底层实现。
1.链表的数据结构ListNode(双向链表):

typedef struct listNode{
      struct listNode *prev;
      struct listNode * next;
      void * value;  
}

一般用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);
}

3.链表的特性
双端:链表节点带有prev 和next 指针,获取某个节点的前置节点和后置节点的时间复杂度都是O(N)
无环:表头节点的 prev 指针和表尾节点的next 都指向NULL,对立案表的访问时以NULL为截止
表头和表尾:因为链表带有head指针和tail 指针,程序获取链表头结点和尾节点的时间复杂度为O(1)
长度计数器:链表中存有记录链表长度的属性 len
多态:链表节点使用 void* 指针来保存节点值,并且可以通过list 结构的dup 、 free、 match三个属性为节点值设置类型特定函数。

4.字典

是一种用于保存键值对的抽象数据结构。 
在字典中,一个键(key)可以和一个值(value)进行关联,字典中的每个键都是独一无二的。在C语言中,并没有这种数据结构,但是Redis 中构建了自己的字典实现。

1.字典的定义

# 1.哈希表
typedef struct dictht {
   //哈希表数组
   dictEntry **table;
   //哈希表大小
   unsigned long size;

   //哈希表大小掩码,用于计算索引值
   unsigned long sizemask;
   //该哈希表已有节点的数量
   unsigned long used;
}
# 2.哈希表节点
typeof struct dictEntry{
   //键
   void *key;
   //值
   union{
      void *val;
      uint64_tu64;
      int64_ts64;
   }
   struct dictEntry *next;
}
# 3.字典
typedef struct dict {
    // 类型特定函数
    dictType *type;
    // 私有数据
    void *privedata;
    // 哈希表
    dictht ht[2];
    // rehash 索引
    in trehashidx;

}

1.哈希表 我们可以看到,在结构中存有指向dictEntry 数组的指针,而我们用来存储数据的空间既是dictEntry
2.哈希表节点 在数据结构中,我们清楚key 是唯一的,但是我们存入里面的key 并不是直接的字符串,而是一个hash 值,通过hash 算法,将字符串转换成对应的hash 值,然后在dictEntry 中找到对应的位置。
这时候我们会发现一个问题,如果出现hash 值相同的情况怎么办?Redis 采用了链地址法(类比于HashMap中的桶):
在这里插入图片描述

3.字典 type 属性 和privdata 属性是针对不同类型的键值对,为创建多态字典而设置的。
ht 属性是一个包含两个项(两个哈希表)的数组
在这里插入图片描述

2.Rehash

随着对哈希表的不断操作,哈希表保存的键值对会逐渐的发生改变,为了让哈希表的负载因子维持在一个合理的范围之内,我们需要对哈希表的大小进行相应的扩展或者压缩,这时候,我们可以通过 rehash(重新散列)操作来完成

5.跳表

跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。跳跃表是一种随机化的数据,跳跃表以有序的方式在层次化的链表中保存元素,效率和平衡树媲美 ——查找、删除、添加等操作都可以在对数期望时间下完成,并且比起平衡树来说,跳跃表的实现要简单直观得多。

Redis 只在两个地方用到了跳跃表,一个是实现有序集合键,另外一个是在集群节点中用作内部数据结构。
Redis 的跳跃表 主要由两部分组成:zskiplist(链表)和zskiplistNode (节点)

typedef struct zskiplistNode{
   //层
     struct zskiplistLevel{
     //前进指针
        struct zskiplistNode *forward;
    //跨度
        unsigned int span;
    } level[];
  //后退指针
    struct zskiplistNode *backward;
  //分值
    double score;
  //成员对象
    robj *obj;
}
typedef struct zskiplist {
     //表头节点和表尾节点
     structz skiplistNode *header,*tail;
     //表中节点数量
     unsigned long length;
     //表中层数最大的节点的层数
     int level;
}zskiplist;

1、层:level 数组可以包含多个元素,每个元素都包含一个指向其他节点的指针。
2、前进指针:用于指向表尾方向的前进指针
3、跨度:用于记录两个节点之间的距离
4、后退指针:用于从表尾向表头方向访问节点
5、分值和成员:跳跃表中的所有节点都按分值从小到大排序。成员对象指向一个字符串,这个字符串对象保存着一个SDS值
在这里插入图片描述

从结构图中我们可以清晰的看到,header,tail分别指向跳跃表的头结点和尾节点。level 用于记录最大的层数,length 用于记录我们的节点数量。
总结:
 - 跳跃表是有序集合的底层实现之一

  • 主要有zskiplist 和zskiplistNode两个结构组成
  • 每个跳跃表节点的层高都是1至32之间的随机数
  • 在同一个跳跃表中,多个节点可以包含相同的分值,但每个节点的对象必须是唯一的
  • 节点按照分值的大小从大到小排序,如果分值相同,则按成员对象大小排序

6.整数集合(Intset)

其实就是一个特殊的集合,里面存储的数据只能够是整数,并且数据量不能过大。

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

}

整数集合的升级

在上述数据结构图中我们可以看到,intset 在默认情况下会帮我们设定整数集合中的编码方式,但是当我们存入的整数不符合整数集合中的编码格式时,就需要使用到Redis 中的升级策略来解决
Intset 中升级整数集合并添加新元素共分为三步进行:
1、根据新元素的类型,扩展整数集合底层数组的空间大小,并为新元素分配空间
2、将底层数组现有的所有元素都转换成新的编码格式,重新分配空间
3、将新元素加入到底层数组中

总结

  • 整数集合的底层实现为数组,这个数组以有序,无重复的范式保存集合元素,在有需要时,程序会根据新添加的元素类型改变这个数组的类型
  • 升级操作为整数集合带来了操作上的灵活性,并且尽可能地节约了内存
  • 整数集合只支持升级操作,不支持降级操作

7.压缩列表

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

参考:http://www.cnblogs.com/jaycekon/p/6227442.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值