Redis的数据结构

之前简单介绍了Redis的特点和基本配置文件。这一章将着重于Redis的底层数据结构和对象。相比于Memcached,Redis丰富的对象类型是一个非常重要的特点,每个对象又有两种以上的编码方式,这在对数据进行管理的时候提供了很大的便捷性,由于其是一个内存型数据库,内存的空间十分有限,不同的编码方式优化了数据存储的空间。本章着重介绍Redis的数据结构,下一章会一一介绍Redis的对象。

Redis数据结构与对象关系总览

先总体看一下每种数据结构与对象的对应关系。Redis中的数据结构一共有六种,对象有五种。以下表格展示了对应关系。每种对象有两种以上的编码类型,即使同种对象在value不同的情况下,选择一种最适合的数据结构进行存储可以节省许多内存空间。

对象底层数据结构编码
字符串对象整型的字符串REDIS_ENCODING_INT
字符串对象embstr编码的SDSREDIS_ENCODING_EMBSTR
字符串对象SDSREDIS_ENCODING_RAW
列表对象压缩列表REDIS_ENCODING_ZIPLIST
列表对象双向链表REDIS_ENCODING_LINKEDLIST
哈希对象压缩列表REDIS_ENCODING_ZIPLIST
哈希对象字典REDIS_ENCODING_HT
集合对象整数集合REDIS_ENCODING_INTSET
集合对象字典REDIS_ENCODING_HT
有序集合对象压缩列表REDIS_ENCODING_ZIPLIST
有序集合对象跳表和字典REDIS_ENCODING_SKIPLIST

Redis中的数据结构

Redis中的数据结构十分丰富,主要包括一下六种,SDS,lists,skiplist,dict,ziplist,intset。接下来对每种进行分析。

简单动态字符串(SDS)

首先说明SDS与C字符串对比的优势:

  1. SDS是一个结构体,里面存放着已用长度,未用长度和实际内容三个属性,使得SDS将获取字符串长度从O(n)变成O(1)。
  2. SDS更加安全,由于C中没有存储字符串长度,很容易出现缓存溢出的事情,而SDS提供的API则会自动修改存储空间大小。
  3. SDS使用空间预分配策略(当需要对SDS进行动态扩展的时候,如果修改后的len<1mb,则申请同样大小的free空间,如果修改后len>=1mb,则free = 1mb)和惰性释放策略(当需要对),减少对内存的分配。
  4. 兼容C的字符串函数。

SDS实际结构体如下。

struct sdshdr{
	int len;	//buf中字符占用数量,不包括'\0'
	int free;	//buf中未使用字符数量,也不包括‘、0’
	char buf[];	//存储实际的内容
}

sds{.h/.c}文件中展示了一些基本操作sds的函数。如下所示,基本上根据名称就可以知道其功能。

static inline size_t sdslen(const sds s);
static inline size_t sdsavail(const sds s);

sds sdsnewlen(const void *init, size_t initlen);
sds sdsnew(const char *init);
sds sdsempty(void);
size_t sdslen(const sds s);
sds sdsdup(const sds s);
void sdsfree(sds s);
size_t sdsavail(const sds s);
sds sdsgrowzero(sds s, size_t len);
sds sdscatlen(sds s, const void *t, size_t len);
sds sdscat(sds s, const char *t);
sds sdscatsds(sds s, const sds t);
sds sdscpylen(sds s, const char *t, size_t len);
sds sdscpy(sds s, const char *t);
sds sdscatvprintf(sds s, const char *fmt, va_list ap);
sds sdscatprintf(sds s, const char *fmt, ...)
    __attribute__((format(printf, 2, 3)));
sds sdscatfmt(sds s, char const *fmt, ...);
sds sdstrim(sds s, const char *cset);
void sdsrange(sds s, int start, int end);
void sdsupdatelen(sds s);
void sdsclear(sds s);
int sdscmp(const sds s1, const sds s2);
sds *sdssplitlen(const char *s, int len, const char *sep, int seplen, int *count);
void sdsfreesplitres(sds *tokens, int count);
void sdstolower(sds s);
void sdstoupper(sds s);
sds sdsfromlonglong(long long value);
sds sdscatrepr(sds s, const char *p, size_t len);
sds *sdssplitargs(const char *line, int *argc);
sds sdsmapchars(sds s, const char *from, const char *to, size_t setlen);
sds sdsjoin(char **argv, int argc, char *sep);

