redis学习1——数据结构和对象

redis学习

简单动态字符串

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

SDS兼容C语言标准字符串处理函数,且在此基础上保证了二进制安全。

什么是二进制安全?

通俗地讲,C语言中,用“\0”表示字符串的结束,如果字符串中本身就有“\0”字符,字符串就会被截断,即非二进制安全;若通过某种机制,保证读写字符串时不损害其内容,则是二进制安全。

3.2以前的SDS设计

如图所示:

struct sdshdr {
    unsigned int len;
    unsigned int free;
    char buf[];
};

在64位系统下,字段len和字段free各占4个字节,紧接着存放字符串。

这样设计有以下的几个优点:

  1. 有单独的统计变量len和free(称为头部)。可以很方便地得到字符串长度。(常数数量级获取字符串长度)
  2. 内容存放在柔性数组buf中,SDS对上层暴露的指针不是指向结构体SDS的指针,而是直接指向柔性数组buf的指针。上层可像读取C字符串一样读取SDS的内容,兼容C语言处理字符串的各种函数。(读取方便)
  3. 杜绝缓冲区溢出,C字符串在拼接的时候假定用户已经开辟了足够多的空间,如果用户没有开辟足够的空间,可能导致空间之后的不安全内存被使用
  4. 减少修改字符串时带来的内存重分配次数
  5. 由于有长度统计变量len的存在,读写字符串时不依赖“\0”终止符,保证了二进制安全。

柔性数组

buf[]是一个柔性数组。柔性数组成员(flexible array member),也叫伸缩性数组成员,只能被放在结构体的末尾。包含柔性数组成员的结构体,通过malloc函数为柔性数组动态分配内存。 之所以用柔性数组存放字符串,是因为柔性数组的地址和结构体是连续的,这样查找内存更快(因为不需要额外通过指针找到字符串的位置);可以很方便地通过柔性数组的首地址偏移得到结构体首地址,进而能很方便地获取其余变量。

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

C字符串每次增长或者缩短,程序都要队保存这个C字符串的数组进行一次内存重分配操作:

  • 如果拼接操作,程序在执行这个操作之前,程序需要先通过内存重分配来扩展底层数组的空间大小
  • 如果是缩短操作,需要手动释放不再使用的内存空间

redis简单字符串通过空间预分配和惰性删除解决这两个问题:

  • **空间预分配:**如果最终字符串长度小于1MB,分配len和free相等的未使用空间长度;如果修改以后,长度大于1MB,那么len为实际的长度,free为1MB
  • **惰性空间释放:**程序不立即使用内存重分配来回收缩短后多出来的字节,而实用free属性来标记不用的空间。

redis5.0的设计

考虑的问题当保存的字符串太短就显得头部很臃肿,因此设计了多个不同的SDS结构体来保存字符串

1. 长度小于32的短字符串
struct __attribute__ ((__packed__)) sdshdr5 {
    unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
    char buf[];
};

第一个字节flags作为头部,低三位是类型,高五位是字符串长度,可以用来表示0到31长度的字符串

2. 长度大于32的字符串

根据字符串长度,有以下四种不同的结构体

struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; /* used */
    uint8_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
    uint16_t len; /* used */
    uint16_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
    uint32_t len; /* used */
    uint32_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
    uint64_t len; /* used */
    uint64_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};

其中len即已用长度,alloc是分配的总长度,flags中,低三位是类型,高五位未使用,然后buf[]就是柔性数组

基本操作
  1. 创建字符串:首先计算好不同类型的头部和初始长度,然后动态分配内存。需要注意以下3点。
    1. 创建空字符串时,SDS_TYPE_5被强制转换为SDS_TYPE_8
    2. 长度计算时有“+1”操作,是为了算上结束符“\0”
    3. 返回值是指向sds结构buf字段的指针
  2. 释放字符串:通过对s的偏移,可定位到SDS结构体的首部,然后调用s_free释放内存
  3. 拼接字符串:如果不需要扩容,直接拼接,然后返回;如果需要扩容,扩容后进行拼接,然后返回。

链表

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;

感觉链表的实现不是很复杂

跳跃表

7130e668-b731-4f94-bc97-ff714837204b

