基本数据结构上
简单动态字符串(SDS)
- 数据结构
redis为了节省内存,针对不同的长度的数据采用不同的数据结构。如下共五种,但SDS_TYPE_5并不使用,因为该类型不会存放数据长度,每次都需要进行分配和释放:
#define SDS_TYPE_5 0
#define SDS_TYPE_8 1
#define SDS_TYPE_16 2
#define SDS_TYPE_32 3
#define SDS_TYPE_64 4
以type=1为例:
typedef char* sds
/*
__attribute__ ((packed)) 的作用就是告诉编译器取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐,是GCC特有的语法
*/
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; /* 数据长度 */
uint8_t alloc; /* 去掉头和null结束符,有效长度+数据长度*/
unsigned char flags; /* 3 lsb of type, 5 unused bits,小端*/
//变长数据
char buf[];
};
-
空间扩容
-
当前有效长度>=新增长度,直接返回
-
更新之后,判断新旧类型是否一致:
- 一致使用remalloc,否则使用malloc+freea.当前有效长度>=新增长度,直接返回
-
增长步长:
- 新增后长度小于预分配长度(1024*1024),扩大一倍;
- 新增后长度大于等于预分配的长度,每次加预分配长度
-
sds sdsMakeRoomFor(sds s, size_t addlen) {
void *sh, *newsh;
size_t avail = sdsavail(s);
size_t len, newlen;
char type, oldtype = s[-1] & SDS_TYPE_MASK;
int hdrlen;
/* Return ASAP if there is enough space left. */
//当前有效长度>=新增长度,直接返回
if (avail >= addlen) return s;
len = sdslen(s);
sh = (char*)s-sdsHdrSize(oldtype);
newlen = (len+addlen);
//新增后长度小于预分配长度(1024*1024),扩大一倍;SDS_MAX_PREALLOC=1024*1024
if (newlen < SDS_MAX_PREALLOC)
newlen *= 2;
//新增后长度大于等于预分配的长度,每次加预分配长度
else
newlen += SDS_MAX_PREALLOC;
type = sdsReqType(newlen);
/* Don't use type 5: the user is appending to the string and type 5 is
* not able to remember empty space, so sdsMakeRoomFor() must be called
* at every appending operation. */
if (type == SDS_TYPE_5) type = SDS_TYPE_8;
// 一致使用remalloc,否则使用malloc+freea.当前有效长度>=新增长度,直接返回
hdrlen = sdsHdrSize(type);
if (oldtype==type) {
newsh = s_realloc(sh, hdrlen+newlen+1);
if (newsh == NULL) return NULL;
s = (char*)newsh+hdrlen;
} else {
/* Since the header size changes, need to move the string forward,
* and can't use realloc */
newsh = s_malloc(hdrlen+newlen+1);
if (newsh == NULL) return NULL;
memcpy((char*)newsh+hdrlen, s, len+1);
s_free(sh);
s = (char*)newsh+hdrlen;
s[-1] = type;
sdssetlen(s, len);
}
sdssetalloc(s, newlen);
return s;
}
-
空间缩容
- 在trim操作时,采用采用的是惰性空间释放即:不会立即使用内存重分配来回收缩短的字节,只是进行移动和标记,并修改数据长度。
sds sdstrim(sds s, const char *cset) { char *start, *end, *sp, *ep; size_t len; sp = start = s; ep = end = s+sdslen(s)-1; while(sp <= end && strchr(cset, *sp)) sp++; while(ep > sp && strchr(cset, *ep)) ep--; len = (sp > ep) ? 0 : ((ep-sp)+1); if (s != sp) memmove(s, sp, len); s[len] = '\0'; sdssetlen(s,len); return s; }
思考:这里为什么要用memmove,而不是用memcpy?
- 真正的删除被放在后续操作中见tryObjectEncoding
if (o->encoding == OBJ_ENCODING_RAW &&
sdsavail(s) > len/10)
{
o->ptr = sdsRemoveFreeSpace(o->ptr);
}
-
优点
- 常量获取字符串长度(len)
- 避免缓冲区溢出
- 减少字符串修改带来的内存频繁重分配次数
- 二进制操作安全:可以保持文本数据,也可以保持任意格式的二进制数据(如视频流数据)
- 以’\0’结尾,使其兼容部分C字符串函数
-
其他
- sds是char*的别名,可以理解为分配的是一块连续内存(表头+数据),根据局部性原理可以提高访问速度。
- 利用C语言内存布局,在sds结构体中使用了一个0长度的数组,既可以达到变长,又能保证内存也是连续的,因此在sds一列操作中,看到使用s[-1]这样的操作搞到惊讶,当然这里的s指向的是buf位置。
/* sdsalloc() = sdsavail() + sdslen() */ static inline size_t sdsalloc(const sds s) { //主要是看这句 unsigned char flags = s[-1]; //省略 return 0; }
双向链表
- 数据结构
redis的链表没啥特别之处,就是普通的双向链表。
/* Node, List, and Iterator are the only data structures used currently. */
typedef struct listNode {
struct listNode *prev;
struct listNode *next;
void *value;
} listNode;
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;
- 迭代器
这里链表的迭代和数据是分开的,采用了类似迭代器模式,这个思想被用到很多场景如后面要分析的leveldb中,可以搜索一下迭代器设计模式。
typedef struct listIter {
listNode *next;
int direction;
} listIter;
listIter *listGetIterator(list *list, int direction)
{
listIter *iter;
if ((iter = zmalloc(sizeof(*iter))) == NULL) return NULL;
if (direction == AL_START_HEAD)
iter->next = list->head;
else
iter->next = list->tail;
iter->direction = direction;
return iter;
}
字典
数据结构
- 数据结构
typedef struct dictEntry {
//key
void *key;
//这里采用了union类型,节省内存
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
//下一个节点,因此可以看出来,采用的链地址法.针对新节点采用头插法
struct dictEntry *next;
} dictEntry;
//dict相关的操作函数,以函数指针的方式存在
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;
//每个hashtable都会有两个,将旧的复制到新的
/* 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; //hash表的大小
unsigned long sizemask;//mask计算索引值
unsigned long used;//表示已经使用的个数
} dictht;
typedef struct dict {
dictType *type;
void *privdata;
dictht ht[2];//新的dictht和旧的dictht,一般只会使用0,ht[1]哈希表只会对ht[0]哈希表进行rehash操作
//如果rehashidx==-1表示不会进行rehash
long rehashidx; /* rehashing not in progress if rehashidx == -1 */
unsigned long iterators; /* number of iterators currently running */
} dict;
- 基本字段解释
字段名 | 描述 | 相关代码 |
---|---|---|
dictht中的size | hash表的长度,其值为大于参数size的最小2的倍数 |
|
| dict中的rehashindex | 标记是否正在进行rehash,默认值-1表示未进行,n表示当前dt[0]中的第n个桶 | 等于-1:
不等于-1:
|
| dict中的sizemask | 用于计算当前的key位于哪一个dictEntry。其大小等于size-1。这样做的目的是因为size为2的倍数,使用&运算要比%性能更高 | |
| dict中的ht | dictht在第一次初始化时只会启用ht[0],ht[1]在整个dict扮演着临时存储的作用 |
|
- 哈希算法
在redis中hash默认使用的是siphash算法(当然也可以自定义)。计算哈希值和索引值的方法如下:
1、使用字典设置的哈希函数,计算键 key 的哈希值
hash = dict->type->hashFunction(key);
2、使用哈希表的sizemask属性和第一步得到的哈希值,计算索引值
index = hash & dict->ht[x].sizemask;相当于取模运算
- 哈希冲突
redis解决哈希冲突的方法是链地址法,而且采用的是头插式。见<redis设计与实现>一书
- resize-扩容
采用链地址法来解决冲突,如果数据量太大,大量的冲突会导致某个桶上的链表非常长,不利于数据查询,因此需要根据负载因子(负载因子用来评估键冲突的概率)来判断当前是否需要进行扩容,见函数_dictExpandIfNeeded。其中resize还与是否正在进行持久化有关:
void updateDictResizePolicy(void) {
if (server.rdb_child_pid == -1 && server.aof_child_pid == -1)
dictEnableResize();
else
dictDisableResize();
}
_dictExpandIfNeeded函数如下:
前提条件:factor=d->ht[0].used/d->ht[0].size>=1,也可以看做是非持久化,
持久化: factor=d->ht[0].used/d->ht[0].size>dict_force_resize_ratio(5)
static int _dictExpandIfNeeded(dict *d)
{
//如果rehash正在进行,那么就不不需要进行扩展
/* Incremental rehashing already in progress. Return. */
if (dictIsRehashing(d)) return DICT_OK;
//如果ht[0]的size为空,那么扩展为默认大小
/* If the hash table is empty expand it to the initial size. */
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. */
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;
}
当然如果扩容无效,或者正在rehash操作,也不进行resize操作,见内部的dictExpand函数
- resize-缩容
redis中进行resize的条件是由两处决定的即htNeedsResize和dictResize。
- 超过了初始值且填充率小于10%,这个说明需要缩容。
- 可以resize(这个条件是持久化状态优先不能进行resize)或者没有正在进行rehash操作(这说明每次只能进行一次)
/* Hash table parameters */
#define HASHTABLE_MIN_FILL 10 /* Minimal hash table fill 10% */
/* This is the initial size of every hash table */
#define DICT_HT_INITIAL_SIZE 4
int htNeedsResize(dict *dict) {
long long size, used;
size = dictSlots(dict);
used = dictSize(dict);
return (size > DICT_HT_INITIAL_SIZE &&
(used*100/size < HASHTABLE_MIN_FILL));
}
int dictResize(dict *d)
{
int minimal;
if (!dict_can_resize || dictIsRehashing(d)) return DICT_ERR;
//如果至少要保证,最少应该为初始化的4
minimal = d->ht[0].used;
if (minimal < DICT_HT_INITIAL_SIZE)
minimal = DICT_HT_INITIAL_SIZE;
return dictExpand(d, minimal);
}
其调用时机被分为两块:相关数据类型的删除操作中以及redis的定时任务(见databasesCron函数)中。
void databasesCron(void) {
//......
/* Resize */
for (j = 0; j < dbs_per_call; j++) {
tryResizeHashTables(resize_db % server.dbnum);
resize_db++;
}
//......
}
具体见最后一张图:
- rehash
上文无论是扩容还是缩容,不一定会导致rehash,具体要看是否是第一次,如果是第一次的话,不会触发rehash,具体看dictExpand函数,最后面的关键行。
/* Expand or create the hash table */
int dictExpand(dict *d, unsigned long 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;
dictht n; /* the new hash table */
unsigned long realsize = _dictNextPower(size);
//如果扩容后,和之前本身的大小一致,那就完全没必要进行扩容
/* 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*));
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. */
if (d->ht[0].table == NULL) {
d->ht[0] = n;
return DICT_OK;
}
//表明启用第二个,开始进行rehash操作
/* Prepare a second hash table for incremental rehashing */
d->ht[1] = n;
d->rehashidx = 0;
return DICT_OK;
}
至于rehash时机,请看下个标签rehash优化。
- rehash优化
- rehash过程,ht[0]中会存在空节点,为了防止阻塞,每次限定如果连续访问10*N(N表示步长)个空节点后,直接返回。见dictRehash函数
- 渐进式hash。是指rehash操作不是一次性、集中式完成的。
- 第一类:正常操作见上面的dictRehash函数。但是对于Redis来说,如果Hash表的key太多,这样可能导致rehash操作需要长时间进行,阻塞服务器,所以Redis本身将rehash操作分散在了后续的每次增删改查中(以桶为单位)。
- 第二类:针对第一类间接式rehash,存在一个问题:如果a服务器长时间处于空闲状态,导致哈希表长期使用0和1号两个表。为解决这个问题,在serverCron定时函数中,每次拿出1ms时间来执行Rehash操作,每次步长为100,但需要开启activerehashing。见databasesCron函数
void databasesCron(void) {
/* Perform hash tables rehashing if needed, but only if there are no
* other processes saving the DB on disk. Otherwise rehashing is bad
* as will cause a lot of copy-on-write of memory pages. */
if (server.rdb_child_pid == -1 && server.aof_child_pid == -1) {
/* We use global counters so if we stop the computation at a given
* DB we'll be able to start from the successive in the next
* cron loop iteration. */
static unsigned int resize_db = 0;
static unsigned int rehash_db = 0;
int dbs_per_call = CRON_DBS_PER_CALL;
int j;
/* Don't test more DBs than we have. */
if (dbs_per_call > server.dbnum) dbs_per_call = server.dbnum;
/* Resize */
for (j = 0; j < dbs_per_call; j++) {
tryResizeHashTables(resize_db % server.dbnum);
resize_db++;
}
/* Rehash:渐进式hash */
if (server.activerehashing) {
for (j = 0; j < dbs_per_call; j++) {
int work_done = incrementallyRehash(rehash_db);
if (work_done) {
/* If the function did some work, stop here, we'll do
* more at the next cron loop. */
break;
} else {
/* If this db didn't need rehash, we'll try the next one. */
rehash_db++;
rehash_db %= server.dbnum;
}
}
}
}
}
这里需要注意的是,在定时任务中不能花太长时间,防止其阻塞其他操作。
int incrementallyRehash(int dbid) {
/* Keys dictionary */
if (dictIsRehashing(server.db[dbid].dict)) {
dictRehashMilliseconds(server.db[dbid].dict,1);
return 1; /* already used our millisecond for this loop... */
}
/* Expires */
if (dictIsRehashing(server.db[dbid].expires)) {
dictRehashMilliseconds(server.db[dbid].expires,1);
return 1; /* already used our millisecond for this loop... */
}
return 0;
}
-
rehash注意点
-
rehash过程dictFind和dictDelete,需要涉及到0和1两个表
-
Redis在持久化时,服务器执行扩展操作所需要的负载因子并不相同,默认为5。
-
rehash过程dictAdd,只插入ht[1]中,确保ht[0]只减不增
-
ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0];
迭代器
- 数据结构
typedef struct dictIterator {
dict *d; // 目标字典对象
long index; // 当前遍历的槽位置,初始化为-1
int table; // ht[0] or ht[1]
int safe; // 表示迭代器是否安全
dictEntry *entry; // 迭代器当前指向的对象
dictEntry *nextEntry; // 迭代器下一个指向的对象
long long fingerprint; // 迭代器指纹,放置迭代过程中字典被修改
} dictIterator;
- 迭代器
redis的dict迭代器分为两种类型,安全迭代器(safe=1)和非安全迭代器(safe=0)。
// 获取非安全迭代器,只读迭代器,允许rehashStep
dictIterator *dictGetIterator(dict *d)
{
dictIterator *iter = zmalloc(sizeof(*iter));
iter->d = d;
iter->table = 0;
iter->index = -1;
iter->safe = 0;
iter->entry = NULL;
iter->nextEntry = NULL;
return iter;
}
// 获取安全迭代器,允许触发过期处理,禁止rehashStep
dictIterator *dictGetSafeIterator(dict *d) {
dictIterator *i = dictGetIterator(d);
i->safe = 1;
return i;
}
安全模式下,支持边遍历边修改(字典key的过期判断),支持字典的添加,删除,查找等操作,但是不支持rehash操作(避免重复遍历)。
非安全模式下,只支持只读操作,使用字典的删除、添加、查找等方法会造成不可预期的问题,如重复遍历元素或者漏掉元素,但支持rehash操作。
- 迭代器选择
-
为避免元素的重复遍历,必须使用迭代器的安全模式,如bgaofwrite以及bgsave操作。
-
遍历过程中需要处理元素,此时也必须要使用安全迭代器,如keys命令。
-
允许遍历过程中出现个别元素重复时,选择非安全迭代器
线段跳表
常规线段跳表
- 定义
是一种可以代替平衡树的数据结构,可以看做是并联的有序链表。跳跃表通过概率保证平衡,而平衡树采用严格的旋转来保证平衡,因此跳跃表比较容易实现,而且相比平衡树有着较高的运行效率。其中Redis默认的最大level为64。
-
常规操作
- 初始化
- 插入
- 查找
- 删除
Redis中的线段跳表
- 数据结构(多线程版的请关注后续leveldb分析)
#define ZSKIPLIST_MAXLEVEL 64 /* Should be enough for 2^64 elements */
#define ZSKIPLIST_P 0.25 /* Skiplist P = 1/4 */
//可能一个节点会存在多层
/* ZSETs use a specialized version of Skiplists */
typedef struct zskiplistNode {
sds ele;//成员对象
double score;//各个节点中分值,在跳跃表中,节点按各自所保存的分值从小到大排列
struct zskiplistNode *backward;//指向位于当前节点的前一个节点。后退指针在程序从表尾想表头遍历时使用
struct zskiplistLevel {
struct zskiplistNode *forward;//用于访问位于表尾方向的其他节点,当程序从表头向表尾进行遍历时,访问会沿着层的前进指针进行
unsigned long span;//跨度,他和遍历操作无关,forward才是用来遍历操作的。跨度实际上是用来计算排位的。
} level[];
} zskiplistNode;
typedef struct zskiplist {
struct zskiplistNode *header, *tail;
unsigned long length;//节点个数
int level;//总层数
} zskiplist;
- 特点
-
排序按照score来排序,如果是score相等,那么则按照ele来排序
-
平均查询时间复杂度为O(logn)。
- 常见操作
- level计算
#define ZSKIPLIST_MAXLEVEL 64 /* Should be enough for 2^64 elements */
#define ZSKIPLIST_P 0.25 /* Skiplist P = 1/4 */
int zslRandomLevel(void) {
int level = 1;
while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
level += 1;
return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}
由上式可以,假设level每增加一层的概率为p。
节点层数为1,概率1-p
节点层数为2,概率p(1-p)
节点层数为3,概率p*p(1-p)
节点层数为4,概率ppp(1-p)
…
节点平均层数为=1(1-p)+2(1-p)p+3pp(1-p)+…=1/(1-p),带入redis的0.25,计算出每个节点的平均指针数1.33
- 插入
排序按照score来排序,如果是score相等,那么则按照ele来排序
- 查找和删除旧不再给出了
整数集合(intset)
- 定义
整数集合(intset)是redis用于保存整数值的集合抽象数据结构,他可以保存类型为16、32或者64位的整数值,且保证集合中不会出现重复元素,数据也是从小到大存储。
#define INTSET_ENC_INT16 (sizeof(int16_t))
#define INTSET_ENC_INT32 (sizeof(int32_t))
#define INTSET_ENC_INT64 (sizeof(int64_t))
/* Return the required encoding for the provided value. */
static uint8_t _intsetValueEncoding(int64_t v) {
if (v < INT32_MIN || v > INT32_MAX)
return INTSET_ENC_INT64;
else if (v < INT16_MIN || v > INT16_MAX)
return INTSET_ENC_INT32;
else
return INTSET_ENC_INT16;
}
- 数据结构
typedef struct intset {
//编码方式
uint32_t encoding;
//contents数组长度
uint32_t length;
//内容,contents具体的内容要视encoding情况而定
int8_t contents[];
} intset;
intset优化点
- 查找
intset查找(intsetSearch)采用的是折半查找方式,时间复杂度为O(logN)
static uint8_t intsetSearch(intset *is, int64_t value, uint32_t *pos) {
int min = 0, max = intrev32ifbe(is->length)-1, mid = -1;
int64_t cur = -1;
/* The value can never be found when the set is empty */
if (intrev32ifbe(is->length) == 0) {
if (pos) *pos = 0;
return 0;
} else {
/* Check for the case where we know we cannot find the value,
* but do know the insert position. */
if (value > _intsetGet(is,max)) {
if (pos) *pos = intrev32ifbe(is->length);
return 0;
} else if (value < _intsetGet(is,0)) {
if (pos) *pos = 0;
return 0;
}
}
//折半查找
while(max >= min) {
//这里防止
mid = ((unsigned int)min + (unsigned int)max) >> 1;
cur = _intsetGet(is,mid);
if (value > cur) {
min = mid+1;
} else if (value < cur) {
max = mid-1;
} else {
break;
}
}
if (value == cur) {
if (pos) *pos = mid;
return 1;
} else {
if (pos) *pos = min;
return 0;
}
}
- 插入与升级
当新插入的元素类型大于当前intset类型时,为防止溢出,会对其进行升级操作。见图中的“是”分支。
升级实例:移动的过程中需要判断插入的值的插入位置取决于输入值是否为负值。
- 升级分析
- 提升灵活性
可以通过自动升级底层数组来适应新元素,所以可以将任意类型的整数添加至集合,而不必担心类型错误
- 节约内存
不同类型采用不同的类型的空间对其存储,从而避免空间浪费
- 降级:不支持降级
- 添加和删除
均需要进行remalloc操作,因此慎用。
压缩列表(ziplist)
数据结构
- 定义
压缩列表是由一系列特殊编码的连续内存块组成的顺序型数据结构,一个压缩列表可以包含任意多个节点,每个节点可以保存一个字节数组或者一个整数值。适合存储小对象和长度有限的数据。
- 数据结构
#define ZIP_END 255 /* Special "end of ziplist" entry. */
#define ZIP_BIG_PREVLEN 254 /* Max number of bytes of the previous entry, for
the "prevlen" field prefixing each entry, to be
represented with just a single byte. Otherwise
it is represented as FF AA BB CC DD, where
AA BB CC DD are a 4 bytes unsigned integer
representing the previous entry len. */
/* Different encoding/length possibilities */
#define ZIP_STR_MASK 0xc0 //1100 0000 字节数组的掩码
#define ZIP_INT_MASK 0x30 //0011 0000 整型掩码
#define ZIP_STR_06B (0 << 6) //0000 0000
#define ZIP_STR_14B (1 << 6)//0100 0000
#define ZIP_STR_32B (2 << 6) //1000 0000
#define ZIP_INT_16B (0xc0 | 0<<4) //1100 0000 | 0000 0000
#define ZIP_INT_32B (0xc0 | 1<<4)//1100 0000 | 0001 0000 =1101 0000
#define ZIP_INT_64B (0xc0 | 2<<4)//1100 0000 | 0010 0000 =1110 0000
#define ZIP_INT_24B (0xc0 | 3<<4)//1100 0000 | 0111 0000 =1111 0000
#define ZIP_INT_8B 0xfe //=1111 1110
/* 4 bit integer immediate encoding |1111xxxx| with xxxx between
* 0001 and 1101. */
#define ZIP_INT_IMM_MASK 0x0f /* Mask to extract the 4 bits value. To add
one is needed to reconstruct the value. */
#define ZIP_INT_IMM_MIN 0xf1 /* 11110001 */
#define ZIP_INT_IMM_MAX 0xfd /* 11111101 */
#define INT24_MAX 0x7fffff
#define INT24_MIN (-INT24_MAX - 1)
//判断是否字节数组,注意字节数组没有以"11"开头的
/* Macro to determine if the entry is a string. String entries never start
* with "11" as most significant bits of the first byte. */
#define ZIP_IS_STR(enc) (((enc) & ZIP_STR_MASK) < ZIP_STR_MASK)
/* Utility macros.*/
//从起始位置开始计算的uint32_t记录的是ziplist的总字节数
/* Return total bytes a ziplist is composed of. */
#define ZIPLIST_BYTES(zl) (*((uint32_t*)(zl)))
//从起始位置+uint32_t开始计算,往后uint32_t记录的是ziplist的tail偏移量
/* Return the offset of the last item inside the ziplist. */
#define ZIPLIST_TAIL_OFFSET(zl) (*((uint32_t*)((zl)+sizeof(uint32_t))))
//ziplist总长度
/* Return the length of a ziplist, or UINT16_MAX if the length cannot be
* determined without scanning the whole ziplist. */
#define ZIPLIST_LENGTH(zl) (*((uint16_t*)((zl)+sizeof(uint32_t)*2)))
/* The size of a ziplist header: two 32 bit integers for the total
* bytes count and last item offset. One 16 bit integer for the number
* of items field. */
#define ZIPLIST_HEADER_SIZE (sizeof(uint32_t)*2+sizeof(uint16_t))
/* Size of the "end of ziplist" entry. Just one byte. */
#define ZIPLIST_END_SIZE (sizeof(uint8_t))
/* Return the pointer to the first entry of a ziplist. */
#define ZIPLIST_ENTRY_HEAD(zl) ((zl)+ZIPLIST_HEADER_SIZE)
//通过偏移量,达到尾部
/* Return the pointer to the last entry of a ziplist, using the
* last entry offset inside the ziplist header. */
#define ZIPLIST_ENTRY_TAIL(zl) ((zl)+intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl)))
//实体的尾部
/* Return the pointer to the last byte of a ziplist, which is, the
* end of ziplist FF entry. */
#define ZIPLIST_ENTRY_END(zl) ((zl)+intrev32ifbe(ZIPLIST_BYTES(zl))-1)
//我们知道如果列表的长度超过了UINT16_MAX,此时zllen不在表示节点的个数,
//如果想要知道节点个数,那么必须要进行遍历
#define ZIPLIST_INCR_LENGTH(zl,incr) { \
if (ZIPLIST_LENGTH(zl) < UINT16_MAX) \
ZIPLIST_LENGTH(zl) = intrev16ifbe(intrev16ifbe(ZIPLIST_LENGTH(zl))+incr); \
}
/* We use this function to receive information about a ziplist entry.
* Note that this is not how the data is actually encoded, is just what we
* get filled by a function in order to operate more easily. */
typedef struct zlentry {
// prevrawlen是前一个节点的长度,prevrawlensize是指prevrawlen的大小,有1字节和5字节两种
unsigned int prevrawlensize; /* Bytes used to encode the previous entry len*/
unsigned int prevrawlen; /* Previous entry len. */
//len为当前节点长度 lensize为编码len所需的字节大小
unsigned int lensize; /* Bytes used to encode this entry type/len.
For example strings have a 1, 2 or 5 bytes
header. Integers always use a single byte.*/
unsigned int len; /* Bytes used to represent the actual entry.
For strings this is just the string length
while for integers it is 1, 2, 3, 4, 8 or
0 (for 4 bit immediate) depending on the
number range. */
// 当前节点的header大小prevrawlensize + lensize.
unsigned int headersize; /* prevrawlensize + lensize. */
//编码方式
unsigned char encoding; /* Set to ZIP_STR_* or ZIP_INT_* depending on
the entry encoding. However for 4 bits
immediate integers this can assume a range
of values and must be range-checked. */
//指向节点起始位置
unsigned char *p; /* Pointer to the very start of the entry, that
is, this points to prev-entry-len field. */
} zlentry;
- 运行时的数据结构(主要是这些是通过一系列操作来总结出来的)
- 物理上的数据结构
数据为字符串
数据为整数
ziplist注意点
- 查找的时间复杂度为O(N)
- 列表的长度超过了UINT16_MAX,此时zllen不在表示节点的个数
- 连锁更新