Redis底层数据结构及基本类型的实现

Redis底层数据结构及基本类型的实现

底层数据结构

简单动态字符串(SDS)

数据结构

redis中用一种名为简单动态字符串(simple dynamic string)的抽象类型作为默认字符串表示,如在命令set name sher 中,在redis数据中创建了新的键值对 name-sher ,而在底层就是存储了保存着name的SDS和sher的SDS,除了保存redis数据库中的字符串值外,SDS还用作缓冲区(AOF持久化中的AOF缓冲区、客户端中的输入缓冲区都是由SDS实现),SDS底层是这样的数据结构:

struct sdshdr {
   
    
    // buf 中已占用空间的长度 等于SDS所保存的字符串长度
    int len;

    // buf 中剩余可用空间的长度
    int free;

    // 数据空间
    char buf[];
};

也就是说与传统的字符串相比,SDS由于在len属性中记录了其存储的字符串长度,所以获取字符串长度时不需要遍历整个字符串,而是直接获取其len属性即可,所以获取SDS存储的字符串的长度时间复杂度为O(1)(而获取C字符串的长度时间复杂度为O(n)),而在SDD中也是由\0作为字符串的结束:
在这里插入图片描述
在SDS中,存储数据的buf数组不一定是字符的数量加一(加一为\0),而是还包括未使用的字节,这些字节的长度由free属性记录。

C字符串的问题以及SDS的解决方案

C字符中出现的问题:

由于C字符串中不记录自己的长度,所以会出现内存溢出和内存泄漏:

  • 如果将C字符串后面进行拼接操作,如果没有提前分配好内存,很可能就会将原字符串地址后存储的数据进行覆盖,造成内存溢出
  • 如果将C字符串进行缩短操作,原字符串后面不再使用的空间如果没有进行释放,就会造成内存泄漏

SDS中的解决方案:

  • 空间预分配:该策略用于优化字符串的增长操作,其策略用伪代码表示为:

    if (len < 0.5MB) 
    	buf.length = buf.length + len
    else 
    	buf.length = buf.length + 1MB
    

    也就是如果对SDS进行修改之后,其长度若小于1MB,则分配给和len属性同样大小的未使用空间,(分配后SDS中len属性的值和free值相同);如果对SDS进行修改后,其长度将大于1MB,则分配个1MB的空间。也就是在扩展SDS的buf数组前,会先检查未使用空间是否够用,够用的话就不会执行内存分配了,这样就减少了内存分配的次数。

  • 惰性空间释放:该策略用于优化字符串的缩短操作,当要缩短SDS字符串时,并不会立即回收缩短后多出来的字节,而是将这些空出来的字节用free属性记录下来,并等待以后使用。

链表(linkedlist)

数据结构

Redis基本类型之一的链表键的实现方式之一就是链表这种数据结构,除了用来作为链表键的底层数据结构外,链表还用作**”发布与订阅“、”慢查询“、”监视器“等功能的实现,在Redis中的链表是一种双向链表**,首先看下链表底层实现的整体结构:
在这里插入图片描述
然后看其源码数据结构定义如下:

/*
 * 双端链表节点
 */
typedef struct listNode {
   
    
    struct listNode *prev; // 前置节点

    struct listNode *next; // 后置节点
    
    void *value; // 节点的值

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

} list;
特点

通过上面的图例以及数据结构的源码我们可以知道Redis中的双端链表具有以下性质:

  • 无环,表头节点的prev指针和表尾节点的next指针都指向null,链表的访问以null为终点;
  • 带表头指针和表尾指针(list结构中定义),获取头结点和尾结点的时间复杂度为O(1);
  • 带链表长度计数器(list结构中的len字段),获取链表节点数量的时间复杂度为O(1);
  • 由于链表的节点存放value字段为一个指针,所以链表实际可以存储各种数据,即数据多态性

字典(dict)

数据结构

字典(映射、符号表、关联数组)就是一种保存键值对key-value的抽象数据结构,在Redis中其应用较为广泛,比如Redis中的数据库就是使用字典来作为底层实现的,因为Redis中的每条记录本身就是个键值对;除了用字典表示数据库外,当一个哈希键key包含的键值对field-value较多时,Redis也会使用字典作为hash的底层实现。首先我们看在普通状态下的字典结构如下:
在这里插入图片描述
我们发现在字典中存在两个哈希表,通常情况下,我们只会用ht[0]这个哈希表,那么ht[1]是用来做什么的?ht[1]哈希表只会在对ht[0]哈希表进行rehash时使用。而在哈希表中存放了哈希表节点的数组,而哈希表节点的数组存放了每个哈希表节点,在哈希表节点中就是真正的键值对数据,这几个数据结构在Redis底层源码实现如下:

/*
 * 字典
 */
typedef struct dict {
   
    dictType *type;// 类型特定函数,如计算哈希值,复制、对比键值的函数等
    void *privdata;// 私有数据
    
    dictht ht[2];// 哈希表
    
    int rehashidx; /* 当 rehash 不在进行时,值为 -1 */

} dict;

对于字典,typeprivdata属性是为创建多态字典有关的,这里不详细进行介绍,而**dictth ht[2]字段恰恰就是存储了用来存储键值对数据的哈希表和用来rehash的哈希表**,rehashidx字段在注释中已经说明其用途。

/*
 * 哈希表
 * 每个字典都使用两个哈希表,从而实现渐进式 rehash 。
 */
typedef struct dictht {
   
    
    dictEntry **table;// 哈希表数组
   
    unsigned long size; // 哈希表大小
    
    unsigned long sizemask; // 哈希表大小掩码,用于计算索引值,总是等于 size - 1
    
    unsigned long used;// 该哈希表已有节点的数量

} dictht;

对于哈希表,其sizemask和哈希表节点计算出的哈希值共同决定了一个键应该被放到table数组的哪个索引上。

/*
 * 哈希表节点
 */
typedef struct dictEntry {
   
  
    void *key;  // 键
 
    union {
      // 值为以下三种结构的之一
        void *val;
        uint64_t u64;
        int64_t s64;
    } v;

    struct dictEntry *next; // 指向下个哈希表节点,形成链表,解决哈希冲突

} dictEntry;

阅读其源码可知,在Redis中,键值对的值可以是一个指针,也可以是一个uint64_t整数,也可以是一个int64_t整数,而在哈希表节点中有一个**next指针,只是为了将多个哈希值相同的键值对连接在一起,以此来解决哈希冲突问题**。

Redis中采用的哈希算法

当将一个新值添加到字典结构中时,程序根据要加入键值对的键计算出哈希值和索引值,然后再根据索引值,将新的哈希表节点(即要加入的键值对)放到哈希表数组ht[0]ht[1],根据是否在rehash)指定索引上。即如下的计算哈希值和索引值的方法:

# 使用字典type字段存储的计算哈希值的函数计算出哈希值
hash = dict->type->hashFunction(key); //作为hash键底层时,用MurmurHash2算法

# 使用哈希表中的sizemask属性和哈希值,计算出索引值
index 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值