sds sdsMakeRoomFor(sds s, size_t addlen);
void sdsIncrLen(sds s, int incr);
sds sdsRemoveFreeSpace(sds s);
size_t sdsAllocSize(sds s);

下面简单对其空间预分配sdsMakeRoomFor()函数的实现简要说明。预分配的作用就是当以有空间不够的时候,提前分配空间。

// 参数s表示sdshdr->buf的指针,addlen表示想要增加的字符数
sds sdsMakeRoomFor(sds s, size_t addlen) {
    struct sdshdr *sh, *newsh;
    // 获取 s 目前的空余空间长度 sdshdr->free
    size_t free = sdsavail(s);
    size_t len, newlen;
    // s 目前的空余空间已经足够,无须再进行扩展,直接返回
    if (free >= addlen) return s;
    // 获取 s 目前已占用空间的长度 sdshdr->len
    len = sdslen(s);
    sh = (void*) (s-(sizeof(struct sdshdr)));
    // s 最少需要的长度(扩展后占用的长度)
    newlen = (len+addlen);
    // 根据新长度,为 s 分配新空间所需的大小 SDS_MAX_PREALLOC = 1024 * 1024 = 1MB
    if (newlen < SDS_MAX_PREALLOC)
        // 如果新长度小于 SDS_MAX_PREALLOC 
        // 那么为它分配两倍于所需长度的空间
        newlen *= 2;
    else
        // 否则,分配长度为目前长度加上 SDS_MAX_PREALLOC
        newlen += SDS_MAX_PREALLOC;
    // T = O(N),重新分配内存空间
    newsh = zrealloc(sh, sizeof(struct sdshdr)+newlen+1);
    // 内存不足,分配失败,返回
    if (newsh == NULL) return NULL;
    // 更新 sds 的空余长度
    newsh->free = newlen - len;
    // 返回 sds
    return newsh->buf;
}

双向链表

Redis中的双向链表基本上和平常所见的双向链表一致,区别在于其在list结构中添加了节点数、头结点指针、尾节点指针和一些操作函数,这些属性可以有效增加链表操作的便捷性。

/*
 * 双向链表节点,每个节点有指向前一个节点的指针,指向后一个节点的指针和指向数据的指针
 */
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;

Redis中的双向链表函数。函数的实现都很简单基础,这里不多介绍。

list *listCreate(void);
void listRelease(list *list);
list *listAddNodeHead(list *list, void *value);
list *listAddNodeTail(list *list, void *value);
list *listInsertNode(list *list, listNode *old_node, void *value, int after);
void listDelNode(list *list, listNode *node);
listIter *listGetIterator(list *list, int direction);
listNode *listNext(listIter *iter);
void listReleaseIterator(listIter *iter);
list *listDup(list *orig);
listNode *listSearchKey(list *list, void *key);
listNode *listIndex(list *list, long index);
void listRewind(list *list, listIter *li);
void listRewindTail(list *list, listIter *li);
void listRotate(list *list);

字典

字典其实就类似STL中的unordered_map,Redis中的字典底层用hash表实现,其中的实现很值得参考。基础的数据结构有四种:hash节点(dictEntry),hash表(dictht),hash相关操作函数(dictType),hash迭代器(dictIterator)。hash相关的一个最大的问题是如何解决hash冲突。经典的方法有:探测法(线性探测,二次探测),链表法,二次散列等。
Redis中的字典采取的方法就是链表法,用单链表连接多个冲突的节点(头插入,冲突的节点放到链表头部)。但是当键值对过多的时候链表法的效率就会线性下降,所以在dict结构体中存放着两个哈希表,ht[0]用于直接存储节点数据,ht[1]用于rehash。Redis中的rehash操作是渐进式的,即一步一步将ht[0]中的数据转移到ht[1]中,这样的做法可以有效减少对服务器性能的影响。rehash的条件有三个:

  1. 当没有执行BGSAVE或BGREWRITEAOF命令时,并且负载因子>=1时候,进行扩展。
  2. 当执行BGSAVE或BGREWRITEAOF命令时,并且负载因子>=5时候,进行扩展。
  3. 当负载因子<=0.1时候进行收缩。

负载因子的计算:负载因子 = 哈希表已保存的节点数 / 哈希表大小(ht[0].used / ht[0].size)