嘿嘿嘿这就是个跳跃表,下次再详细讲结构,这边先总结它是怎么实现的

跳跃表具有以下几个概念术语:

  1. 层,每个节点都有一个层序数组,每层带有两个属性,前进指针和跨度。前进指针用于访问位于表尾方向的其他节点,而跨度则记录了前进指针和当前节点的距离
  2. 分值,跳变中节点以分值作为关键字来排序

跳跃表

typedef struct zskiplistNode {				//跳表节点
    robj *obj;								//成员对象
    double score;							//用来排序的分数
    struct zskiplistNode *backward;			//后向指针
    struct zskiplistLevel {					//跳表层级
        struct zskiplistNode *forward;		//前向指针
        unsigned int span;					//跨度
    } level[];
} zskiplistNode;

typedef struct zskiplist {					//跳表
    struct zskiplistNode *header, *tail;	//头尾节点
    unsigned long length;					//跳表长度
    int level;								//跳表高度
} zskiplist;

typedef struct zset {						//有序集合
    dict *dict;
    zskiplist *zsl;
} zset;

跳跃表操作

创建跳跃表
  1. 创建跳跃表结构体对象zsl。
  2. 将zsl的头节点指针指向新创建的头节点。
  3. 跳跃表层高初始化为1,长度初始化为0,尾节点指向NULL。

头节点是一个特殊的节点,不存储有序集合的member信息。头节点是跳跃表中第一个插入的节点,其level数组的每项forward都为NULL,span值都为0。

创建节点
  1. 生成层高:节点层高的最小值为1,最大值是ZSKIPLIST_MAXLEVEL,Redis5中节点层高的值为64。Redis通过zslRandomLevel函数随机生成一个1~64的值,作为新建节点的高度,值越大出现的概率越低。节点层高确定之后便不会再修改。
  2. 创建节点:跳跃表的每个节点都是有序集合的一个元素,在创建跳跃表节点时,待创建节点的层高()、分值、member等都已确定。对于跳跃表的每个节点,我们需要申请内存来存储。
插入节点
  1. 查找要插入的位置

    x = zsl->header;
    for (i = zsl->level-1; i >= 0; i--) {						//逐层遍历,计算插入节点中,每层的前一个节点、从头节点到达插入节点的跨度
        rank[i] = i == (zsl->level-1) ? 0 : rank[i+1];			//初始化rank(跨度)
        while (x->level[i].forward &&
                (x->level[i].forward->score < score ||			//当前节点分值比较小
                    (x->level[i].forward->score == score &&		//当前节点分值相等,但是字典序在插入节点之前
                    sdscmp(x->level[i].forward->ele,ele) < 0)))
        {
            rank[i] += x->level[i].span;						//累加rank
            x = x->level[i].forward;							//指针偏移
        }
        update[i] = x;											//记录前一个节点
    }
    
  2. 调整跳跃表高度

    level = zslRandomLevel();						//找个随机高度
    for (i = zsl->level; i < level; i++) {			//从当前高度迭代到新高度
        rank[i] = 0;
        update[i] = zsl->header;					//前一个节点是header
        update[i]->level[i].span = zsl->length;		//前一个节点的跨度就是rank
    }
    zsl->level = level;								//新的高度
    
  3. 插入节点

    x = zslCreateNode(level,score,ele);		//创建一个新的节点
    for (i = 0; i < level; i++) {
        x->level[i].forward = update[i]->level[i].forward;					//新节点更新后继
        update[i]->level[i].forward = x;									//新节点的前一个节点更新后继
        x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]);	//新节点更新跨度
        update[i]->level[i].span = (rank[0] - rank[i]) + 1;					//新节点的前一个节点更新跨度
    }
    
  4. 调整backward

    x->backward = (update[0] == zsl->header) ? NULL : update[0];	//更新新节点的backward
    if (x->level[0].forward)				//更新新节点后继的backward
        x->level[0].forward->backward = x;
    else
        zsl->tail = x;	//更新尾节点
    zsl->length++;	//更新长度
    return x;
    
