对于Redis的使用者来讲, Redis做为Key-Value型的内存数据库, 其Value有多种类型.node
这些Value的类型, 只是"Redis的用户认为的, Value存储数据的方式". 而在具体实现上, 各个Type的Value到底如何存储, 这对于Redis的使用者来讲是不公开的.redis
举个粟子: 使用下面的命令建立一个Key-Value算法
$ SET "Hello" "World"
对于Redis的使用者来讲, Hello
这个Key, 对应的Value是String类型, 其值为五个ASCII字符组成的二进制数据. 但具体在底层实现上, 这五个字节是如何存储的, 是不对用户公开的. 即, Value的Type, 只是表象, 具体数据在内存中以何种数据结构存放, 这对于用户来讲是没必要要了解的.数据库
Redis对使用者暴露了五种Value Type, 其底层实现的数据结构有8种, 分别是:数组
而衔接"底层数据结构"与"Value Type"的桥梁的, 则是Redis实现的另一种数据结构: redisObject
. Redis中的Key与Value在表层都是一个redisObject
实例, 故该结构有所谓的"类型", 便是ValueType
. 对于每一种Value Type
类型的redisObject
, 其底层至少支持两种不一样的底层数据结构来实现. 以应对在不一样的应用场景中, Redis的运行效率, 或内存占用.安全
这是一种用于存储二进制数据的一种结构, 具备动态扩容的特色. 其实现位于src/sds.h
与src/sds.c
中, 其关键定义以下:服务器
typedef char *sds; /* Note: sdshdr5 is never used, we just access the flags byte directly. * However is here to document the layout of type 5 SDS strings. */ struct __attribute__ ((__packed__)) sdshdr5 { unsigned char flags; /* 3 lsb of type, and 5 msb of string length */ char buf[]; }; 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[]; };
SDS的整体概览以下图:数据结构
其中sdshdr
是头部, buf
是真实存储用户数据的地方. 另外注意, 从命名上能看出来, 这个数据结构除了能存储二进制数据, 显然是用于设计做为字符串使用的, 因此在buf
中, 用户数据后总跟着一个\0
. 即图中 "数据" + "\0" 是为所谓的buf
app
SDS有五种不一样的头部. 其中sdshdr5
实际并未使用到. 因此实际上有四种不一样的头部, 分别以下:dom
len
分别以uint8
, uint16
, uint32
, uint64
表示用户数据的长度(不包括末尾的\0
)alloc
分别以uint8
, uint16
, uint32
, uint64
表示整个SDS, 除过头部与末尾的\0
, 剩余的字节数.flag
始终为一字节, 以低三位标示着头部的类型, 高5位未使用.当在程序中持有一个SDS实例时, 直接持有的是数据区的头指针, 这样作的用意是: 经过这个指针, 向前偏一个字节, 就能取到flag
, 经过判断flag低三位的值, 能迅速判断: 头部的类型, 已用字节数, 总字节数, 剩余字节数. 这也是为何sds
类型便是char *
指针类型别名的缘由.
建立一个SDS实例有三个接口, 分别是:
// 建立一个不含数据的sds: // 头部 3字节 sdshdr8 // 数据区 0字节 // 末尾 \0 占一字节 sds sdsempty(void); // 带数据建立一个sds: // 头部 按initlen的值, 选择最小的头部类型 // 数据区 从入参指针init处开始, 拷贝initlen个字节 // 末尾 \0 占一字节 sds sdsnewlen(const void *init, size_t initlen); // 带数据建立一个sds: // 头部 按strlen(init)的值, 选择最小的头部类型 // 数据区 入参指向的字符串中的全部字符, 不包括末尾 \0 // 末尾 \0 占一字节 sds sdsnew(const char *init);
sdsnewlen
用于带二进制数据建立sds实例, sdsnew
用于带字符串建立sds实例. 接口返回的sds能够直接传入libc中的字符串输出函数中进行操做, 因为不管其中存储的是用户的二进制数据, 仍是字符串, 其末尾都带一个\0, 因此至少调用libc中的字符串输出函数是安全的.在对SDS中的数据进行修改时, 若剩余空间不足, 会调用sdsMakeRoomFor
函数用于扩容空间, 这是一个很低级的API, 一般状况下不该当由SDS的使用者直接调用. 其实现中核心的几行以下:
sds sdsMakeRoomFor(sds s, size_t addlen) { ... /* Return ASAP if there is enough space left. */ if (avail >= addlen) return s; len = sdslen(s); sh = (char*)s-sdsHdrSize(oldtype); newlen = (len+addlen); if (newlen < SDS_MAX_PREALLOC) newlen *= 2; else newlen += SDS_MAX_PREALLOC; ... }
能够看到, 在扩充空间时
addlen
可用SDS_MAC_PREALLOC
时, 申请空间再翻一倍. 若整体空间已经超过了阈值, 则步进增加SDS_MAC_PREALLOC
. 这个阈值的默认值为 1024 * 1024
SDS也提供了接口用于移除全部未使用的内存空间. sdsRemoveFreeSpace
, 该接口没有间接的被任何SDS其它接口调用, 即默认状况下, SDS不会自动回收预留空间. 在SDS的使用者须要节省内存时, 由使用者自行调用:
sds sdsRemoveFreeSpace(sds s);
总结:
sdsMakeRoomFor
, 每一次扩充空间, 都会预留大量的空间. 这样作的考量是: 若是一个SDS实例中的数据被变动了, 那么颇有可能会在后续发生屡次变动.这是普通的链表实现, 链表结点不直接持有数据, 而是经过void *
指针来间接的指向数据. 其实现位于 src/adlist.h
与src/adlist.c
中, 关键定义以下:
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;
其内存布局以下图所示:
这是一个平平无奇的链表的实现. list
在Redis除了做为一些Value Type的底层实现外, 还普遍用于Redis的其它功能实现中, 做为一种数据结构工具使用. 在list
的实现中, 除了基本的链表定义外, 还额外增长了:
listIter
的定义, 与相关接口的实现.list
中的链表结点自己并不直接持有数据, 而是经过value
字段, 以void *
指针的形式间接持有, 因此数据的生命周期并不彻底与链表及其结点一致. 这给了list
的使用者至关大的灵活性. 好比能够多个结点持有同一份数据的地址. 但与此同时, 在对链表进行销毁, 结点复制以及查找匹配时, 就须要list
的使用者将相关的函数指针赋值于list.dup
, list.free
, list.match
字段.dict
是Redis底层数据结构中实现最为复杂的一个数据结构, 其功能相似于C++标准库中的std::unordered_map
, 其实现位于 src/dict.h
与 src/dict.c
中, 其关键定义以下:
typedef struct dictEntry { void *key; union { void *val; uint64_t u64; int64_t s64; double d; } v; struct dictEntry *next; } dictEntry; 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; /* This is our hash table structure. Every dictionary has two of this as we * implement incremental rehashing, for the old to the new table. */ typedef struct dictht { dictEntry **table; unsigned long size; unsigned long sizemask; unsigned long used; } dictht; typedef struct dict { dictType *type; void *privdata; dictht ht[2]; long rehashidx; /* rehashing not in progress if rehashidx == -1 */ unsigned long iterators; /* number of iterators currently running */ } dict; /* If safe is set to 1 this is a safe iterator, that means, you can call * dictAdd, dictFind, and other functions against the dictionary even while * iterating. Otherwise it is a non safe iterator, and only dictNext() * should be called while iterating. */ typedef struct dictIterator { dict *d; long index; int table, safe; dictEntry *entry, *nextEntry; /* unsafe iterator fingerprint for misuse detection. */ long long fingerprint; } dictIterator;
其内存布局以下所示:
dict
中存储的键值对, 是经过dictEntry
这个结构间接持有的, k
经过指针间接持有键, v
经过指针间接持有值. 注意, 若值是整数值的话, 是直接存储在v字段中的, 而不是间接持有. 同时next
指针用于指向, 在bucket索引值冲突时, 以链式方式解决冲突, 指向同索引的下一个dictEntry
结构.dictht.table
中, 结点自己是散布在内存中的, 顺序表中存储的是dictEntry
的指针dictht
结构, 其经过table
字段间接的持有顺序表形式的bucket, bucket的容量存储在size
字段中, 为了加速将散列值转化为bucket中的数组索引, 引入了sizemask
字段, 计算指定键在哈希表中的索引时, 执行的操做相似于dict->type->hashFunction(键) & dict->ht[x].sizemask
. 从这里也能够看出来, bucket的容量适宜于为2的幂次, 这样计算出的索引值能覆盖到全部bucket索引位.dict
即为字典. 其中type
字段中存储的是本字典使用到的各类函数指针, 包括散列函数, 键与值的复制函数, 释放函数, 以及键的比较函数. privdata
是用于存储用户自定义数据. 这样, 字典的使用者能够最大化的自定义字典的实现, 经过自定义各类函数实现, 以及能够附带私有数据, 保证了字典有很大的调优空间.ht[2]
这个数组字段. 其用意是这样的:
dict
仅持有一个哈希表dictht
的实例, 即整个字典由一个bucket实现.dictht
的实例, ht[0]
指向旧哈希表, ht[1]
指向扩容后的新哈希表. 平滑扩容的重点在于两个策略:
ht[1]
指向的哈希表中ht[0]
中的一个bucket索引位持有的结点链表, 迁移到ht[1]
中去. 迁移的进度保存在rehashidx
这个字段中.在旧表中因为冲突而被连接在同一索引位上的结点, 迁移到新表后, 可能会散布在多个新表索引中去.ht[0]
指向的旧表会被释放, 以后会将新表的持有权转交给ht[0]
, 再重置ht[1]
指向NULL
dict->ht[0]->table[rehashindex]->k
与dict->ht[0]->table[rehashindex]->v
分别指向的实际数据, 内存地址都不会变化. 没有发生键数据与值数据的拷贝或移动, 扩容整个过程仅是各类指针的操做. 速度很是快dict
的使用者是无感知的. 若扩容是一次性的, 当新旧bucket容量特别大时, 迁移全部结点必然会致使耗时陡增.除了字典自己的实现外, 其中还顺带实现了一个迭代器, 这个迭代器中有字段safe
以标示该迭代器是"安全迭代器"仍是"非安全迭代器", 所谓的安全与否, 指是的这种场景:
设想在运行迭代器的过程当中, 字典正处于平滑扩容的过程当中. 在平滑扩容的过程当中时, 旧表一个索引位上的, 由冲突而链起来的多个结点, 迁移到新表后, 可能会散布到新表的多个索引位上. 且新的索引位的值可能比旧的索引位要低.
遍历操做的重点是, 保证在迭代器遍历操做开始时, 字典中持有的全部结点, 都会被遍历到. 而若在遍历过程当中, 一个未遍历的结点, 从旧表迁移到新表后, 索引值减少了, 那么就可能会致使这个结点在遍历过程当中被遗漏.
因此, 所谓的"安全"迭代器, 其在内部实现时: 在迭代过程当中, 若字典正处于平滑扩容过程, 则暂停结点迁移, 直至迭代器运行结束. 这样虽然不能保证在迭代过程当中插入的结点会被遍历到, 但至少保证在迭代起始时, 字典中持有的全部结点都会被遍历到.
这也是为何dict
结构中有一个iterators
字段的缘由: 该字段记录了运行于该字典上的安全迭代器的数目. 若该数目不为0, 字典是不会继续进行结点迁移平滑扩容的.
下面是字典的扩容操做中的核心代码, 咱们以插入操做引发的扩容为例:
先是插入操做的外部逻辑:
int dictAdd(dict *d, void *key, void *val) { dictEntry *entry = dictAddRaw(d,key,NULL); // 调用dictAddRaw if (!entry) return DICT_ERR; dictSetVal(d, entry, val); return DICT_OK; } dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing) { long index; dictEntry *entry; dictht *ht; if (dictIsRehashing(d)) _dictRehashStep(d); // 若在平滑扩容过程当中, 先步进迁移一个bucket索引 /* Get the index of the new element, or -1 if * the element already exists. */ // 在计算键在bucket中的索引值时, 内部会检查是否须要扩容 if ((index = _dictKeyIndex(d, key, dictHashKey(d,key), existing)) == -1) return NULL; /* Allocate the memory and store the new entry. * Insert the element in top, with the assumption that in a database * system it is more likely that recently added entries are accessed * more frequently. */ ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0]; entry = zmalloc(sizeof(*entry)); entry->next = ht->table[index]; ht->table[index] = entry; ht->used++; /* Set the hash entry fields. */ dictSetKey(d, entry, key); return entry; }
下面是计算bucket索引值的函数, 内部会探测该哈希表是否须要扩容, 若是须要扩容(结点数目与bucket数组长度比例达到1:1), 就使字典进入平滑扩容过程:
static long _dictKeyIndex(dict *d, const void *key, uint64_t hash, dictEntry **existing) { unsigned long idx, table; dictEntry *he; if (existing) *existing = NULL; /* Expand the hash table if needed */ if (_dictExpandIfNeeded(d) == DICT_ERR) // 探测是否须要扩容, 若是须要, 则开始扩容 return -1; for (table = 0; table <= 1; table++) { idx = hash & d->ht[table].sizemask; /* Search if this slot does not already contain the given key */ he = d->ht[table].table[idx]; while(he) { if (key==he->key || dictCompareKeys(d, key, he->key)) { if (existing) *existing = he; return -1; } he = he->next; } if (!dictIsRehashing(d)) break; } return idx; } /* Expand the hash table if needed */ static int _dictExpandIfNeeded(dict *d) { /* Incremental rehashing already in progress. Return. */ if (dictIsRehashing(d)) return DICT_OK; // 若是正在扩容过程当中, 则什么也不作 /* If the hash table is empty expand it to the initial size. */ // 若字典中本无元素, 则初始化字典, 初始化时的bucket数组长度为4 if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE); /* If we reached the 1:1 ratio, and we are allowed to resize the hash * table (global setting) or we should avoid it but the ratio between * elements/buckets is over the "safe" threshold, we resize doubling * the number of buckets. */ // 若字典中元素的个数与bucket数组长度比值大于1:1时, 则调用dictExpand进入平滑扩容状态 if (d->ht[0].used >= d->ht[0].size && (dict_can_resize || d->ht[0].used/d->ht[0].size > dict_force_resize_ratio)) { return dictExpand(d, d->ht[0].used*2); } return DICT_OK; } int dictExpand(dict *d, unsigned long size) { dictht n; /* the new hash table */ // 新建一个dictht结构 unsigned long realsize = _dictNextPower(size); /* the size is invalid if it is smaller than the number of * elements already inside the hash table */ if (dictIsRehashing(d) || d->ht[0].used > size) return DICT_ERR; /* Rehashing to the same table size is not useful. */ if (realsize == d->ht[0].size) return DICT_ERR; /* Allocate the new hash table and initialize all pointers to NULL */ n.size = realsize; n.sizemask = realsize-1; n.table = zcalloc(realsize*sizeof(dictEntry*));// 初始化dictht下的table, 即bucket数组 n.used = 0; /* Is this the first initialization? If so it's not really a rehashing * we just set the first hash table so that it can accept keys. */ // 如果新字典初始化, 直接把dictht结构挂在ht[0]中 if (d->ht[0].table == NULL) { d->ht[0] = n; return DICT_OK; } // 不然, 把新dictht结构挂在ht[1]中, 并开启平滑扩容(置rehashidx为0, 字典处于非扩容状态时, 该字段值为-1) /* Prepare a second hash table for incremental rehashing */ d->ht[1] = n; d->rehashidx = 0; return DICT_OK; }
下面是平滑扩容的实现:
static void _dictRehashStep(dict *d) { // 若字典上还运行着安全迭代器, 则不迁移结点 // 不然每次迁移一个旧bucket索引上的全部结点 if (d->iterators == 0) dictRehash(d,1); } int dictRehash(dict *d, int n) { int empty_visits = n*10; /* Max number of empty buckets to visit. */ if (!dictIsRehashing(d)) return 0; while(n-- && d->ht[0].used != 0) { dictEntry *de, *nextde; /* Note that rehashidx can't overflow as we are sure there are more * elements because ht[0].used != 0 */ assert(d->ht[0].size > (unsigned long)d->rehashidx); // 在旧bucket中, 找到下一个非空的索引位 while(d->ht[0].table[d->rehashidx] == NULL) { d->rehashidx++; if (--empty_visits == 0) return 1; } // 取出该索引位上的结点链表 de = d->ht[0].table[d->rehashidx]; /* Move all the keys in this bucket from the old to the new hash HT */ // 把全部结点迁移到新bucket中去 while(de) { uint64_t h; nextde = de->next; /* Get the index in the new hash table */ h = dictHashKey(d, de->key) & d->ht[1].sizemask; de->next = d->ht[1].table[h]; d->ht[1].table[h] = de; d->ht[0].used--; d->ht[1].used++; de = nextde; } d->ht[0].table[d->rehashidx] = NULL; d->rehashidx++; } /* Check if we already rehashed the whole table... */ // 检查是否旧表中的全部结点都被迁移到了新表 // 若是是, 则置先释放原旧bucket数组, 再置ht[1]为ht[0] // 最后再置rehashidx=-1, 以示字典不处于平滑扩容状态 if (d->ht[0].used == 0) { zfree(d->ht[0].table); d->ht[0] = d->ht[1]; _dictReset(&d->ht[1]); d->rehashidx = -1; return 0; } /* More to rehash... */ return 1; }
总结:
dictEntry
结构持有, 故在平滑扩容过程当中, 不涉及用户数据的拷贝dictType
结构与dict.privdata
字段), 对于一些特定场合使用的键数据, 用户能够自行选择更高效更特定化的散列函数zskiplist
是Redis实现的一种特殊的跳跃表. 跳跃表是一种基于线性表实现简单的搜索结构, 其最大的特色就是: 实现简单, 性能能逼近各类搜索树结构. 血统纯正的跳跃表的介绍在维基百科中便可查阅. 在Redis中, 在原版跳跃表的基础上, 进行了一些小改动, 便是如今要介绍的zskiplis
t结构.
其定义在src/server.h
中, 以下:
/* ZSETs use a specialized version of Skiplists */ typedef struct zskiplistNode { sds ele; 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;
其内存布局以下图:
zskiplist
的核心设计要点为:
level[]
的长度为32ele
字段, 还有一个字段score
, 其标示着结点的得分, 结点之间凭借得分来判断前后顺序, 跳跃表中的结点按结点的得分升序排列.backward
指针, 这是原版跳跃表中所没有的. 该指针指向结点的前一个紧邻结点.zskiplistLevel
结构. 实际数量在结点建立时, 按幂次定律随机生成(不超过32). 每一个zskiplistLevel
中有两个字段.
forward
字段指向比本身得分高的某个结点(不必定是紧邻的), 而且, 若当前zskiplistLevel
实例在level[]
中的索引为X
, 则其forward
字段指向的结点, 其level[]
字段的容量至少是X+1
. 这也是上图中, 为何forward
指针老是画的水平的缘由.span
字段表明forward
字段指向的结点, 距离当前结点的距离. 紧邻的两个结点之间的距离定义为1.zskiplist
中持有字段level
, 用以记录全部结点(除过头结点外), level[]
数组最长的长度.跳跃表主要用于, 在给定一个分值的状况下, 查找与该分值最接近的结点. 搜索时, 伪代码以下:
int level = zskiplist->level - 1; zskiplistNode p = zskiplist->head; while(1 && p) { zskiplistNode q = (p->level)[level]->forward: if(q->score > 分值) { if(level > 0) { level--; } else { return : q为整个跳跃表中, 分值大于指定分值的第一个结点 q->backward为整个跳跃表中, 分值小于或等于指定分值的最后一个结点 } } else { p = q; } }
跳跃表的实现比较简单, 最复杂的操做便是插入与删除结点, 须要仔细处理邻近结点的全部level[]
中的全部zskiplistLevel
结点中的forward
与span
的值的变动.
另外, 关于新建立的结点, 其level[]
数组长度的随机算法, 在接口zslInsert
的实现中, 核心代码片段以下:
zskiplistNode *zslInsert(zskiplist *zsl, double score, sds ele) { //... level = zslRandomLevel(); // 随机生成新结点的, level[]数组的长度 if (level > zsl->level) { // 若生成的新结点的level[]数组的长度比当前表中全部结点的level[]的长度都大 // 那么头结点中须要新增几个指向该结点的指针 // 并刷新ziplist中的level字段 for (i = zsl->level; i < level; i++) { rank[i] = 0; update[i] = zsl->header; update[i]->level[i].span = zsl->length; } zsl->level = level; } x = zslCreateNode(level,score,ele); // 建立新结点 //... 执行插入操做 } // 按幂次定律生成小于32的随机数的函数 // 宏 ZSKIPLIST_MAXLEVEL 的定义为32, 宏 ZSKIPLIST_P 被设定为 0.25 // 即 // level == 1的几率为 75% // level == 2的几率为 75% * 25% // level == 3的几率为 75% * 25% * 25% // ... // level == 31的几率为 0.75 * 0.25^30 // 而 // level == 32的几率为 0.75 * sum(i = 31 ~ +INF){ 0.25^i } int zslRandomLevel(void) { int level = 1; while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF)) level += 1; return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL; }
这是一个用于存储在序的整数的数据结构, 也底层数据结构中最简单的一个, 其定义与实如今src/intest.h
与src/intset.c
中, 关键定义以下:
typedef struct intset { uint32_t encoding; uint32_t length; int8_t contents[]; } intset; #define INTSET_ENC_INT16 (sizeof(int16_t)) #define INTSET_ENC_INT32 (sizeof(int32_t)) #define INTSET_ENC_INT64 (sizeof(int64_t))
inset
结构中的encoding
的取值有三个, 分别是宏INTSET_ENC_INT16
, INTSET_ENC_INT32
, INTSET_ENC_INT64
. length
表明其中存储的整数的个数, contents
指向实际存储数值的连续内存区域. 其内存布局以下图所示:
intset
中各字段, 包括contents
中存储的数值, 都是以主机序(小端字节序)存储的. 这意味着Redis若运行在PPC这样的大端字节序的机器上时, 存取数据都会有额外的字节序转换开销encoding == INTSET_ENC_INT16
时, contents
中以int16_t
的形式存储着数值. 相似的, 当encoding == INTSET_ENC_INT32
时, contents
中以int32_t
的形式存储着数值.int32_t
的取值范围, 整个intset
都要进行升级, 即全部的数值都须要以int64_t
的形式存储. 显然升级的开销是很大的.intset
中的数值是以升序排列存储的, 插入与删除的复杂度均为O(n). 查找使用二分法, 复杂度为O(log_2(n))intset
的代码实现中, 不预留空间, 即每一次插入操做都会调用zrealloc
接口从新分配内存. 每一次删除也会调用zrealloc
接口缩减占用的内存. 省是省了, 但内存操做的时间开销上升了.intset
的编码方式一经升级, 不会再降级.总之, intset
适合于以下数据的存储:
int16_t
或int32_t
的取值范围中ziplist
是Redis底层数据结构中, 最苟的一个结构. 它的设计宗旨就是: 省内存, 从牙缝里省内存. 设计思路和TLV一致, 但为了从牙缝里节省内存, 作了不少额外工做.
ziplist
的内存布局与intset
同样: 就是一块连续的内存空间. 但区域划分比较复杂, 概览以下图:
intset
同样, ziplist
中的全部值都是以小端序存储的zlbytes
字段的类型是uint32_t
, 这个字段中存储的是整个ziplist
所占用的内存的字节数zltail
字段的类型是uint32_t
, 它指的是ziplist
中最后一个entry
的偏移量. 用于快速定位最后一个entry
, 以快速完成pop
等操做zllen
字段的类型是uint16_t
, 它指的是整个ziplit
中entry
的数量. 这个值只占16位, 因此蛋疼的地方就来了: 若是ziplist
中entry
的数目小于65535, 那么该字段中存储的就是实际entry
的值. 若等于或超过65535, 那么该字段的值固定为65535, 但实际数量须要一个个entry
的去遍历全部entry
才能获得.zlend
是一个终止字节, 其值为全F, 即0xff
. ziplist
保证任何状况下, 一个entry
的首字节都不会是255
在画图展现entry
的内存布局以前, 先讲一下entry
中都存储了哪些信息:
entry
中存储了它前一个entry
所占用的字节数. 这样支持ziplist
反向遍历.entry
用单独的一块区域, 存储着当前结点的类型: 所谓的类型, 包括当前结点存储的数据是什么(二进制, 仍是数值), 如何编码(若是是数值, 数值如何存储, 若是是二进制数据, 二进制数据的长度)entry
的内存布局以下所示:
prevlen
便是"前一个entry所占用的字节数", 它自己是一个变长字段, 规约以下:
entry
占用的字节数小于 254, 则prevlen
字段占一字节entry
占用的字节数等于或大于 254, 则prevlen
字段占五字节: 第一个字节值为 254, 即0xfe
, 另外四个字节, 以uint32_t
存储着值.encoding
字段的规约就复杂了许多
encoding
占一字节. 在这一字节中, 高两位值固定为0, 低六位值以无符号整数的形式存储着二进制数据的长度. 即 00xxxxxx
, 其中低六位bitxxxxxx
是用二进制保存的数据长度.encoding
占用两个字节. 在这两个字节16位中, 第一个字节的高两位固定为01
, 剩余的14个位, 以小端序无符号整数的形式存储着二进制数据的长度, 即 01xxxxxx, yyyyyyyy
, 其中yyyyyyyy
是高八位, xxxxxx
是低六位.encoding
占用五个字节. 第一个字节是固定值10000000
, 剩余四个字节, 按小端序uint32_t
的形式存储着二进制数据的长度. 这也是ziplist
能存储的二进制数据的最大长度, 超过2^32-1
字节的二进制数据, ziplist
没法存储.encoding
和data
的规约以下:
entry
, 其encoding
都仅占用一个字节. 而且最高两位均是11
[0, 12]
中, 则encoding
和data
挤在同一个字节中. 即为1111 0001
~1111 1101
, 高四位是固定值, 低四位的值从0001
至1101
, 分别表明 0 ~ 12这十五个数值[-128, -1] [13, 127]
中, 则encoding == 0b 1111 1110
. 数值存储在紧邻的下一个字节, 以int8_t
形式编码[-32768, -129] [128, 32767]
中, 则encoding == 0b 1100 0000
. 数值存储在紧邻的后两个字节中, 以小端序int16_t
形式编码[-8388608, -32769] [32768, 8388607]
中, 则encoding == 0b 1111 0000
. 数值存储在紧邻的后三个字节中, 以小端序存储, 占用三个字节.[-2^31, -8388609] [8388608, 2^31 - 1]
中, 则encoding == 0b 1101 0000.
数值存储在紧邻的后四个字节中, 以小端序int32_t
形式编码int64_t
所能表达的范围内, 则encoding == 0b 1110 0000
, 数值存储在紧邻的后八个字节中, 以小端序int64_t
形式编码在大规模数值存储中, ziplist
几乎不浪费内存空间, 其苟的程序到达了字节级别, 甚至对于[0, 12]
区间的数值, 连data
里的那一个字节也要省下来. 显然, ziplist
是一种特别节省内存的数据结构, 但它的缺点也十分明显:
intset
同样, ziplist
也不预留内存空间, 而且在移除结点后, 也是当即缩容, 这表明每次写操做都会进行内存分配操做.ziplist
最蛋疼的一个问题是: 结点若是扩容, 致使结点占用的内存增加, 而且超过254字节的话, 可能会致使链式反应: 其后一个结点的entry.prevlen
须要从一字节扩容至五字节. 最坏状况下, 第一个结点的扩容, 会致使整个ziplist
表中的后续全部结点的entry.prevlen
字段扩容. 虽然这个内存重分配的操做依然只会发生一次, 但代码中的时间复杂度是o(N)级别, 由于链式扩容只能一步一步的计算. 但这种状况的几率十分的小, 通常状况下链式扩容能连锁反映五六次就很不幸了. 之因此说这是一个蛋疼问题, 是由于, 这样的坏场景下, 其实时间复杂度并不高: 依次计算每一个entry
新的空间占用, 也就是o(N), 整体占用计算出来后, 只执行一次内存重分配, 与对应的memmove
操做, 就能够了. 蛋疼说的是: 代码特别难写, 难读. 下面放一段处理插入结点时处理链式反应的代码片段, 你们自行感觉一下:unsigned char *__ziplistInsert(unsigned char *zl, unsigned char *p, unsigned char *s, unsigned int slen) { size_t curlen = intrev32ifbe(ZIPLIST_BYTES(zl)), reqlen; unsigned int prevlensize, prevlen = 0; size_t offset; int nextdiff = 0; unsigned char encoding = 0; long long value = 123456789; /* initialized to avoid warning. Using a value that is easy to see if for some reason we use it uninitialized. */ zlentry tail; /* Find out prevlen for the entry that is inserted. */ if (p[0] != ZIP_END) { ZIP_DECODE_PREVLEN(p, prevlensize, prevlen); } else { unsigned char *ptail = ZIPLIST_ENTRY_TAIL(zl); if (ptail[0] != ZIP_END) { prevlen = zipRawEntryLength(ptail); } } /* See if the entry can be encoded */ if (zipTryEncoding(s,slen,&value,&encoding)) { /* 'encoding' is set to the appropriate integer encoding */ reqlen = zipIntSize(encoding); } else { /* 'encoding' is untouched, however zipStoreEntryEncoding will use the * string length to figure out how to encode it. */ reqlen = slen; } /* We need space for both the length of the previous entry and * the length of the payload. */ reqlen += zipStorePrevEntryLength(NULL,prevlen); reqlen += zipStoreEntryEncoding(NULL,encoding,slen); /* When the insert position is not equal to the tail, we need to * make sure that the next entry can hold this entry's length in * its prevlen field. */ int forcelarge = 0; nextdiff = (p[0] != ZIP_END) ? zipPrevLenByteDiff(p,reqlen) : 0; if (nextdiff == -4 && reqlen < 4) { nextdiff = 0; forcelarge = 1; } /* Store offset because a realloc may change the address of zl. */ offset = p-zl; zl = ziplistResize(zl,curlen+reqlen+nextdiff); p = zl+offset; /* Apply memory move when necessary and update tail offset. */ if (p[0] != ZIP_END) { /* Subtract one because of the ZIP_END bytes */ memmove(p+reqlen,p-nextdiff,curlen-offset-1+nextdiff); /* Encode this entry's raw length in the next entry. */ if (forcelarge) zipStorePrevEntryLengthLarge(p+reqlen,reqlen); else zipStorePrevEntryLength(p+reqlen,reqlen); /* Update offset for tail */ ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+reqlen); /* When the tail contains more than one entry, we need to take * "nextdiff" in account as well. Otherwise, a change in the * size of prevlen doesn't have an effect on the *tail* offset. */ zipEntry(p+reqlen, &tail); if (p[reqlen+tail.headersize+tail.len] != ZIP_END) { ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+nextdiff); } } else { /* This element will be the new tail. */ ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(p-zl); } /* When nextdiff != 0, the raw length of the next entry has changed, so * we need to cascade the update throughout the ziplist */ if (nextdiff != 0) { offset = p-zl; zl = __ziplistCascadeUpdate(zl,p+reqlen); p = zl+offset; } /* Write the entry */ p += zipStorePrevEntryLength(p,prevlen); p += zipStoreEntryEncoding(p,encoding,slen); if (ZIP_IS_STR(encoding)) { memcpy(p,s,slen); } else { zipSaveInteger(p,value,encoding); } ZIPLIST_INCR_LENGTH(zl,1); return zl; } unsigned char *__ziplistCascadeUpdate(unsigned char *zl, unsigned char *p) { size_t curlen = intrev32ifbe(ZIPLIST_BYTES(zl)), rawlen, rawlensize; size_t offset, noffset, extra; unsigned char *np; zlentry cur, next; while (p[0] != ZIP_END) { zipEntry(p, &cur); rawlen = cur.headersize + cur.len; rawlensize = zipStorePrevEntryLength(NULL,rawlen); /* Abort if there is no next entry. */ if (p[rawlen] == ZIP_END) break; zipEntry(p+rawlen, &next); /* Abort when "prevlen" has not changed. */ if (next.prevrawlen == rawlen) break; if (next.prevrawlensize < rawlensize) { /* The "prevlen" field of "next" needs more bytes to hold * the raw length of "cur". */ offset = p-zl; extra = rawlensize-next.prevrawlensize; zl = ziplistResize(zl,curlen+extra); p = zl+offset; /* Current pointer and offset for next element. */ np = p+rawlen; noffset = np-zl; /* Update tail offset when next element is not the tail element. */ if ((zl+intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))) != np) { ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+extra); } /* Move the tail to the back. */ memmove(np+rawlensize, np+next.prevrawlensize, curlen-noffset-next.prevrawlensize-1); zipStorePrevEntryLength(np,rawlen); /* Advance the cursor */ p += rawlen; curlen += extra; } else { if (next.prevrawlensize > rawlensize) { /* This would result in shrinking, which we want to avoid. * So, set "rawlen" in the available bytes. */ zipStorePrevEntryLengthLarge(p+rawlen,rawlen); } else { zipStorePrevEntryLength(p+rawlen,rawlen); } /* Stop here, as the raw length of "next" has not changed. */ break; } } return zl; }
这种代码的特色就是: 最好由做者去维护, 最好一次性写对. 由于读起来真的费劲, 改起来也很费劲.
若是说ziplist
是整个Redis中为了节省内存, 而写的最苟的数据结构, 那么称quicklist
就是在最苟的基础上, 再苟了一层. 这个结构是Redis在3.2版本后新加的, 在3.2版本以前, 咱们能够讲, dict
是最复杂的底层数据结构, ziplist
是最苟的底层数据结构. 在3.2版本以后, 这两个记录被双双刷新了.
这是一种, 以ziplist
为结点的, 双端链表结构. 宏观上, quicklist
是一个链表, 微观上, 链表中的每一个结点都是一个ziplist
.
它的定义与实现分别在src/quicklist.h
与src/quicklist.c
中, 其中关键定义以下:
/* Node, quicklist, and Iterator are the only data structures used currently. */ /* quicklistNode is a 32 byte struct describing a ziplist for a quicklist. * We use bit fields keep the quicklistNode at 32 bytes. * count: 16 bits, max 65536 (max zl bytes is 65k, so max count actually < 32k). * encoding: 2 bits, RAW=1, LZF=2. * container: 2 bits, NONE=1, ZIPLIST=2. * recompress: 1 bit, bool, true if node is temporarry decompressed for usage. * attempted_compress: 1 bit, boolean, used for verifying during testing. * extra: 12 bits, free for future use; pads out the remainder of 32 bits */ typedef struct quicklistNode { struct quicklistNode *prev; struct quicklistNode *next; unsigned char *zl; unsigned int sz; /* ziplist size in bytes */ unsigned int count : 16; /* count of items in ziplist */ unsigned int encoding : 2; /* RAW==1 or LZF==2 */ unsigned int container : 2; /* NONE==1 or ZIPLIST==2 */ unsigned int recompress : 1; /* was this node previous compressed? */ unsigned int attempted_compress : 1; /* node can't compress; too small */ unsigned int extra : 10; /* more bits to steal for future usage */ } quicklistNode; /* quicklistLZF is a 4+N byte struct holding 'sz' followed by 'compressed'. * 'sz' is byte length of 'compressed' field. * 'compressed' is LZF data with total (compressed) length 'sz' * NOTE: uncompressed length is stored in quicklistNode->sz. * When quicklistNode->zl is compressed, node->zl points to a quicklistLZF */ typedef struct quicklistLZF { unsigned int sz; /* LZF size in bytes*/ char compressed[]; } quicklistLZF; /* quicklist is a 40 byte struct (on 64-bit systems) describing a quicklist. * 'count' is the number of total entries. * 'len' is the number of quicklist nodes. * 'compress' is: -1 if compression disabled, otherwise it's the number * of quicklistNodes to leave uncompressed at ends of quicklist. * 'fill' is the user-requested (or default) fill factor. */ typedef struct quicklist { quicklistNode *head; quicklistNode *tail; unsigned long count; /* total count of all entries in all ziplists */ unsigned long len; /* number of quicklistNodes */ int fill : 16; /* fill factor for individual nodes */ unsigned int compress : 16; /* depth of end nodes not to compress;0=off */ } quicklist; typedef struct quicklistIter { const quicklist *quicklist; quicklistNode *current; unsigned char *zi; long offset; /* offset in current ziplist */ int direction; } quicklistIter; typedef struct quicklistEntry { const quicklist *quicklist; quicklistNode *node; unsigned char *zi; unsigned char *value; long long longval; unsigned int sz; int offset; } quicklistEntry;
这里定义了五个结构体:
quicklistNode
, 宏观上, quicklist
是一个链表, 这个结构描述的就是链表中的结点. 它经过zl
字段持有底层的ziplist
. 简单来说, 它描述了一个ziplist
实例quicklistLZF
, ziplist
是一段连续的内存, 用LZ4算法压缩后, 就能够包装成一个quicklistLZF
结构. 是否压缩quicklist
中的每一个ziplist
实例是一个可配置项. 若这个配置项是开启的, 那么quicklistNode.zl
字段指向的就不是一个ziplist
实例, 而是一个压缩后的quicklistLZF
实例quicklist
. 这就是一个双链表的定义. head, tail
分别指向头尾指针. len
表明链表中的结点. count
指的是整个quicklist
中的全部ziplist
中的entry
的数目. fill
字段影响着每一个链表结点中ziplist
的最大占用空间, compress
影响着是否要对每一个ziplist
以LZ4算法进行进一步压缩以更节省内存空间.quicklistIter
是一个迭代器quicklistEntry
是对ziplist
中的entry
概念的封装. quicklist
做为一个封装良好的数据结构, 不但愿使用者感知到其内部的实现, 因此须要把ziplist.entry
的概念从新包装一下.quicklist
的内存布局图以下所示:
下面是有关quicklist
的更多额外信息:
quicklist.fill
的值影响着每一个链表结点中, ziplist
的长度.
ziplist
的最大长度. 具体为:
-1
不超过4kb-2
不超过 8kb-3
不超过 16kb-4
不超过 32kb-5
不超过 64kbentry
数目限制单个ziplist
的长度. 值即为数目. 因为该字段仅占16位, 因此以entry
数目限制ziplist
的容量时, 最大值为2^15个quicklist.compress
的值影响着quicklistNode.zl
字段指向的是原生的ziplist
, 仍是通过压缩包装后的quicklistLZF
0
表示不压缩, zl
字段直接指向ziplist
1
表示quicklist
的链表头尾结点不压缩, 其他结点的zl
字段指向的是通过压缩后的quicklistLZF
2
表示quicklist
的链表头两个, 与末两个结点不压缩, 其他结点的zl
字段指向的是通过压缩后的quicklistLZF
2^16
quicklistNode.encoding
字段, 以指示本链表结点所持有的ziplist
是否通过了压缩. 1
表明未压缩, 持有的是原生的ziplist
, 2
表明压缩过quicklistNode.container
字段指示的是每一个链表结点所持有的数据类型是什么. 默认的实现是ziplist
, 对应的该字段的值是2
, 目前Redis没有提供其它实现. 因此实际上, 该字段的值恒为2quicklistNode.recompress
字段指示的是当前结点所持有的ziplist
是否通过了解压. 若是该字段为1
即表明以前被解压过, 且须要在下一次操做时从新压缩.quicklist
的具体实现代码篇幅很长, 这里就不贴代码片段了, 从内存布局上也能看出来, 因为每一个结点持有的ziplist
是有上限长度的, 因此在与操做时要考虑的分支状况比较多. 想一想都蛋疼.
quicklist
有本身的优势, 也有缺点, 对于使用者来讲, 其使用体验相似于线性数据结构, list
做为最传统的双链表, 结点经过指针持有数据, 指针字段会耗费大量内存. ziplist
解决了耗费内存这个问题. 但引入了新的问题: 每次写操做整个ziplist
的内存都须要重分配. quicklist
在二者之间作了一个平衡. 而且使用者能够经过自定义quicklist.fill
, 根据实际业务状况, 经验主义调参.
dict
做为字典结构, 优势不少, 扩展性强悍, 支持平滑扩容等等, 但对于字典中的键值均为二进制数据, 且长度都很小时, dict
的中的一坨指针会浪费很多内存, 所以Redis又实现了一个轻量级的字典, 即为zipmap
.
zipmap
适合使用的场合是:
dict
支持各类嵌套, 字典自己并不持有数据, 而仅持有数据的指针. 但zipmap
是直接持有数据的.zipmap
的定义与实如今src/zipmap.h
与src/zipmap.c
两个文件中, 其定义与实现均未定义任何struct结构体, 由于zipmap
的内存布局就是一块连续的内存空间. 其内存布局以下所示:
zipmap
起始的第一个字节存储的是zipmap
中键值对的个数. 若是键值对的个数大于254的话, 那么这个字节的值就是固定值254, 真实的键值对个数须要遍历才能得到.zipmap
的最后一个字节是固定值0xFF
zipmap
中的每个键值对, 称为一个entry
, 其内存占用如上图, 分别六部分:
len_of_key
, 一字节或五字节. 存储的是键的二进制长度. 若是长度小于254, 则用1字节存储, 不然用五个字节存储, 第一个字节的值固定为0xFE
, 后四个字节以小端序uint32_t
类型存储着键的二进制长度.key_data
为键的数据len_of_val
, 一字节或五字节, 存储的是值的二进制长度. 编码方式同len_of_key
len_of_free
, 固定值1字节, 存储的是entry
中未使用的空间的字节数. 未使用的空间即为图中的free
, 它通常是因为键值对中的值被替换发生的. 好比, 键值对hello <-> word
被修改成hello <-> w
后, 就空了四个字节的闲置空间val_data
, 为值的数据free
, 为闲置空间. 因为len_of_free
的值最大只能是254, 因此若是值的变动致使闲置空间大于254的话, zipmap
就会回收内存空间.衔接底层数据结构, 与五种Value Type之间的桥梁就是redisObject
这个结构. 该结构的关键定义以下(位于src/server.h
中):
/*----------------------------------------------------------------------------- * Data types *----------------------------------------------------------------------------*/ /* A redis object, that is a type able to hold a string / list / set */ /* The actual Redis Object */ #define OBJ_STRING 0 #define OBJ_LIST 1 #define OBJ_SET 2 #define OBJ_ZSET 3 #define OBJ_HASH 4 /* Objects encoding. Some kind of objects like Strings and Hashes can be * internally represented in multiple ways. The 'encoding' field of the object * is set to one of this fields for this object. */ #define OBJ_ENCODING_RAW 0 /* Raw representation */ #define OBJ_ENCODING_INT 1 /* Encoded as integer */ #define OBJ_ENCODING_HT 2 /* Encoded as hash table */ #define OBJ_ENCODING_ZIPMAP 3 /* Encoded as zipmap */ #define OBJ_ENCODING_LINKEDLIST 4 /* No longer used: old list encoding. */ #define OBJ_ENCODING_ZIPLIST 5 /* Encoded as ziplist */ #define OBJ_ENCODING_INTSET 6 /* Encoded as intset */ #define OBJ_ENCODING_SKIPLIST 7 /* Encoded as skiplist */ #define OBJ_ENCODING_EMBSTR 8 /* Embedded sds string encoding */ #define OBJ_ENCODING_QUICKLIST 9 /* Encoded as linked list of ziplists */ #define LRU_BITS 24 #define LRU_CLOCK_MAX ((1<<LRU_BITS)-1) /* Max value of obj->lru */ #define LRU_CLOCK_RESOLUTION 1000 /* LRU clock resolution in ms */ #define OBJ_SHARED_REFCOUNT INT_MAX typedef struct redisObject { unsigned type:4; unsigned encoding:4; unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or * LFU data (least significant 8 bits frequency * and most significant 16 bits access time). */ int refcount; void *ptr; } robj;
redisObject
的内存布局以下:
从定义上来看, redisObject
有:
type
字段encoding
字段, 代表对象底层使用的数据结构类型lru
字段refcount
ptr
字段redisObject
的通用操做API以下:
API | 功能 |
---|---|
char *strEncoding(int encoding) |
返回各类编码的可读字符串表达 |
void decrRefCount(robj *o); |
引用计数-1. 若减后引用计数会降为0, 则会自动调用 freeXXXObject 函数释放对象 |
void decrRefCountVoid(void *o); |
功能同decrRefCount , 只不过接收的是void * 型参数 |
void incrRefCount(robj *o); |
引用计数+1 |
robj *makeObjectShared(robj *o); |
将对象置为"全局共享对象", 所谓的"全局只读共享对象", 有如下特征 0. 内部引用计数为 INT_MAX 0. 引用计数操做函数对其不起做用 0. 多纯种共享读是安全的, 不须要加锁 0. 禁止写操做 |
robj *resetRefCount(robj *obj); |
将引用计数置为0, 但不会调用freeXXXObject 函数释放对象 |
robj *createObject(int type, void *ptr); |
建立一个对象, 对象类型由参数指定, 对象底层编码指定为RAW , 底层数据由参数提供, 对象引用计数为1.并初始化 lru 字段. 若服务器采用LRU算法, 则置该字段的值为当前分钟级别的一个时间戳. 若服务器采用LFU算法, 则置为一个计数值. |
unsigned long long estimateObjectIdleTime(robj *o) |
获取一个对象未被访问的时间, 单位为毫秒. 因为 redisObject 中lru 字段有24位, 并非无限长, 因此有循环溢出的风险, 当发生循环溢出时(即当前LRU时钟计数比对象中的lru 字段小), 那么该函数始终保守认为循环溢出只发生了一次 |
字符串对象支持三种编码方式: INT
, RAW
, EMBSTR
, 三种方式的内存布局分别以下:
字符串对象的相关接口以下:
分类 | API名 | 功能 |
---|---|---|
建立接口 | robj *createEmbeddedStringObject(const char *ptr,size_t len) |
建立一个编码为EMBSTR 的字符串对象.即底层使用SDS, 且SDS与RedisObject位于同一块连续内存上 |
-- | robj *createRawStringObject(const char *ptr,size_t len) |
建立一个编码为RAW 的字符串对象.即底层使用SDS, 且SDS由RedisObject间接持有 内部是先用入参建立一个SDS, 而后用这个SDS再去调用 createObject |
-- | robj *createStringObject(const char *ptr,size_t len) |
建立一个字符串对象. 当 len 参数的值小于或等于OBJ_ENCODING_EMBSTR_SIZE_LIMIT 时, 编码方式为EMBSTR , 不然为RAW 内部是经过调用 createRawStringObject 与createEmbeddedStringObject 来建立不一样编码的字符串对象的 |
-- | robj *createStringObjectFromLongLong(long long value) |
根据整数值, 建立一个字符串对象. 若可复用全局共享字符串对象池中的对象, 则会尽可能复用. 不然以最节省内存的原则, 来决定对象的编码 |
-- | robj *createStringObjectFromLongDouble(long double value,int humanfriendly) |
根据浮点数值, 建立一个字符串对象 其中参数 humanfriendly 不为0, 则字符串以小数形式表达. 不然以exp计数法表达.根据字符串表达的长短, 编码多是RAW , 或EMBSTR |
释放接口 | void freeStringObject(robj *o) |
释放字符串对象. 若字符串对象底层使用SDS, 则调用 sdsfree 释放这个SDS.不然什么也不作 |
读写接口 | robj *dupStringObject(const robj *o) |
建立一个字符串对象的深拷贝副本. 不影响原字符串对象的引用计数. 建立的副本与原字符串毫无关联 |
-- | int isSdsRepresentableAsLongLong(sds s,long long *llval) |
判断SDS字符串是不是一个取值在long long 数值范围内的数值的字符串表达. 若是是, 就把相应的数值置在出参中内部调用的是 string2ll 来判断严格来说这不该该算是 RedisObject 的接口函数, 而应当算是SDS 的接口函数" |
-- | int isObjectRepresentableAsLongLong(robj *o,long long *llval) |
判断字符串对象是不是一个取值在long long 数值范围内的数值的字符串表达. 若是是, 就把相应的数值置在出参中. |
-- | robj *tryObjectEncoding(robj *o) |
尝试缩减这个字符串对象的内存占用. 策略为: 若是字符串对象表明的是一个位于 long 取值范围内的数值, 则尝试返回全局共享字符串对象池里的等价对象. 若因为服务器配置等缘由不成功, 则尝试将对象编码改成INT 若是以上都不成功, 则尝试将对象的编码改成 EMBSTR 若以上都不成功, 则在对象的编码为 RAW 的状态下, 至少调用sdsRemoveFreeSpace 来移除掉内部SDS中, 闲置的内存空间 |
-- | robj *getDecodedObject(robj *o) |
返回字符串对象的一个浅拷贝. 在编码为 RAW 或EMBSTR 时, 底层数据引用计数+1, 返回一个共享句柄在编码为 INT 时, 返回一个编码为RAW 或EMBSTR 的新副本的句柄. 新旧对象之间无关 |
-- | size_t stringObjectLen(robj *o) |
返回字符串对象中的字符个数 |
-- | int getDoubleFromObject(const robj *o,double *target) |
从字符串对象中解析出数值, 兼容整数值 |
-- | int getLongLongFromObject(robj *o,long long *target) |
从字符串对象中解析出整数值, 不兼容浮点数值 |
-- | int getLongDoubleFromObject(robj *o,long double *target) |
从字符串对象中解析出数值, 兼容整数值 |
-- | int compareStringObjects(robj *a, robj *b) |
二进制比较两个字符串对象. 如有字符串对象使用的是INT 编码, 则先会把ptr中的数值转化为字符串表达, 而后再去比较 |
-- | int collateStringObjects(robj *a, robj *b) |
底层调用strcoll去比较两个字符串对象. 比较的大小结果受LC_LOCALE 的影响 |
-- | int equalStringObjects(robj *a, robj *b) |
字符串判等 |
-- | #define sdsEncodedObject(objptr) |
宏, 判断字符串对象的内部是否为SDS实现. 即编码为RAW 或EMBSTR |
哈希对象的底层实现有两种, 一种是dict
, 一种是ziplist
. 分别对应编码HT
与ZIPLIST
. 而以前介绍的zipmap
这种结构, 虽然也是一种轻量级的字典结构, 且纵使在源代码中有相应的编码宏值, 但遗憾的是, 至Redis 4.0.10, 目前哈希对象的底层编码仍然只有ziplist
与dict
两种
dict
自没必要说, 自己就是字典类型, 存储键值对的. 用ziplist
做为底层数据结构时, 是将键值对以<key1><value1><key2><value2>...<keyn><valuen>
这样的形式存储在ziplist
中的. 两种编码内存布局分别以下:
上图中不严谨的地方有:
ziplist
中每一个entry, 除了键与值自己的二进制数据, 还包括其它字段, 图中没有画出来dict
底层可能持有两个dictht
实例dict
的哈希冲突须要注意的是: 当采用HT
编码, 即便用dict
做为哈希对象的底层数据结构时, 键与值均是以sds的形式存储的.
哈希对象的相关接口以下:
分类 | API名 | 功能 |
---|---|---|
建立接口 | robj *createHashObject(void) |
建立一个空哈希对象 底层编码使用 ZIPLIST , 即底层使用ziplist |
释放接口 | void freeHashObject(robj *o) |
释放哈希对象 若哈希对象底层使用的是dict, 则调用 dictRelease 释放这个dict若哈希对象底层使用的是ziplist, 则直接释放掉这个ziplist占用的连续内存空间 |
编码转换接口 | void hashTypeConvertZiplist(robj *o, int enc) |
将哈希对象的编码从ZIPLIST 转换为HT , 即底层实现从ziplist转为dict |
-- | void hashTypeConvert(robj *o, int enc) |
转换哈希对象的编码. 虽然接口设计的好像能够在底层编码之间互相转换, 但实际上这个接口的实现, 目前仅支持从 ZIPLIST 转向HT |
-- | void hashTypeTryConversion(robj *o,robj **argv,int start,int end) |
o 是一个哈希对象. argv 是其它对象的数组.(最好是字符串对象, 且为SDS实现)这个函数会检查 argv 数组中, 从start 到end 之间的全部对象, 若是这些对象中, 但凡是有一个对象是字符串对象, 且长度超过了用ziplist实现哈希对象时, ziplist的限长那么 o 这个哈希对象的编码就会从ZIPLIST 转为HT |
读写接口 | int hashTypeSet(robj *o,sds field,sds value,int flags) |
向哈希对象写入一个键值对. 在底层编码为 HT 时, flag 将影响插入键值对时的具体行为. flag 可有标志位 HASH_SET_TAKE_VALUE 与HASH_SET_TAKE_FIELD , 若对应位置1, 表明键与值直接引用参数值. 不然表明要调用sdsdup 接口拷贝键与值.在底层编码为 ZIPLIST 时, 键与值必然会被拷贝 |
-- | int hashTypeExists(robj *o, sds field) |
查询指定键在哈希对象中是否存在 |
-- | unsigned long hashTypeLength(const robj *o) |
查询哈希对象中的键值对总数 |
-- | int hashTypeGetFromZiplist(robj *o, sds field,unsigned char **vstr,unsigned int *vlen,long long *vll) |
从编码为ZIPLIST 的哈希对象中, 取出一个键对应的值. 键从 field 传入, 当值为数值类型时, 值以*vll 传出, 当值为二进制类型时, 值以*vstr 与*vlen 传出 |
-- | sds hashTypeGetFromHashTable(robj *o, sds field) |
从编码为HT 的哈希对象中, 取出一个键对应的值.键从 field 传入, 值以返回值传出. 若值不存在, 返回NULL" |
-- | "int hashTypeGetValue(robj *o,sds field,unsigned char **vstr,unsigned int *vlen,long long *vll) |
取出哈希对象中指定键对应的值. 若值是数值类型, 则以*vll 传出, 不然以*vstr 与*vlen 传出 |
-- | robj *hashTypeGetValueObject(robj *o, sds field) |
取出哈希对象中指定键对应的值, 并包装成RedisObject 返回. 返回的对象为字符串对象 |
-- | size_t hashTypeGetValueLength(robj *o, sds field) |
取出哈希对象中指定键对应的值的长度 |
-- | int hashTypeDelete(robj *o, sds field) |
删除哈希对象中的一个键值对. 键不存在时返回0, 成功删除返回1 |
迭代器接口 | hashTypeIterator *hashTypeInitIterator(robj *subject) |
在指定哈希对象上建立一个迭代器 |
-- | void hashTypeReleaseIterator(hashTypeIterator *hi) |
释放哈希对象的迭代器 |
-- | int hashTypeNext(hashTypeIterator *hi) |
让哈希迭代器步进一步 |
-- | void hashTypeCurrentFromZiplist(hashTypeIterator *hi,int what,unsigned char **vstr,unsigned int *vlen,long long *vll) |
取出哈希对象迭代器当前指向的键 或值. 当what 传入OBJ_HASH_KEY 时, 取的是键, 不然取的是值.注意, 该函数仅在哈希对象的编码为 ZIPLIST 时才能正确运行 |
-- | sds hashTypeCurrentFromHashTable(hashTypeIterator *hi,int what) |
取出哈希对象迭代器当前指向的键 或值. 当what 传入OBJ_HASH_KEY 时, 取的是键, 不然取的是值.注意, 该函数仅在哈希对象的编码为 HT 时才能正确运行 |
-- | void hashTypeCurrentObject(hashTypeIterator *hi,int what,unsigned char **vstr,unsigned int *vlen,long long *vll) |
取出哈希对象迭代器当前指向的键或值. 当what 传入OBJ_HASH_KEY 时, 取的是键, 不然取的是值. |
-- | sds hashTypeCurrentObjectNewSds(hashTypeIterator *hi,int what) |
取出哈希对象迭代器当前指向的键或值. 且把键或值以一个全新的SDS字符串返回. 当what 传入OBJ_HASH_KEY 时, 取的是键, 不然取的是值. |
列表对象的底层实现, 历史上是有两种的, 分别是ziplist
与list
, 但截止Redis 4.0.10版本, 全部的列表对象API都再也不支持除去quicklist
以外的任何底层实现. 也就是说, 目前(Redis 4.0.10), 列表对象支持的底层实现实质上只有一种, 便是quicklist
.
列表对象的建立API依然支持从ziplist
的实例建立一个列表对象, 即你能够建立一个底层编码为ZIPLIST
的列表对象, 但若是用该列表对象去调用任何其它列表对象的API, 都会致使panic. 在使用以前, 你只能再次调用相关的底层编码转换接口, 将这个列表对象的底层编码转换为QUICKLIST
.
而且遗憾的是, LINKEDLIST
这种编码, 即底层为list
的列表, 被完全淘汰了. 也就是说, 截止目前(Redis 4.0.10), Redis定义的10个对象编码方式宏名中, 有两个被彻底闲置了, 分别是: OBJ_ENCODING_ZIPMAP
与OBJ_ENCODING_LINKEDLIST
. 从Redis的演进历史上来看, 前者是后续可能会获得支持的编码值, 后者则应该是被完全淘汰了.
列表对象的内存布局以下图所示:
列表对象的API接口以下:
分类 | API名 | 功能 |
---|---|---|
建立接口 | robj *createQuicklistObject(void) |
建立一个列表对象. 内部编码为QUICKLIST 即内部使用quicklist实现的列表对象 |
-- | robj *createZiplistObject(void) |
建立一个列表对象. 内部编码为ZIPLIST 即内部使用ziplist实现的列表对象 |
释放接口 | void freeListObject(robj *o) |
释放一个列表对象 |
编码转换接口 | void listTypeConvert(robj *subject, int enc) |
转换列表对象的内部编码. 虽然接口设计的好你能够在底层编码之间互相转换, 但实际上这个接口的实现, 目前仅支持从 ZIPLIST 转换为QUICKLIST 而且蛋疼的是, 4.0.10这个版本中, 全部的列表对象操做API内部实现都仅支持编码方式为 QUICKLIST 的列表对象, 其它编码方式会panic.因此目前为止, 这个API的惟一做用, 就是配合 createZiplistObject 接口, 来使用一个ziplist建立一个内部编码为QUICKLIST 的列表对象. |
读写接口 | void listTypePush(robj *subject,robj *value,int where) |
向列表对象中添加一个数据. 由 where 参数的值控制是在头部添加, 仍是尾部添加.where 可选的值为LIST_HEAD , LIST_TAIL |
-- | robj *listTypePop(robj *subject,int where) |
从列表对象的头部或尾部取出一个数据. 取出的数据经过被包装成字符串对象后返回. 具体取出位置经过参数 where 控制 |
-- | unsigned long listTypeLength(const robj *subject) |
获取列表对象中保存的数据的个数 |
-- | void listTypeInsert(listTypeEntry *entry,robj *value, int where) |
将字符串对象中的数据插入到列表对象的头部或尾部. 插入过程当中不会拷贝字符串对象持有的数据自己. 但会缩减字符串对象的引用计数. |
-- | int listTypeEqual(listTypeEntry *entry, robj *o) |
判断字符串对象o 与列表对象中指定位置上存储的数据是否相同. |
-- | robj *listTypeGet(listTypeEntry *entry) |
获取列表对象中指定位置的数据. 位置信息经过 entry 传入, 这是一个入参. 数据将拷贝一份后经过SDS形式返回 |
迭代器接口 | listTypeIterator *listTypeInitIterator(robj *subject,long index,unsigned char direction) |
建立一个列表对象迭代器 |
-- | void listTypeReleaseIterator(listTypeIterator *li) |
释放一个列表对象迭代器 |
-- | int listTypeNext( listTypeIterator *li, listTypeEntry *entry) |
让列表对象迭代器步进一步, 并将步进以前迭代器所指向的数据保存在entry 中 |
-- | void listTypeDelete( listTypeIterator *iter, listTypeEntry *entry) |
删除列表迭代器当前指向的列表对象中存储的数据. 被删除的数据经过 entry 返回 |
集合对象的底层实现有两种, 分别是intset
和dict
. 分别对应编码宏中的INTSET
和HT
. 显然当使用intset
做为底层实现的数据结构时, 集合中存储的只能是数值数据, 且必须是整数. 而当使用dict
做为集合对象的底层实现时, 是将数据所有存储于dict
的键中, 值字段闲置不用.
集合对象的内存布局以下图所示:
集合对象的API接口以下:
分类 | API名 | 功能 |
---|---|---|
建立接口 | robj *createSetObject(void) |
建立一个空集合对象. 底层编码使用 HT , 即底层使用dict |
-- | robj *createIntsetObject(void) |
建立一个空集合对象. 底层编码使用 INTSET , 即底层使用intset |
-- | robj *setTypeCreate(sds value) |
建立一个空集合对象. 注意入参虽然携带了一个数据, 但这个数据并不会存储在集合中 这个数据只起到决定编码方式的做用, 若这个数据是数值的字符串表达, 则底层编码则为 INTSET , 不然为HT |
释放接口 | void freeSetObject(robj *o) |
释放集合对象. 若集合对象底层使用的是dict, 则调用 dictRelease 释放这个dict若集合对象底层使用的是intset, 则直接释放这个intset占用的连续内存 |
编码转换接口 | void setTypeConvert(robj *setobj, int enc) |
转换集合对象的内部编码 虽然接口设计的好你能够在底层编码之间互相转换, 但实际上这个接口的实现, 目前仅支持从 INTSET 转换为HT |
读写接口 | int setTypeAdd(robj *subject, sds value) |
向集合对象中写入一个数据 |
-- | int setTypeRemove(robj *setobj, sds value) |
删除集合对象中的一个数据 |
-- | int setTypeIsMember(robj *subject, sds value) |
判断指定数据是否在集合对象中 |
-- | int setTypeRandomElement(robj *setobj, sds *sdsele, int64_t *llele) |
从集合对象中, 随机选出一个数据, 将其数据经过出参返回. 若数据是数值类型, 则从 *llele 返回, 不然, 从*sdsele 返回.注意该接口若取得二进制数据, 则 *sdsele 是直接引用集合内的数据, 而不是拷贝一份 |
-- | unsigned long setTypeSize(const robj *subject) |
返回集合中数据的个数 |
迭代器接口 | setTypeIterator *setTypeInitIterator(robj *subject) |
建立一个集合对象迭代器 |
-- | void setTypeReleaseIterator(setTypeIterator *si) |
释放集合对象迭代器 |
-- | int setTypeNext( setTypeIterator *si, sds *sdsele, int64_t *llele) |
让集合迭代器步进一步, 并从出参中返回步进前迭代器所指向的数据. 若数据是数值类型, 则从 *llele 返回, 不然, 从*sdsele 返回注意该接口若取得二进制数据, 则 *sdsele 是直接引用集合内的数据, 而不是拷贝一份 |
-- | sds setTypeNextObject(setTypeIterator *si) |
让集合迭代器步进一步, 并把步进前所指向的数据, 拷贝一份, 构形成一个新的SDS, 做为返回值返回 |
有序集合的底层实现依然有两种, 一种是使用ziplist
做为底层实现, 另一种比较特殊, 底层使用了两种数据结构: dict
与skiplist
. 前者对应的编码值宏为ZIPLIST
, 后者对应的编码值宏为SKIPLIST
使用ziplist
来实如今序集合很容易理解, 只须要在ziplist
这个数据结构的基础上作好排序与去重就能够了. 使用zskiplist
来实现有序集合也很容易理解, Redis中实现的这个跳跃表彷佛自然就是为了实现有序集合对象而实现的, 那么为何还要辅助一个dict
实例呢? 咱们先看来有序集合对象在这两种编码方式下的内存布局, 而后再作解释:
首先是编码为ZIPLIST
时, 有序集合的内存布局以下:
而后是编码为SKIPLIST
时, 有序集合的内存布局以下:
在使用dict
与skiplist
实现有序集合时, 跳跃表负责按分数索引, 字典负责按数据索引. 跳跃表按分数来索引, 查找时间复杂度为O(lgn). 字典按数据索引时, 查找时间复杂度为O(1). 设想若是没有字典, 若是想按数据查分数, 就必须进行遍历. 两套底层数据结构均只做为索引使用, 即不直接持有数据自己. 数据被封装在SDS中, 由跳跃表与字典共同持有. 而数据的分数则由跳跃表结点直接持有(double类型数据), 由字典间接持有.
有序集合对象的API接口以下:
分类 | API名 | 功能 |
---|---|---|
建立接口 | robj *createZsetObject(void) |
建立一个有序集合对象 默认内部编码为 SKIPLIST , 即内部使用zskiplist与dict来实现有序集合 |
-- | robj *createZsetZiplistObject(void) |
建立一个有序集合对象 指定内部编码为 ZIPLIST , 即内部使用ziplist来实现有序集合 |
释放接口 | void freeZsetObject(robj *o) |
释放一个有序集合对象 |
编码转换接口 | void zsetConvert(robj *zobj, int encoding) |
转换有序集合对象的内部编码 能够在 ZIPLIST 与SKIPLIST 两种编码间转换 |
-- | void zsetConvertToZiplistIfNeeded(robj *zobj, size_t maxelelen) |
判断当前有序集合对象是否有必要将底层编码转换为ZIPLIST , 若是有必要, 就执行转换 |
读写接口 | int zsetScore(robj *zobj, sds member, double *score) |
获取有序集合中, 指定数据的得分. 数据由 member 参数携带, 经过二进制判等的方式匹配 |
-- | int zsetAdd( robj *zobj, double score, sds ele, int *flags, double *newscore) |
向有序集合中添加数据, 或更新已存在的数据的得分.flag 是一个in-out参数, 其做为入参, 控制函数的具体行为, 其做为出参, 报告函数执行的结果.做为入参时, *flags 的语义以下:ZADD_INCR 递增已存在的数据的得分. 若是数据不存在, 则添加数据, 并设置得分. 且若newscore != NULL , 执行操做后, 数据的得分还会赋值给*newscore ZADD_NX 仅当数据不存在时, 执行添加数据并设置得分, 不然什么也不作ZADD_XX 仅当数据存在时, 执行重置数据得分. 不然什么也不作做为出参, *flags 的语义以下:ZADD_NAN 数据的得分不是一个数值, 表明内部出现的异常ZADD_ADDED 新数据已经添加至集合中ZADD_UPDATED 数据的得分已经更新ZADD_NOP 函数什么也没作 |
-- | int zsetDel(robj *zobj, sds ele) |
从有序集合中移除一个数据 |
-- | long zsetRank(robj *zobj, sds ele, int reverse) |
获取有序集合中, 指定数据的排名. 若 reverse==0 , 排名以得分升序排列. 不然排名以得分降序排列.第一个数据的排名为0, 而不是1 |
-- | unsigned int zsetLength(const robj *zobj) |
获取有序集合对象中存储的数据个数 |