Redis的数据结构

mac2026-06-14  36

之前简单介绍了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字符串对比的优势:

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

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

每个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; }

参考资料:

《Redis设计与实现》SDS相关博客Redis的rehash策略Redis中的跳表
最新回复(0)