删除节点
  1. 查找要删除的节点,和前面相同,记录了各层的前驱和rank

  2. 更新跨度和后继

    void zslDeleteNode(zskiplist *zsl, zskiplistNode *x, zskiplistNode **update) {
        int i;
        for (i = 0; i < zsl->level; i++) {
            if (update[i]->level[i].forward == x) {
                update[i]->level[i].span += x->level[i].span - 1;	//更新待删除节点的i层前一个节点的跨度
                update[i]->level[i].forward = x->level[i].forward;	//更新待删除节点的i层前一个节点后继
            } else {
                update[i]->level[i].span -= 1;
            }
        }
    }
    
删除跳跃表

获取到跳跃表对象之后,从头节点的第0层开始,通过forward指针逐步向后遍历,每遇到一个节点便将释放其内存。当所有节点的内存都被释放之后,释放跳跃表对象,即完成了跳跃表的删除操作。代码如下。

void zslFree(zskiplist *zsl) {
    zskiplistNode *node = zsl->header->level[0].forward, *next;

    zfree(zsl->header);
    while(node) {
        next = node->level[0].forward;
        zslFreeNode(node);
        node = next;
    }
    zfree(zsl);
}

压缩列表

压缩列表ziplist本质上就是一个字节数组,是Redis为了节约内存而设计的一种线性数据结构,可以包含多个元素,每个元素可以是一个字节数组或一个整数。

Redis的有序集合、散列和列表都直接或者间接使用了压缩列表。当有序集合或散列表的元素个数比较少,且元素都是短字符串时,Redis便使用压缩列表作为其底层数据存储结构。列表使用快速链表(quicklist)数据结构存储,而快速链表就是双向链表与压缩列表的组合。

整体表结构

压缩列表的结构为:

8529eac9-5956-44a5-8add-d638f2005313

  1. zlbytes: 压缩列表的字节长度,占4个字节,因此压缩列表最多有 2 32 − 1 2^{32 -1} 2321个字节。
  2. zltail: 压缩列表尾元素相对于压缩列表起始地址的偏移量,占4个字节。
  3. zllen: 压缩列表的元素个数,占2个字节。zllen无法存储元素个数超过65535( 2 16 − 1 2^{16 -1} 2161)的压缩列表,必须遍历整个压缩列表才能获取到元素个数。
  4. entryX: 压缩列表存储的元素,可以是字节数组或者整数,长度不限。entry的编码结构将在后面详细介绍。
  5. zlend: 压缩列表的结尾,占1个字节,恒为0xFF。

列表元素结构

压缩列表元素的编码结构

baa93f17-b3b0-4ab2-b923-3499ff287e26

previous_entry_length

previous_entry_length字段表示前一个元素的字节长度,占1个或者5个字节

  1. 当前一个元素的长度小于254字节时,用1个字节表示;
  2. 当前一个元素的长度大于或等于254字节时,用5个字节来表示。而此时previous_entry_length字段的第1个字节是固定的0xFE,后面4个字节才真正表示前一个元素的长度。

假设已知当前元素的首地址为p,那么p-previous_entry_length就是前一个元素的首地址,从而实现压缩列表从尾到头的遍历。

encoding

encoding字段表示当前元素的编码,即content字段存储的数据类型(整数或者字节数组),数据内容存储在content字段。为了节约内存,encoding字段同样长度可变。

9acda896-92ea-472a-aa66-feb866140974

content

如上述所说,每个压缩列表节点可以保存一个字节数组或者一个整数值。其中,字节数组可以是以下三种长度之一:

  • 长度小于等于63(2的6次方-1)字节的字节数组
  • 长度小于等于16383(2的14次方-1)字节的字节数组
  • 长度小于等于2的32次方-1字节的字节数组。

而整数值可以是以下六种长度之一

  • 4位长,介于0至12之间的无符号整数
  • 1字节长的有符号整数
  • 3字节长的有符号整数
  • int16_t类型整数
  • int32_t类型整数
  • int64_t类型整数

**结构体zlenty:**对于压缩列表的任意元素,获取前一个元素的长度、判断存储的数据类型、获取数据内容都需要经过复杂的解码运算。解码后的结果应该被缓存起来,为此定义了结构体zlentry,用于表示解码后的压缩列表元素。

