Redis深入解析之数据结构

Redis数据库中的每个键值对都是由对象组成,键总是一个字符串对象(string object),而值可以由字符串对象、列表对象(list object)、哈希对象(hash object)、集合对象(set object)和有序集合对象(sorted set object)这五种。下面分别介绍每一种。

一、简单动态字符串

1、定义

从Redis3.2开始,sds就有了5种类型,5种类型分别存放不同大小的字符串。其定义在sds.h文件中华,定义如下
typedef char *sds;

 //sdshdr5在源码中没有用到
struct __attribute__ ((__packed__)) sdshdr5 {
    unsigned char flags; /* 低三位为type, 高五位为字符串长度 */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; /* 已经使用长度 */
    uint8_t alloc; /* 分配的字符串长度,除头部和结尾的空字符 */
    unsigned char flags; /* 低三位为type, 高五位为字符串长度 */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
    uint16_t len; 
    uint16_t alloc;
    unsigned char flags; 
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
    uint32_t len; 
    uint32_t alloc; 
    unsigned char flags;
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
    uint64_t len;
    uint64_t alloc;
    unsigned char flags;
    char buf[];
};

2、SDS和C语言字符串的区别和改进

2.1 获取字符串长度的复杂度是常数级的

C字符串获取长度的时候需要遍历整个字符串,此操作的复杂度为O(N)。而SDS记录了字符串的长度len。在设置和更新字符串的时候,自动更新此长度。

2.2 避免了缓冲区溢出

C字符串在拼接的时候,要保证拼接后的字符串已分配足够的长度,不然会造成缓冲区溢出。而SDS在修改字符串时先根据alloc判断空间是否满足修改所需,避免了缓冲区溢出。如果空间不够,SDS会先扩展空间。

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

C字符串每次修改和删除都会进行一次内存重新分配。SDS通过空间预分配、惰性空间释放等措施减少内存分配。因为内存分配是非常耗时的过程。

2.4 二进制安全

C字符串必须符合某种编码,并且中间不能有空格,所以不能保存图片、音频等二进制的数据。Redis的buf数组是用来保存二进制数据的,所以没有这些限制。SDS是通过len来判断字符串是否结束,而不是通过空格。

二、链表

1、定义

链表在实际开发中的使用非常广泛,在Redis中也是,比如列表键、发布与订阅、慢查询、监视器,以及服务器本身。链表的定义在adlist.h中,定义如下:
//链表节点定义
typedef struct listNode {
    //前节点
    struct listNode *prev;
    //后节点
    struct listNode *next;
    //节点值
    void *value;
} listNode;
//链表迭代器
typedef struct listIter {
    //链表下一节点
    listNode *next;
    //迭代方向
    int direction;
} listIter;
//双端链表
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;

2、Redis链表特性

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

三、字典

字典,又称为符号表(symbol table)、关联数组(associative array)或映射(map),是一种用于保存键值对(key-value pair)的抽象数据结构。
字典在Redis中的应用相当广泛,比如Redis的数据库就是使用字典来作为底层实现的,对数据库的增、删、查、改操作也是构建在对字典的操作之上的。除了用来表示数据库之外,字典还是哈希键的底层实现之一,当一个哈希键包含的键值对比较多,又或者键值对中的元素都是比较长的字符串时,Redis就会使用字典作为哈希键的底层实现。

1、定义

Redis的字典使用哈希表作为底层实现,一个哈希表里面可以有多个哈希表节点,而每个哈希表节点就保存了字典中的一个键值对。字典的定义在dict.h文件中,定义如下:

1.1、哈希表节点

//哈希表节点
typedef struct dictEntry {
    //哈希键
    void *key;
    //哈希值
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    //指向下一个哈希表节点
    struct dictEntry *next;
} dictEntry;
key属性保存着键值对中的键,而v属性则保存着键值对中的值,其中键值对的值可以是一个指针,或者是一个uint64_t整数,又或者是一个int64_t整数。next属性是指向另一个哈希表节点的指针,这个指针可以将多个哈希值相同的键值对连接在一次,以此来解决键冲突(collision)的问题。

1.2 类型特定函数

//类型特定函数
typedef struct dictType {
    //计算哈希值函数
    uint64_t (*hashFunction)(const void *key);
    //复制哈希键函数
    void *(*keyDup)(void *privdata, const void *key);
    //复制哈希值函数
    void *(*valDup)(void *privdata, const void *obj);
    //哈希键对比函数
    int (*keyCompare)(void *privdata, const void *key1, const void *key2);
    //销毁哈希键函数
    void (*keyDestructor)(void *privdata, void *key);
    //销毁哈希值函数
    void (*valDestructor)(void *privdata, void *obj);
} dictType;
每个dictType结构保存了一簇用于操作特定类型键值对的函数,Redis会为用途不同的字典设置不同的类型特定函数。

1.3 哈希表

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

1.4 字典

//Redis中的字典定义
typedef struct dict {
    //类型特定函数,是dictType类型
    dictType *type;
    //私有数据
    void *privdata;
    //哈希表
    dictht ht[2];
    //rehash索引,当rehash不在进行时,值为-1
    long rehashidx; /* rehashing not in progress if rehashidx == -1 */
    //当前迭代器的数量
    unsigned long iterators; /* number of iterators currently running */
} dict;
type属性和privdata属性是针对不同类型的键值对,为创建多态字典而设置的:
type属性是一个指向dictType结构的指针,每个dictType结构保存了一簇用于操作特定类型键值对的函数,Redis会为用途不同的字典设置不同的类型特定函数。
而privdata属性则保存了需要传给那些类型特定函数的可选参数。

1.5 字典的迭代器

 //如果迭代器的数量设置为1是安全的,意味着可以调用dictAdd, dictFind等方法。否则
 //是不安全的迭代器,只能调用dictNext()方法
typedef struct dictIterator {
    dict *d;
    long index;
    int table, safe;
    dictEntry *entry, *nextEntry;
    //滥用fingerprint是非安全的迭代
    long long fingerprint;
} dictIterator;

哈希算法

你可能感兴趣的:(redis,数据库,nosql)