rehash的步骤也有三步:

  1. 为字典ht[1]分配新的空间,扩展大小为第一个大于等于ht[0].used*2的2 ^ n数字,收缩大小为第一个大于等于ht[0].used的2 ^ n 数字。
  2. 将ht[0]中的键值对重新计算哈希值和索引值,将其放到为ht[1]中。
  3. 释放ht[0],将ht[1]设置为ht[0],并在ht[1]中创建一个新的空白哈希表。
/*
 * 哈希表节点
 */
typedef struct dictEntry {
    // 键
    void *key;
    // 值,union公用地址空间
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
    } v;
    // 指向下个哈希表节点,形成链表
    struct dictEntry *next;
} dictEntry;
/*
 * 字典类型特定函数,
 */
typedef struct dictType {
    // 计算哈希值的函数
    unsigned int (*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;
/*
 * 哈希表
 *
 * 每个字典都使用两个哈希表,从而实现渐进式 rehash 。
 */
typedef struct dictht {
    // 哈希表数组,数组中的每个节点都是一个dictEntry*
    dictEntry **table;
    // 哈希表大小(table数组大小)
    unsigned long size;
    // 哈希表大小掩码,和哈希值一起计算索引值
    // 总是等于 size - 1
    unsigned long sizemask;
    // 该哈希表已有节点的数量
    unsigned long used;
} dictht;
/*
 * 字典
 */
typedef struct dict {
    // 类型特定函数
    dictType *type;
    // 私有数据
    void *privdata;
    // 两个哈希表,一般只用ht[0],ht[1]在rehash的时候才用
    dictht ht[2];
    // rehash 索引
    // 当 rehash 不在进行时,值为 -1
    int rehashidx; /* rehashing not in progress if rehashidx == -1 */
    // 目前正在运行的安全迭代器的数量
    int iterators; /* number of iterators currently running */
} dict;
/*
 * 字典迭代器
 *
 * 如果 safe 属性的值为 1 ,那么在迭代进行的过程中,
 * 程序仍然可以执行 dictAdd 、 dictFind 和其他函数,对字典进行修改。
 *
 * 如果 safe 不为 1 ,那么程序只会调用 dictNext 对字典进行迭代,
 * 而不对字典进行修改。
 */
typedef struct dictIterator {
    // 被迭代的字典
    dict *d;
    // table :正在被迭代的哈希表号码,值可以是 0 或 1 。
    // index :迭代器当前所指向的哈希表索引位置。
    // safe :标识这个迭代器是否安全
    int table, index, safe;
    // entry :当前迭代到的节点的指针
    // nextEntry :当前迭代节点的下一个节点
    //             因为在安全迭代器运作时, entry 所指向的节点可能会被修改,
    //             所以需要一个额外的指针来保存下一节点的位置,
    //             从而防止指针丢失
    dictEntry *entry, *nextEntry;
    long long fingerprint; /* unsafe iterator fingerprint for misuse detection */
} dictIterator;

字典中提供的api如下。

dict *dictCreate(dictType *type, void *privDataPtr);
int dictExpand(dict *d, unsigned long size);
int dictAdd(dict *d, void *key, void *val);
dictEntry *dictAddRaw(dict *d, void *key);
int dictReplace(dict *d, void *key, void *val);
dictEntry *dictReplaceRaw(dict *d, void *key);
int dictDelete(dict *d, const void *key);
int dictDeleteNoFree(dict *d, const void *key);
void dictRelease(dict *d);
dictEntry * dictFind(dict *d, const void *key);
void *dictFetchValue(dict *d, const void *key);
int dictResize(dict *d);
dictIterator *dictGetIterator(dict *d);
dictIterator *dictGetSafeIterator(dict *d);
dictEntry *dictNext(dictIterator *iter);
void dictReleaseIterator(dictIterator *iter);
dictEntry *dictGetRandomKey(dict *d);
int dictGetRandomKeys(dict *d, dictEntry **des, int count);
void dictPrintStats(dict *d);
unsigned int dictGenHashFunction(const void *key, int len);
unsigned int dictGenCaseHashFunction(const unsigned char *buf, int len);
void dictEmpty(dict *d, void(callback)(void*));
void dictEnableResize(void);
void dictDisableResize(void);
int dictRehash(dict *d, int n);
int dictRehashMilliseconds(dict *d, int ms);
void dictSetHashFunctionSeed(unsigned int initval);
unsigned int dictGetHashFunctionSeed(void);
unsigned long dictScan(dict *d, unsigned long v, dictScanFunction *fn, void *privdata);

rehash策略是哈希表中的重中之重,有机会会新开一篇博客对其记录。

跳表

跳表其实是Nosql中运用的很常见的一种数据结构,之前在LevelDB中数据结构中已经提及,这里对Redis中的跳表实现进行简单梳理。这里转载自大佬对于skiplist,平衡树,哈希表的对比分析,全文参考这里

  1. skiplist和各种平衡树(如AVL、红黑树等)的元素是有序排列的,而哈希表不是有序的。因此,在哈希表上只能做单个key的查找,不适宜做范围查找。所谓范围查找,指的是查找那些大小在指定的两个值之间的所有节点。
  2. 在做范围查找的时候,平衡树比skiplist操作要复杂。在平衡树上,我们找到指定范围的小值之后,还需要以中序遍历的顺序继续寻找其它不超过大值的节点。如果不对平衡树进行一定的改造,这里的中序遍历并不容易实现。而在skiplist上进行范围查找就非常简单,只需要在找到小值之后,对第1层链表进行若干步的遍历就可以实现。
  3. 平衡树的插入和删除操作可能引发子树的调整,逻辑复杂,而skiplist的插入和删除只需要修改相邻节点的指针,操作简单又快速。
  4. 从内存占用上来说,skiplist比平衡树更灵活一些。一般来说,平衡树每个节点包含2个指针(分别指向左右子树),而skiplist每个节点包含的指针数目平均为1/(1-p),具体取决于参数p的大小。如果像Redis里的实现一样,取p=1/4,那么平均每个节点包含1.33个指针,比平衡树更有优势。
  5. 查找单个key,skiplist和平衡树的时间复杂度都为O(log n),大体相当;而哈希表在保持较低的哈希值冲突概率的前提下,查找时间复杂度接近O(1),性能更高一些。所以我们平常使用的各种Map或dictionary结构,大都是基于哈希表实现的。
  6. 从算法实现难度上来比较,skiplist比平衡树要简单得多。

接下来看一下Redis中跳表的实现。相关数据结构如下。可以看出Redis中的跳表相比于LevelDB中的实现有较大的区别,最明显的就是每个节点增加了分值和后退指针的设计(我个人觉得Redis的可读性更强,实现更好)。

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;

skiplist操作函数如下。

zskiplist *zslCreate(void);
void zslFree(zskiplist *zsl);
zskiplistNode *zslInsert(zskiplist *zsl, double score, robj *obj);
unsigned char *zzlInsert(unsigned char *zl, robj *ele, double score);
int zslDelete(zskiplist *zsl, double score, robj *obj);
zskiplistNode *zslFirstInRange(zskiplist *zsl, zrangespec *range);
zskiplistNode *zslLastInRange(zskiplist *zsl, zrangespec *range);
double zzlGetScore(unsigned char *sptr);
void zzlNext(unsigned char *zl, unsigned char **eptr, unsigned char **sptr);
void zzlPrev(unsigned char *zlchushihua, unsigned char **eptr, unsigned char **sptr);
unsigned int zsetLength(robj *zobj);
void zsetConvert(robj *zobj, int encoding);
unsigned long zslGetRank(zskiplist *zsl, double score, robj *o);

压缩列表

压缩列表是一个特殊编码的顺序性连续内存块数据结构,可以用于存储字符串值和整数值。压缩列表的组成成分如下:
压缩列表图chushihua

属性类型长度用途
zlbyteuint32_t4字节记录整个列表内存字节数
zltailuint32_t4字节记录列表尾部节点距离起始地址多少字节
zllenuint16_t2字节记录整个列表节点数
entry列表数据节点不限节点数据
zlenduint8_t1字节标志压缩列表末端

每个entry节点的格式如下:
entry节点

属性长度作用
previous_entry_len1字节或5字节记录压缩列表前一个节点的长度
encoding一字节、两字节以及五字节记录节点content属性所保存数据的类型以及长度
content不限保存节点的值,可以是一个字节数组或整数

相关压缩列表函数如下。

unsigned char *ziplistNew(void);
unsigned char *ziplistPush(unsigned char *zl, unsigned char *s, unsigned int slen, int where);
unsigned char *ziplistIndex(unchushihuasigned char *zl, int index);
unsigned char *ziplistNext(unsigned char *zl, unsigned char *p);
unsigned char *ziplistPrev(unsigned char *zl, unsigned char *p);
unsigned int ziplistGet(unsigned char *p, unsigned char **sval, unsigned int *slen, long long *lval);
unsigned char *ziplistInsert(unsigned char *zl, unsigned char *p, unsigned char *s, unsigned int slen);chushihua
unsigned char *ziplistDelete(unsigned char *zl, unsigned char **p);
unsigned char *ziplistDeleteRange(unsigned char *zl, unsigned int index, unsigned int num);
unsigned int ziplistCompare(unsigned char *p, unsigned char *s, unsigned int slen);
unsigned char *ziplistFind(unsigned char *p, unsigned char *vstr, unsigned int vlen, unsigned int skip);
unsigned int ziplistLen(unsigned char *zl);
size_t ziplistBlobLen(unsigned char *zl);

ziplistNew()函数就是对一个新的压缩列表进行初始化。初始化都是通过宏来实现的。

/* Create a new empty ziplist. 
 *
 * 创建并返回一个新的 ziplist 
 *
 * T = O(1)
 */
unsigned char *ziplistNew(void) {
    // ZIPLIST_HEADER_SIZE 是 ziplist 表头的大小
    // 1 字节是表末端 ZIP_END 的大小
    unsigned int bytes = ZIPLIST_HEADER_SIZE+1;
    // 为表头和表末端分配空间
    unsigned char *zl = zmalloc(bytes);
    // 初始化表属性
    ZIPLIST_BYTES(zl) = intrev32ifbe(bytes);
    ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(ZIPLIST_HEADER_SIZE);
    ZIPLIST_LENGTH(zl) = 0;
    // 设置表末端
    zl[bytes-1] = ZIP_END;
    return zl;
}

整数集合

Intset表示整数集合。intset中有一个升级策略(当有新加入元素的类型比原整数集合的类型大,此时就需要对数组中类型进行改变)。升级策略的优势在于可以节约内存,提升存储灵活性。

typedef struct intset {
    // 编码方式,有:INTSET_ENC_INT16,INTSET_ENC_INT32,INTSET_ENC_INT64
    uint32_t encoding;
    // 集合包含的元素数量
    uint32_t length;
    // 保存元素的数组,content数组中的元素类型是encoding属性值
    int8_t contents[];
} intset;

intset操作函数。

intset *intsetNew(void);
intset *intsetAdd(intset *is, int64_t value, uint8_t *success);
intset *intsetRemove(intset *is, int64_t value, int *success);
uint8_t intsetFind(intset *is, int64_t value);
int64_t intsetRandom(intset *is);
uint8_t intsetGet(intset *is, uint32_t pos, int64_t *value);
uint32_t intsetLen(intset *is);
size_t intsetBlobLen(intset *is);

这里主要看看向整数集合添加元素以及升级策略的实现。

/* 
 * 尝试将元素 value 添加到整数集合中。
 *
 * *success 的值指示添加是否成功:
 * - 如果添加成功,那么将 *success 的值设为 1 。
 * - 因为元素已存在而造成添加失败时,将 *success 的值设为 0 。
 *
 * T = O(N)
 */
intset *intsetAdd(intset *is, int64_t value, uint8_t *success) {
    // 计算编码 value 所需的长度
    uint8_t valenc = _intsetValueEncoding(value);
    uint32_t pos;
    // 默认设置插入为成功
    if (success) *success = 1;
    // 如果 value 的编码比整数集合现在的编码要大
    // 那么表示 value 必然可以添加到整数集合中
    // 并且整数集合需要对自身进行升级,才能满足 value 所需的编码
    if (valenc > intrev32ifbe(is->encoding)) {
        /* This always succeeds, so we don't need to curry *success. */
        // T = O(N)
        return intsetUpgradeAndAdd(is,value);
    } else {
        // 运行到这里,表示整数集合现有的编码方式适用于 value
        // 在整数集合中查找 value ,看他是否存在:
        // - 如果存在,那么将 *success 设置为 0 ,并返回未经改动的整数集合
        // - 如果不存在,那么可以插入 value 的位置将被保存到 pos 指针中
        //   等待后续程序使用
        if (intsetSearch(is,value,&pos)) {
            if (success) *success = 0;
            return is;
        }
        // 运行到这里,表示 value 不存在于集合中
        // 程序需要将 value 添加到整数集合中
        // 为 value 在集合中分配空间
        is = intsetResize(is,intrev32ifbe(is->length)+1);
        // 如果新元素不是被添加到底层数组的末尾
        // 那么需要对现有元素的数据进行移动,空出 pos 上的位置,用于设置新值
        // 举个例子
        // 如果数组为:
        // | x | y | z | ? |
        //     |<----->|
        // 而新元素 n 的 pos 为 1 ,那么数组将移动 y 和 z 两个元素
        // | x | y | y | z |
        //         |<----->|
        // 这样就可以将新元素设置到 pos 上了:
        // | x | n | y | z |
        // T = O(N)
        if (pos < intrev32ifbe(is->length)) intsetMoveTail(is,pos,pos+1);
    }
    // 将新值设置到底层数组的指定位置中
    _intsetSet(is,pos,value);
    // 增一集合元素数量的计数器
    is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
    // 返回添加新元素后的整数集合
    return is;
}

/*  
 * 根据值 value 所使用的编码方式,对整数集合的编码进行升级,
 * 并将值 value 添加到升级后的整数集合中。
 *
 * 返回值:添加新元素之后的整数集合
 *
 * T = O(N)
 */
static intset *intsetUpgradeAndAdd(intset *is, int64_t value) {
    // 当前的编码方式
    uint8_t curenc = intrev32ifbe(is->encoding);
    // 新值所需的编码方式
    uint8_t newenc = _intsetValueEncoding(value);
    // 当前集合的元素数量
    int length = intrev32ifbe(is->length);
    // 根据 value 的值,决定是将它添加到底层数组的最前端还是最后端
    // 注意,因为 value 的编码比集合原有的其他元素的编码都要大
    // 所以 value 要么大于集合中的所有元素,要么小于集合中的所有元素
    // 因此,value 只能添加到底层数组的最前端或最后端
    int prepend = value < 0 ? 1 : 0;
    // 更新集合的编码方式
    is->encoding = intrev32ifbe(newenc);
    // 根据新编码对集合(的底层数组)进行空间调整
    // T = O(N)
    is = intsetResize(is,intrev32ifbe(is->length)+1);
    // 根据集合原来的编码方式,从底层数组中取出集合元素
    // 然后再将元素以新编码的方式添加到集合中
    // 当完成了这个步骤之后,集合中所有原有的元素就完成了从旧编码到新编码的转换
    // 因为新分配的空间都放在数组的后端,所以程序先从后端向前端移动元素
    // 举个例子,假设原来有 curenc 编码的三个元素,它们在数组中排列如下:
    // | x | y | z | 
    // 当程序对数组进行重分配之后,数组就被扩容了(符号 ? 表示未使用的内存):
    // | x | y | z | ? |   ?   |   ?   |
    // 这时程序从数组后端开始,重新插入元素:
    // | x | y | z | ? |   z   |   ?   |
    // | x | y |   y   |   z   |   ?   |
    // |   x   |   y   |   z   |   ?   |
    // 最后,程序可以将新元素添加到最后 ? 号标示的位置中:
    // |   x   |   y   |   z   |  new  |
    // 上面演示的是新元素比原来的所有元素都大的情况,也即是 prepend == 0
    // 当新元素比原来的所有元素都小时(prepend == 1),调整的过程如下:
    // | x | y | z | ? |   ?   |   ?   |
    // | x | y | z | ? |   ?   |   z   |
    // | x | y | z | ? |   y   |   z   |
    // | x | y |   x   |   y   |   z   |
    // 当添加新值时,原本的 | x | y | 的数据将被新值代替
    // |  new  |   x   |   y   |   z   |
    // T = O(N)
    while(length--)
        _intsetSet(is,length+prepend,_intsetGetEncoded(is,length,curenc));
    // 设置新值,根据 prepend 的值来决定是添加到数组头还是数组尾
    if (prepend)
        _intsetSet(is,0,value);
    else
        _intsetSet(is,intrev32ifbe(is->length),value);
    // 更新整数集合的元素数量
    is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
    return is;
}

参考资料:

  1. 《Redis设计与实现》
  2. SDS相关博客
  3. Redis的rehash策略
  4. Redis中的跳表
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值