typedef struct zlentry {
    unsigned int prevrawlensize;
    unsigned int prevrawlen;

    unsigned int lensize;
    unsigned int len;
    unsigned char encoding;

    unsigned int headersize; 

    unsigned char *p;
} zlentry;

结构体zlentry定义了7个字段

回顾压缩列表元素的编码结构,可变因素实际上不止3个:previous_entry_length字段的长度(prevrawlensize)、previous_entry_length字段存储的内容(prevrawlen)、encoding字段的长度(lensize)、encoding字段的内容(len表示元素数据内容的长度,encoding表示数据类型)和当前元素首地址(p);而headersize则表示当前元素的首部长度,即previous_entry_length字段长度与encoding字段长度之和。

函数zipEntry用来解码压缩列表的元素,存储于zlentry结构体。

void zipEntry(unsigned char *p, zlentry *e) {
    ZIP_DECODE_PREVLEN(p, e->prevrawlensize, e->prevrawlen);	//解码previous_entry_length字段,此时入参ptr指向元素首地址。
    ZIP_DECODE_LENGTH(p + e->prevrawlensize, e->encoding, e->lensize, e->len);	//解码encoding字段逻辑,此时入参ptr指向元素首地址偏移previous_entry_length字段长度的位置。
    e->headersize = e->prevrawlensize + e->lensize;
    e->p = p;
}

散列表、字典

字典又称散列表,是用来存储键值(key-value)对的一种数据结构,在很多高级语言中都有实现,如PHP的数组。但是C语言没有这种数据结构,Redis是K-V型数据库,整个数据库是用字典来存储的,对Redis数据库进行任何增、删、改、查操作,实际就是对字典中的数据进行增、删、改、查操作。

根据Redis数据库的特点,便可知字典有如下特征。

  1. 可以存储海量数据,键值对是映射关系,可以根据键以O(1)的时间复杂度取出或插入关联值。
  2. 键值对中键的类型可以是字符串、整型、浮点型等,且键是唯一的。例如:执行set test"hello world"命令,此时的键test类型为字符串,如test这个键存在数据库中,则为修改操作,否则为插入操作。
  3. 键值对中值的类型可为String、Hash、List、Set、SortedSet。

redis的散列表实现和java相比真的是太简单了

typedef struct dictht {
    dictEntry **table;                /*指针数组,用于存储键值对*/
    unsigned long size;                /*table数组的大小*/
    unsigned long sizemask;        /*掩码 = size - 1 */
    unsigned long used;                /*table数组已存元素个数,包含next单链表的数据*/
} dictht;

typedef struct dictEntry {
    void *key;                        /*存储键*/
    union {
        void *val;                        /*db.dict中的val*/
        uint64_t u64;
        int64_t s64;                /*db.expires中存储过期时间*/
        double d;
    } v;                                /*值,是个联合体*/
    struct dictEntry *next;        /*当Hash冲突时,指向冲突的元素,形成单链表*/
} dictEntry;

typedef struct dict {
    dictType *type;           /*该字典对应的特定操作函数*/
    void *privdata;           /*该字典依赖的数据*/ 
    dictht ht[2];               /*Hash表,键值对存储在此*/
    long rehashidx;            /*rehash标识。默认值为-1,代表没进行rehash操作;不为-1时,代表正进行rehash操作,存储的值表示Hash表ht[0]的rehash操作进行到了哪个索引值*/
    unsigned long iterators; /* 当前运行的迭代器数*/
} dict;

