之前简单介绍了Redis的特点和基本配置文件。这一章将着重于Redis的底层数据结构和对象。相比于Memcached,Redis丰富的对象类型是一个非常重要的特点,每个对象又有两种以上的编码方式,这在对数据进行管理的时候提供了很大的便捷性,由于其是一个内存型数据库,内存的空间十分有限,不同的编码方式优化了数据存储的空间。本章着重介绍Redis的数据结构,下一章会一一介绍Redis的对象。
Redis数据结构与对象关系总览
先总体看一下每种数据结构与对象的对应关系。Redis中的数据结构一共有六种,对象有五种。以下表格展示了对应关系。每种对象有两种以上的编码类型,即使同种对象在value不同的情况下,选择一种最适合的数据结构进行存储可以节省许多内存空间。
对象 | 底层数据结构 | 编码 |
---|---|---|
字符串对象 | 整型的字符串 | REDIS_ENCODING_INT |
字符串对象 | embstr编码的SDS | REDIS_ENCODING_EMBSTR |
字符串对象 | SDS | REDIS_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字符串对比的优势:
- SDS是一个结构体,里面存放着已用长度,未用长度和实际内容三个属性,使得SDS将获取字符串长度从O(n)变成O(1)。
- SDS更加安全,由于C中没有存储字符串长度,很容易出现缓存溢出的事情,而SDS提供的API则会自动修改存储空间大小。
- SDS使用空间预分配策略(当需要对SDS进行动态扩展的时候,如果修改后的len<1mb,则申请同样大小的free空间,如果修改后len>=1mb,则free = 1mb)和惰性释放策略(当需要对),减少对内存的分配。
- 兼容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的条件有三个:
- 当没有执行BGSAVE或BGREWRITEAOF命令时,并且负载因子>=1时候,进行扩展。
- 当执行BGSAVE或BGREWRITEAOF命令时,并且负载因子>=5时候,进行扩展。
- 当负载因子<=0.1时候进行收缩。
负载因子的计算:负载因子 = 哈希表已保存的节点数 / 哈希表大小(ht[0].used / ht[0].size)
rehash的步骤也有三步:
- 为字典ht[1]分配新的空间,扩展大小为第一个大于等于ht[0].used*2的2 ^ n数字,收缩大小为第一个大于等于ht[0].used的2 ^ n 数字。
- 将ht[0]中的键值对重新计算哈希值和索引值,将其放到为ht[1]中。
- 释放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,平衡树,哈希表的对比分析,全文参考这里。
- skiplist和各种平衡树(如AVL、红黑树等)的元素是有序排列的,而哈希表不是有序的。因此,在哈希表上只能做单个key的查找,不适宜做范围查找。所谓范围查找,指的是查找那些大小在指定的两个值之间的所有节点。
- 在做范围查找的时候,平衡树比skiplist操作要复杂。在平衡树上,我们找到指定范围的小值之后,还需要以中序遍历的顺序继续寻找其它不超过大值的节点。如果不对平衡树进行一定的改造,这里的中序遍历并不容易实现。而在skiplist上进行范围查找就非常简单,只需要在找到小值之后,对第1层链表进行若干步的遍历就可以实现。
- 平衡树的插入和删除操作可能引发子树的调整,逻辑复杂,而skiplist的插入和删除只需要修改相邻节点的指针,操作简单又快速。
- 从内存占用上来说,skiplist比平衡树更灵活一些。一般来说,平衡树每个节点包含2个指针(分别指向左右子树),而skiplist每个节点包含的指针数目平均为1/(1-p),具体取决于参数p的大小。如果像Redis里的实现一样,取p=1/4,那么平均每个节点包含1.33个指针,比平衡树更有优势。
- 查找单个key,skiplist和平衡树的时间复杂度都为O(log n),大体相当;而哈希表在保持较低的哈希值冲突概率的前提下,查找时间复杂度接近O(1),性能更高一些。所以我们平常使用的各种Map或dictionary结构,大都是基于哈希表实现的。
- 从算法实现难度上来比较,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
属性 | 类型 | 长度 | 用途 |
---|---|---|---|
zlbyte | uint32_t | 4字节 | 记录整个列表内存字节数 |
zltail | uint32_t | 4字节 | 记录列表尾部节点距离起始地址多少字节 |
zllen | uint16_t | 2字节 | 记录整个列表节点数 |
entry | 列表数据节点 | 不限 | 节点数据 |
zlend | uint8_t | 1字节 | 标志压缩列表末端 |
每个entry节点的格式如下:
属性 | 长度 | 作用 |
---|---|---|
previous_entry_len | 1字节或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;
}
参考资料:
- 《Redis设计与实现》
- SDS相关博客
- Redis的rehash策略
- Redis中的跳表