1. Redis的对象
Redis使用对象来表示数据库中的键和值,每当我们在Redis的数据库中新创建一个键值对时,我们至少会创建两个对象,一个用作键值对的键,一个用作键值对的值。Redis总共有5种对象类型,分别是字符串对象,链表对象,哈希对象,集合对象,有序集合对象。Redis是一个键值对数据库,Redis的键总是一个字符串对象,Redis的值可以为字符串对象、列表对象、哈希对象、集合对象、有序集合对象等。
Redis的每个对象都使用一个redisObject结构来表示,该结构中保存和数据有关的很多属性,包括type属性、encoding属性、lru属性、refcount属性、ptr属性等。
#define REDIS_LRU_BITS 24
typedef struct redisObject {
// 类型
unsigned type:4;
// 编码
unsigned encoding:4;
// 对象最后一次被访问的时间
unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */
// 引用计数
int refcount;
// 指向实际值的指针
void *ptr;
} robj;
1.1 对象类型type
类型常量 | 对象的名称 |
REDIS_STRING | 字符串对象 |
REDIS_LIST | 列表对象 |
REDIS_HASH | 哈希对象 |
REDIS_SET | 集合对象 |
REDIS_ZSET | 有序集合对象 |
1.2 编码和底层实现encoding
编码常量 | 编码对应的底层数据结构 |
REDIS_ENCODING_INT | long类型整数 |
REDIS_ENCODING_EMBSTR | embstr编码的简单动态字符串 |
REDIS_ENCODING_RAW | 简单动态字符串 |
REDIS_ENCODING_HT | 字典 |
REDIS_ENCODING_LINKEDLIST | 双端链表 |
REDIS_ENCODING_ZIPLIST | 压缩列表 |
REDIS_ENCODING_INTSET | 整数集合 |
REDIS_ENCODING_SKEPLIST | 跳跃表和字典 |
每种对象类型都至少使用了两种不同的编码:字符串对象可以使用long类型整数、embstr编码的简单动态字符串和简单动态字符串,列表对象可以使用压缩列表和双端链表,哈希对象可以使用压缩列表和字典,集合对象可以使用整数集合和字典,有序集合对象可以使用压缩列表和跳跃表和字典。
1.3 lru时间
lru属性记录了对象最后一次被命令程序访问的时间,可以使用当前时间减去lru时间就可以得出键的空转时长,键的空转时长在Redis的lru内存回收策略中使用。
1.4 引用计数
- C语言不具备内存回收功能,所以Redis在自己的对象系统中构建了一个引用计数实现内存回收机制,通过这一机制,程序可以通过跟踪对象的引用计数信息,在适当的时候自动释放对象并且回收内存;
- 对象的引用计数除了用于内存回收之外,引用计数还带有对象共享的作用。
1.5 实际值指针
ptr指向实际值的指针,这里指向的一般为一个结构体指针。
2. 简单动态字符串SDS
字符串对象的底层实现可以选择使用SDS,Redis自己构建了一种名为简单动态字符串(SDS)的抽象类型,SDS定义在sds.h中,SDS的各种API在sds.c中实现。
/*
* 类型别名,用于指向 sdshdr 的 buf 属性
*/
typedef char *sds;
/*
* 保存字符串对象的结构
*/
struct sdshdr {
// buf 中已占用空间的长度
int len;
// buf 中剩余可用空间的长度
int free;
// 数据空间
char buf[];
};
2.1 结构体最后为什么定义一个空数组:
- 不需要初始化,数组名直接就是缓冲区数据的起始地址(如果存在数据)。
- 不占任何空间,指针需要占用4 byte长度空间,空数组不占任何空间,节约了空间,在计算结构体的size时,不会计算最后一个元素,例如上面sizeof(struct sdshdr) = 8。
- 适合制作动态buffer,一次性分配结构体和缓冲区。为了防止内存泄漏,如果是分两次分配(结构体和缓冲区),那么要是第二次malloc失败了,必须回滚释放第一个分配的结构体。这样带来了编码麻烦。其次,分配了第二个缓冲区以后,如果结构里面用的是指针,还要为这个指针赋值。同样,在free这个buffer的时候,用指针也要两次free。如果用空数组,所有问题一次解决。
2.2 SDS与C字符串的区别:
- 常数复杂度获取字符串的长度,SDS的字符串长度可以通过len属性获取。
- 杜绝缓冲区溢出,SDS的API需要对SDS进行修改时,会检查SDS的空间是否满足修改所需的要求,如果不满足,API会自动将SDS空间扩展至执行修改所需的大小。
- 减少修改字符串带来的内存重新分配次数:
1)通过空间预分配,可以减少字符串拼接时内存重分配的次数。当SDS的长度小于1MB时,分配两倍和len属性同样大小的未使用空间。如果SDS的长度大于1MB时,将分配1MB的未使用空间。
2)通过惰性空间释放,可以减少字符串缩短时的内存回收的次数。SDS提供了相应的API,让我们在有需要时,真正释放SDS的未使用空间。
- 二进制安全,SDS的API都会以二进制的方式来处理buf数组中的内容,因此SDS不仅可以保存文本,还可以保存二进制。而对于C的字符串来说,遇到空格就终止了。
2.3 SDS的API函数
static inline size_t sdslen(const sds s) // 返回 sds 实际保存的字符串的长度
static inline size_t sdsavail(const sds s) // 返回 sds 可用空间的长度
sds sdsnew(const char *init); // 创建一个包含给定C字符串的SDS
sds sdsempty(void); // 创建一个空SDS
sds sdsdup(const sds s); // 创建一个给定SDS的副本
void sdsfree(sds s); // 释放给定的SDS
size_t sdsavail(const sds s); // 返回SDS的未使用的空间字节数
sds sdsgrowzero(sds s, size_t len); // 用空字符串扩展SDS
sds sdscat(sds s, const char *t); // 给定的C字符串连接到SDS末尾
sds sdscatsds(sds s, const sds t); // 两个SDS进行连接
sds sdscpy(sds s, const char *t); // C字符串复制进SDS,覆盖原有的SDS
void sdsclear(sds s); // 清空SDS
int sdscmp(const sds s1, const sds s2); // 比较两个SDS是否相同
3. 双端链表
列表对象的底层实现可以为双端链表。双端链表内置在许多高级语言中,如C++,但是Redis使用C语言编写,没有内置这种数据结构,所以Redis构建了自己的双端链表实现。Redis的链表和链表节点定义在adlist.h中,链表的API函数在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;
dup、free和match成员是用于实现多态链表所需的类型特定函数。
3.1 Redis链表实现的特性:
- 双端:链表节点带有prev和next指针
- 无环
- 带表头指针和表尾指针
- 带链表长度计数器
- 多态 :链表节点使用void *指针来保存节点值,并且可以通过list结构的dup、free和match三个属性为节点值设置类型特定函数,所以链表可以用于保存各种不同类型的值。
3.2 Redis链表的API函数
#define AL_START_HEAD 0 // 从表头向表尾进行迭代
#define AL_START_TAIL 1 // 从表尾到表头进行迭代
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); // 将迭代器的方向设置为 AL_START_HEAD,并将迭代指针重新指向表头节点。
void listRewindTail(list *list, listIter *li); // 将迭代器的方向设置为 AL_START_TAIL,并将迭代指针重新指向表尾节点。
void listRotate(list *list); // 表尾节点弹出,插入到表头
4. 字典
哈希对象的底层实现可以为字典。字典经常作为一种数据结构内置在高级编程语言中,如C++,但是Redis使用C语言编写,并没有内置这种数据结构,因此Redis构建了自己的字典实现。字典对象的定义在dict.h中,字典对象的API实现在dict.c中。
/*
* 哈希表
*
* 每个字典都使用两个哈希表,从而实现渐进式 rehash 。
*/
typedef struct dictht {
// 哈希表数组
dictEntry **table;
// 哈希表大小
unsigned long size;
// 哈希表大小掩码,用于计算索引值
// 总是等于 size - 1
unsigned long sizemask;
// 该哈希表已有节点的数量
unsigned long used;
} dictht;
/*
* 哈希表节点
*/
typedef struct dictEntry {
// 键
void *key;
// 值
union {
void *val;
uint64_t u64;
int64_t s64;
} v;
// 指向下个哈希表节点,形成链表
struct dictEntry *next;
} dictEntry;
Reids的字典使用哈希表作为底层实现,一个哈希表里面可以有多个哈希表节点,而每个哈希表节点则保存了字典中的一个键值对。
- dictht 中size属性记录了哈希表的大小,也即是table数组的大小,而used属性则记录了哈希表目前已有节点的数量。
- dictEntry中key属性保存键值对的键,v属性保存键值对的值,键值对的值可以是一个指针,或者是一个uint64_t整数,又或者是int64_t整数。
/*
* 字典
*/
typedef struct dict {
dictType *type; // 类型特定函数
void *privdata; // 私有数据
dictht ht[2]; // 哈希表
// rehash 索引
// 当 rehash 不在进行时,值为 -1
int rehashidx; /* rehashing not in progress if rehashidx == -1 */
// 目前正在运行的安全迭代器的数量
int iterators; /* number of iterators currently running */
} dict;
/*
* 字典类型特定函数
*/
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;
/*
* 字典迭代器
*
* 如果 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;
- dict中type属性是一个指向dictType结构的指针,每个dictType结构保存了一簇用于操作特性类型键值对的函数,Redis会为不同的字典设置不同的类型特定函数。
- dict中的privdata属性则保存了需要传给那些类型特定函数的可选参数。
- ht数组中每个项都是一个dictht哈希表,一般情况下,字典只使用ht[0]哈希表,ht[1]哈希表只会在对ht[0]哈希表进行rehash时使用。
- rehashidx记录了rehash目前的进度,如果目前没有进行rehash,那么它的值为-1。
4.1 哈希和rehash
(1) 哈希算法:
第一步,计算哈希值:hash = dict->type->hashFunction(key);
第二步,根据哈希值和sizemask的值计算索引值:index = hash & dict->ht[x].sizemask;
(2)解决冲突:哈希表使用链地址法解决冲突
(3)rehash:为了让哈希表的负载因子(load_factor)维持在一个合理的范围内,当哈希表保存的键值对数量太多或者太少时,程序需要对哈希表的大小进行相应的扩展或者收缩。
load_factor = ht[0].used / ht[0].size
当一下条件中的任意一个被满足时,程序会自动开始对哈希表执行扩展操作:
- 服务器目前没有执行BGSAVE命令或者BGREWRITEAOF命令,并且哈希表的负载因子大于等于1.
- 服务器目前正在执行BGSAVE命令或者BGREWRITEAOF命令,并且哈希表的负载因子大于等于5.
- 当哈希表的负载因子小于0.1时,程序自动开始对哈希表执行收缩操作。
rehash的操作步骤如下:
- 为字典ht[1]哈希表分配空间,这个空间大小取决于要执行的操作,以及ht[0]当前包含的键值对数量。
- 将保存在ht[0]中的所有键值对rehash到ht[1]上面:rehash是指重新计算键的哈希值和索引,然后将键值对放在ht[1]哈希表指定的位置上。
- ht[0]全部迁移到ht[1]之后,释放ht[0],将ht[1]设置为ht[0],并在ht[1]新创建一个空白哈希表。
(4)渐进式rehash :ht[0]的键值对rehash到ht[1]的动作不是一次性、集中式的完成,而是分多次、渐进式的完成的。这是因为如果一个哈希表保存的键值对过多,一次性rehash会导致服务器在一段时间内停止服务。渐进式rehash的步骤如下:
- 为ht[1]分配空间,让字典同时持有ht[0]和ht[1]
- 把字典中的rehashidx设置为0,表示rehash工作正式开始
- 在rehash进行期间,每次对字典执行添加、删除、查找或者更新操作时,程序除了执行的操作外,还会顺带将ht[0]哈希表在rehashidx索引上的所有键值对rehash到ht[1],当rehash工作完成后,程序将rehashidx属性的值增一。即每次只rehash哈希表中的一个节点,分多次完成rehash工作。
- 随着字典操作的不断执行,最终ht[0]的所有键值对都会rehash到ht[1],这时程序将rehashidx的值设置为-1,表示rehash完成。
- 在渐进式rehash的过程中,字典的删除、查找、更新等操作会在两个哈希表上执行。新添加到字典的键值对一律会被保存到ht[1]里面,而ht[0]则不再进行任何添加操作。这一措施保证了ht[0]所包含的键值对数量只减不增,并且最终变成空表。
4.2 哈希表的API函数
dict *dictCreate(dictType *type, void *privDataPtr); // 创建一个新的字典,哈希表此时为空
int dictExpand(dict *d, unsigned long size); // 创建一个新的哈希表,并设置到字典中
int dictAdd(dict *d, void *key, void *val); // 将一个键值对添加到字典中
int dictReplace(dict *d, void *key, void *val); // 将一个键值对添加到字典中,如果已经存在,则更新
int dictDelete(dict *d, const void *key); // 从字典中删除给定的键对应的键值对
void dictRelease(dict *d); // 释放给定字典,以及字典中包含的键值对
void *dictFetchValue(dict *d, const void *key); // 返回给定键的值
dictEntry *dictGetRandomKey(dict *d); // 从哈希表中返回一随机键值对
int dictGetRandomKeys(dict *d, dictEntry **des, int count); // 随机返回count个键值对
5. 整数集合
集合对象的底层实现可以选择整数集合。整数集合intset是Redis用于保存整数值的集合抽象数据类型,它可以保存类型为int16_t、int32_t、int64_t的整数值,并且保证集合中不会出现重复元素,并且集合中的元素是有序的。整数集合定义在intset.h中,整数集合的API实现在intset.c中。
typedef struct intset {
uint32_t encoding; // 编码方式
uint32_t length; // 集合包含的元素数量
int8_t contents[]; // 保存元素的数组
} intset;
- intset中length记录了整数集合包含的元素的数量,也是contents数组的长度。虽然contents属性声明为int8_t类型的数组,但实际上contents数组并不保存任何int8_t类型的值,contents数组的真正类型取决于encoding属性的值。encoding可以取值为:INTSET_ENC_INT16、INTSET_ENC_INT32、INTSET_ENC_INT64。
5.1 集合的升级
当我们将一个新元素添加到整数集合里面,并且新元素类型比整数集合现有的所有元素类型都要长时,整数集合需要先进行升级,然后才能将新元素添加到整数集合里面。升级提升了灵活性并且节约了内存,整数集合不支持降级操作。升级分为三步:
1) 根据新元素类型,扩展整数集合底层数组的空间大小,并为新元素分配空间。
2) 转换原有元素的数据类型,并将转换后的元素放在正确的位置上,放置的过程中需要维持底层数组的有序性质不变。
3) 将新元素添加到底层数组里面。
5.2 集合对象的函数API
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); // 返回整数集合中占用的内存字节数
6. 跳跃表
有序集合对象的底层实现可以选择使用跳跃表和字典来实现,跳跃表用于实现范围查找,字典用于实现单点查找。跳跃表是一种有序数据结构,支持平均O(logN)、最坏O(N)复杂度的节点查找,在大部分情况下,跳跃表的效率可以和平衡树相媲美,并且因为跳跃表的实现比平衡树更简单,所以不少程序使用跳跃表来代替平衡树。跳跃表的实现定义在redis.h中,实现在redis.c中。
/*
* 跳跃表节点
*/
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;
- zskiplist 中 header指向跳跃表头节点,tail指向跳跃表尾节点,level记录了目前跳跃表内,层数最大的那个节点的层数(表头节点的层数不计算在内),length记录了跳跃表的长度,也即是跳跃表目前包含的节点数量。
- zskiplistNode包含了4个属性,分别是层(level)、后退(backward)、分值(score)、成员对象(obj):
1)层(level):跳跃表节点的level数组可以包含多个元素,每个元素都包含一个指向其他节点的指针,程序可以通过这些层来加快访问其他节点的速度,一般来说,层的数量越多,访问其他节点的速度就越快。每次创建一个新跳跃表节点的时候,程序根据幂次定律(power law,越大的数出现的概率越小)随机生成一个介于1和32之间的值作为level数组的大小,这个大小就是层的“高度”。
2)后退(backward):节点的后退指针(backward属性)用于从表尾向表头方向访问节点:跟可以一次跳过多个节点的前进指针不同,因为每个节点只有一个后退指针,所以每次只能后退至前一个节点。
3)分值(score):节点的分值(score属性)是一个double类型的浮点数,跳跃表中的所有节点都按分值从小到大来排序。
4)成员对象(obj):节点的成员对象(obj属性)是一个指针,它指向一个字符串对象,而字符串对象则保存着一个SDS值。
6.1 为什么使用跳表而不使用平衡树:
1)在做范围查找的时候,平衡树比skiplist操作要复杂。在平衡树上,我们找到指定范围的小值之后,还需要以中序遍历的顺序继续寻找其它不超过大值的节点。如果不对平衡树进行一定的改造,这里的中序遍历并不容易实现。而在skiplist上进行范围查找就非常简单,只需要在找到小值之后,对第1层链表进行若干步的遍历就可以实现。
2)平衡树的插入和删除操作可能引发子树的调整,逻辑复杂,而skiplist的插入和删除只需要修改相邻节点的指针,操作简单又快速。
6.2 跳跃表的函数API
/* 表示开区间/闭区间范围的结构 */
typedef struct {
double min, max; // 最小值和最大值
/* 指示最小值和最大值是否*不*包含在范围之内,值为 1 表示不包含,值为 0 表示包含 */
int minex, maxex; /* are min or max exclusive? */
} zrangespec;
zskiplist *zslCreate(void); // 创建一个新的跳跃表
void zslFree(zskiplist *zsl); // 释放给定跳跃表,以及表中包含的所有节点
zskiplistNode *zslInsert(zskiplist *zsl, double score, robj *obj); // 将包含给定成员和分值的节点添加到跳跃表中
unsigned long zslGetRank(zskiplist *zsl, double score, robj *o); // 返回包含给定成员和分值的节点排位,排位表示这个节点是链表中的第几个节点
int zslDelete(zskiplist *zsl, double score, robj *obj); // 删除跳跃表中包含给定成员和分值的节点
zskiplistNode *zslFirstInRange(zskiplist *zsl, zrangespec *range); // 给定一个范围,返回跳跃表中第一个符合这个范围的节点
zskiplistNode *zslLastInRange(zskiplist *zsl, zrangespec *range); // 给定一个范围,返回跳跃表中最后一个符合这个范围的节点
7. 压缩列表
压缩列表的数据结构可以作为列表对象,哈希对象和有序集合对象的底层实现。当一个列表键只包含少量列表项,并且每个列表项要么就是小整数值,要么就是长度比较短的字符串,那么redis就会使用压缩列表来做列表键的底层实现。
7.1 压缩列表的组成
压缩列表是Redis为了节约内存而开发的,准确的说压缩列表其实不算一种数据结构,因为压缩列表是由一系列特殊编码的连续内存块组成的顺序型存储结构,也就是说,一个压缩列表就是内存中一块连续的内存。一个压缩列表可以包含任意多个节点,每个节点可以保存一个字节数组或者一个整数值。下表展示了压缩列表的组成部分:
zlbytes | zltail | zllen | entry1 | entry2 | ... ... | entryN | zlend |
- zlbytes:uint32_t类型,记录了整个压缩列表占用的内存字节数
- zltail:uint32_t类型,记录了压缩列表表尾节点距离压缩列表的起始地址有多少字节
- zllen:uint16_t类型,记录了压缩列表包含的节点数量
- entryX:列表节点,节点的长度由节点保存的内容决定
- zlend:uint8_t,特殊值0xFF,用于标记压缩列表的末端
7.2 压缩列表节点的构成
每个压缩列表节点都由三个部分组成,分别是:previous_entry_length、encoding和content,下面分别对这三部分进行介绍。
previous_entry_length | encoding | content |
(1)previous_entry_length
这个属性以字节为单位,记录了压缩列表中前一个节点的长度,这个属性的长度可以是1字节或者5字节。如果前一个节点的长度小于254字节,那么这个属性的长度为1字节,如果前一个节点的长度大于254字节,那么这个属性的长度为5字节,其中第一个字节会被设置为0xFE,后面四个字节用于保存前一个节点的长度。
因为节点的这个属性保存了前一个节点的长度,所以程序可以通过指针运算,根据当前节点的起始地址计算出前一个节点的起始地址,压缩列表的从尾向表头遍历的操作就是使用这一原理实现的。
(2)encoding
这个属性记录了content属性所保存的数据类型以及长度。encoding字段的大小可以为1字节,2字节或者5字节,值的最高位为00,01,10是字节数组编码,这种编码表示content保存着字节数组,数组长度由编码除去最高两位的其它位记录。值的最高位为11开头的是整数编码,这种编码表示节点的content属性保存着整数值,整数类型和长度由编码除去最高两位之后的其它位记录。
(3)content
content负责保存节点的值,节点值可以是一个字节数组或者是整数,值的类型和长度由encoding属性决定。
7.3 压缩列表的API函数
unsigned char *ziplistNew(void); // 创建一个新的压缩列表
// 创建一个包含给定值的新节点,并将这个新节点添加到压缩列表表头或者表尾
unsigned char *ziplistPush(unsigned char *zl, unsigned char *s, unsigned int slen, int where);
unsigned char *ziplistIndex(unsigned 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);
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); // 返回压缩列表目前占用的内存字节数