typedef struct dictType {
    uint64_t (*hashFunction)(const void *key);                /*该字典对应的Hash函数*/
    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;

操作

初始化

在redis-server启动中,整个数据库会先初始化一个空的字典用于存储整个数据库的键值对。初始化一个空字典,调用的是dict.h文件中的dictCreate函数

dictCreate函数初始化一个空字典的主要步骤为:1. 申请空间、2. 调用_dictInit函数,3. 给字典的各个字段赋予初始值。初始化后,一个字典内存占用情况如图5-9所示。

添加元素

上述命令是给Server的空数据库添加第一对键值对,Server端收到命令后,最终会执行到setKey(redisDbdb,robjkey,robj*val)函数,前文介绍字典的特性时提到过,每个键必须是唯一的,所以元素添加需要经过这么几步来完成:先查找该键是否存在,存在则执行修改,否则添加键值对。而setKey函数的主要逻辑也是如此,其主要流程如下

  1. 调用dictFind函数,查询键是否存在,是则调用dbOverwrite函数修改键值对,否则调用dbAdd函数添加元素。
  2. dbAdd最终调用dict.h文件中的dictAdd函数插入键值对。
扩容

随着Redis数据库添加操作逐步进行,存储键值对的字典会出现容量不足,达到上限,此时就需要对字典的Hash表进行扩容,扩容对应的源码是dict.h文件中的dictExpand函数。

扩容主要流程为:①申请一块新内存,初次申请时默认容量大小为4个dictEntry;非初次申请时,申请内存的大小则为当前Hash表容量的一倍。②把新申请的内存地址赋值给ht[1],并把字典的rehashidx标识由-1改为0,表示之后需要进行rehash操作。此时字典的内存结构示意图为图5-11所示。

扩容后,字典容量及掩码值会发生改变,同一个键与掩码经位运算后得到的索引值就会发生改变,从而导致根据键查找不到值的情况。解决这个问题的方法是,新扩容的内存放到一个全新的Hash表中(ht[1]),并给字典打上在进行rehash操作中的标识(即rehashidx!=-1)。此后,新添加的键值对都往新的Hash表中存储;而修改、删除、查找操作需要在ht[0]、ht[1]中进行检查,然后再决定去对哪个Hash表操作。除此之外,还需要把老Hash表(ht[0])中的数据重新计算索引值后全部迁移插入到新的Hash表(ht[1])中,此迁移过程称作rehash

rehash

rehash除了扩容时会触发,缩容时也会触发。Redis整个rehash的实现,主要分为如下几步完成。

1)给Hash表ht[1]申请足够的空间;扩容时空间大小为当前容量2,即d->ht[0].used2;当使用量不到总空间10%时,则进行缩容。缩容时空间大小则为能恰好包含d->ht[0].used个节点的2^N次方幂整数,并把字典中字段rehashidx标识为0。

2)进行rehash操作调用的是dictRehash函数,重新计算ht[0]中每个键的Hash值与索引值(重新计算就叫rehash),依次添加到新的Hash表ht[1],并把老Hash表中该键值对删除。把字典中字段rehashidx字段修改为Hash表ht[0]中正在进行rehash操作节点的索引值。

3)rehash操作后,清空ht[0],然后对调一下ht[1]与ht[0]的值,并把字典中rehashidx字段标识为-1。

执行插入、删除、查找、修改等操作前,都先判断当前字典rehash操作是否在进行中,进行中则调用dictRehashStep函数进行rehash操作(每次只对1个节点进行rehash操作,共执行1次)。除这些操作之外,当服务空闲时,如果当前字典也需要进行rehsh操作,则会调用incrementallyRehash函数进行批量rehash操作(每次对100个节点进行rehash操作,共执行1毫秒)。在经历N次rehash操作后,整个ht[0]的数据都会迁移到ht[1]中,这样做的好处就把是本应集中处理的时间分散到了上百万、千万、亿次操作中,所以其耗时可忽略不计。

迭代器遍历

遍历数据库的原则为:①不重复出现数据;②不遗漏任何数据。熟悉Redis命令的读者应该知道,遍历Redis整个数据库主要有两种方式:全遍历 (例如keys命令)、间断遍历 (hscan命令)

全遍历
typedef struct dictIterator {
    dict *d;             //迭代的字典
    int index;           //当前迭代到Hash表中哪个索引值
    int table, safe;     //table用于表示当前正在迭代的Hash表,即ht[0]与ht[1],safe用于表示当前创建的是否为安全迭代器
    dictEntry *entry, *nextEntry;//当前节点,下一个节点
    long long fingerprint;//字典的指纹,当字典未发生改变时,该值不变,发生改变时则值也随着改变
} dictIterator;

整个数据结构占用了48字节,其中d字段指向需要迭代的字典;index字段代表当前读取到Hash表中哪个索引值;table字段表示当前正在迭代的Hash表(即ht[0]与ht[1]中的0和1);safe字段表示当前创建的迭代器是否为安全模式;entry字段表示正在读取的节点数据;nextEntry字段表示entry节点中的next字段所指向的数据。

fingerprint字段是一个64位的整数,表示在给定时间内字典的状态。在这里称其为字典的指纹,因为该字段的值为字典(dict结构体)中所有字段值组合在一起生成的Hash值,所以当字典中数据发生任何变化时,其值都会不同。

迭代器遍历数据分为两类:

  1. 普通迭代器,只遍历数据;
  2. 安全迭代器,遍历的同时删除数据。
普通迭代器
  1. 调用dictGetIterator函数初始化一个普通迭代器,此时会把iter->safe值置为0,表示初始化的迭代器为普通迭代器
  2. 循环调用dictNext函数依次遍历字典中Hash表的节点,首次遍历时会通过dictFingerprint函数拿到当前字典的指纹值
  3. 当调用dictNext函数遍历完字典Hash表中节点数据后,释放迭代器时会继续调用dictFingerprint函数计算字典的指纹值,并与首次拿到的指纹值比较,不相等则输出异常"===ASSERTION FAILED===",且退出程序执行

普通迭代器通过步骤1、步骤3的指纹值对比,来限制整个迭代过程中只能进行迭代操作,即迭代过程中字典数据的修改、添加、删除、查找等操作都不能进行,只能调用dictNext函数迭代整个字典,否则就报异常,由此来保证迭代器取出数据的准确性。

安全迭代器

安全迭代器和普通迭代器迭代数据原理类似,也是通过循环调用dictNext函数依次遍历字典中Hash表的节点。安全迭代器确保读取数据的准确性,不是通过限制字典的部分操作来实现的,而是通过限制rehash的进行来确保数据的准确性,因此迭代过程中可以对字典进行增删改查等操作。

原理上很简单,如果当前字典有安全迭代器运行,则不进行渐进式rehash操作,rehash操作暂停,字典中数据就不会被重复遍历,由此确保了读取数据的准确性。

当Redis执行部分命令时会使用安全迭代器迭代字典数据,例如keys命令。keys命令主要作用是通过模式匹配,返回给定模式的所有key列表,遇到过期的键则会进行删除操作。Redis数据键值对都存储在字典中,因此keys命令会通过安全迭代器来遍历整个字典。安全迭代器整个迭代过程也较为简单,主要分如下几个步骤。

  1. 调用dictGetSafeIterator函数初始化一个安全迭代器,此时会把iter->safe值置为1,表示初始化的迭代器为安全迭代器,safe字段置为1。
  2. 循环调用dictNext函数依次遍历字典中Hash表的节点,首次遍历时会把字典中iterators字段进行加1操作,确保迭代过程中渐进式rehash操作会被中断执行。
  3. 当调用dictNext函数遍历完字典Hash表中节点数据后,释放迭代器时会把字典中iterators字段进行减1操作,确保迭代后渐进式rehash操作能正常进行。
间断遍历

当数据库中有海量数据时,执行keys命令进行一次数据库全遍历,耗时肯定不短,会造成短暂的Redis不可用,所以在Redis在2.8.0版本后新增了scan操作,也就是“间断遍历”。而dictScan是“间断遍历”中的一种实现,主要在迭代字典中数据时使用,例如hscan命令迭代整个数据库中的key,以及zscan命令迭代有序集合所有成员与值时,都是通过dictScan函数来实现的字典遍历。dictScan遍历字典过程中是可以进行rehash操作的,通过算法来保证所有的数据能被遍历到。

整数集合

整数集合(intset)是一个有序的、存储整型数据的结构。我们知道Redis是一个内存数据库,所以必须考虑如何能够高效地利用内存。当Redis集合类型的元素都是整数并且都处在64位有符号整数范围之内时,使用该结构体存储。

在两种情况下,底层编码会发生转换。一种情况为当元素个数超过一定数量之后(默认值为512),即使元素类型仍然是整型,也会将编码转换为hashtable,该值由如下配置项决定:

set-max-intset-entries 512

另一种情况为当增加非整型变量时,例如在集合中增加元素’a’后,testSet的底层编码从intset转换为hashtable

结构体

typedef struct intset {
    uint32_t encoding;//编码类型
    uint32_t length;//元素个数
    int8_t contents[];//柔性数组,根据encoding字段决定几个字节表示一个元素
} intset

encoding:编码类型,决定每个元素占用几个字节。有如下3种类型。

  1. INTSET_ENC_INT16:当元素值都位于INT16_MIN和INT16_MAX之间时使用。该编码方式为每个元素占用2个字节。
  2. INTSET_ENC_INT32:当元素值位于INT16_MAX到INT32_MAX或者INT32_MIN到INT16_MIN之间时使用。该编码方式为每个元素占用4个字节。
  3. INTSET_ENC_INT64:当元素值位于INT32_MAX到INT64_MAX或者INT64_MIN到INT32_MIN之间时使用。该编码方式为每个元素占用8个字节。

bbb0960c-724f-4da2-8040-a0a739c1aacd

intset结构体会根据待插入的值决定是否需要进行扩容操作。扩容会修改encoding字段,而encoding字段决定了一个元素在contents柔性数组中占用几个字节。所以当修改encoding字段之后,intset中原来的元素也需要在contents中进行相应的扩展。只要待插入的值导致了扩容,则该值在待插入的intset中不是最大值就是最小值

升级

当插入的时候,如果新元素的类型比所有元素类型都要长时,要先进行升级

  1. 根据新元素的类型,扩展整数集合底层数组的空间大小
  2. 所有元素转换类型
  3. 添加新元素

引发升级的新元素要不比所有的元素都小(一个巨小的负数),要不比所有元素都大(一个巨大的正数),所以它要不在头,要不在尾。

操作

查询

基于二分查找

插入
  1. 编码是否满足,不满足升级
  2. 检索,找到就不添加,没找到就添加
  3. 扩展
  4. 挪动位置,进行插入
  5. 长度加1
删除
  1. 找到元素
  2. 通过移动内存把元素覆盖掉

对象

redis提供了五大基本数据对象(现在好像不止了)供客户端使用

  1. 字符串对象
  2. 列表对象
  3. 哈希对象
  4. 集合对象
  5. 有序集合对象

redis通过底层的数据结果组合实现了这几种对象。

每个对象都由一个redisObject结构表示,该结构中和保存数据有关的三个属性分别是type属性、encoding属性和ptr属性

typedef struct redisObject {
    unsigned type:4;//类型
    unsigned encoding:4;//编码
    unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */
    int refcount;
    void *ptr;//指向底层实现数据结构的指针
} robj;

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

Redis使用对象来表示数据库中的键和值,每当我们在redis的数据库中新创建一个键值对的时候,我们至少会创建两个对象,一个用作键值对的键(键对象),另一个对象作键值对的值(值对象)。其中,键总是一个字符串对象,而值可以是其他任意对象,因此

  • 当我们称呼一个数据库键为"字符串键"的时候,指的是,它的值是字符串
  • 当我们称呼一个数据库键为“列表键”的时候,指的是,它的值是列表

诸如此类

TYPE命令也类似,对一个数据库键执行TYPE命令时,命令返回键对应的值对象的类型

不同类型值对象的TYPE命令输出

对象对象type属性的值Type命令的输出
字符串对象REDIS_STRINGstring
列表对象REDIS_LISTlist
哈希对象REDIS_HASHhash
集合对象REDIS_SETset
有序集合对象REDIS_ZSETzset

内存回收

因为C语言不具备自动内存回收功能,所以redis在自己的对象系统中构建了一个引用技术(reference counting)技术实现的内存回收机制,通过这一机制,程序可以通过跟踪对象的引用技术信息,在适当的时候自动释放对象,并进行内存回收

  • 当创建一个新对象时,引用技术的值会被初始化为1
  • 当对象被一个新的程序使用时,它的引用计数会被增1
  • 当对象不再被一个程序使用时,它的引用计数会被减1
  • 当对象的引用计数值变为0时,对象所占用的内存会被释放

对象共享

只对int编码的字符串对象进行共享

对象的空转时长

redisObject结构包含一个lru属性,该属性记录对象最后一次被程序访问的时间。如果内存回收算法是volatile-lru或者allkeys-lru,那么服务器占用的内存数高于限制时,空转时间长的会优先被服务器释放,从而回收内存。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值