文章目录
redis学习
简单动态字符串
Redis没有直接使用C语言传统的字符串表示(以空字符解物的字符数组),而是自己构建了一种名为简单动态字符串(simple dynamic string,SDS)的抽象类型,并将SDS用作Redis的默认字符串表示。
SDS兼容C语言标准字符串处理函数,且在此基础上保证了二进制安全。
什么是二进制安全?
通俗地讲,C语言中,用“\0”表示字符串的结束,如果字符串中本身就有“\0”字符,字符串就会被截断,即非二进制安全;若通过某种机制,保证读写字符串时不损害其内容,则是二进制安全。
3.2以前的SDS设计
如图所示:
struct sdshdr {
unsigned int len;
unsigned int free;
char buf[];
};
在64位系统下,字段len和字段free各占4个字节,紧接着存放字符串。
这样设计有以下的几个优点:
- 有单独的统计变量len和free(称为头部)。可以很方便地得到字符串长度。(常数数量级获取字符串长度)
- 内容存放在柔性数组buf中,SDS对上层暴露的指针不是指向结构体SDS的指针,而是直接指向柔性数组buf的指针。上层可像读取C字符串一样读取SDS的内容,兼容C语言处理字符串的各种函数。(读取方便)
- 杜绝缓冲区溢出,C字符串在拼接的时候假定用户已经开辟了足够多的空间,如果用户没有开辟足够的空间,可能导致空间之后的不安全内存被使用
- 减少修改字符串时带来的内存重分配次数
- 由于有长度统计变量len的存在,读写字符串时不依赖“\0”终止符,保证了二进制安全。
柔性数组
buf[]是一个柔性数组。柔性数组成员(flexible array member),也叫伸缩性数组成员,只能被放在结构体的末尾。包含柔性数组成员的结构体,通过malloc函数为柔性数组动态分配内存。 之所以用柔性数组存放字符串,是因为柔性数组的地址和结构体是连续的,这样查找内存更快(因为不需要额外通过指针找到字符串的位置);可以很方便地通过柔性数组的首地址偏移得到结构体首地址,进而能很方便地获取其余变量。
减少修改字符串时带来的内存重分配次数
C字符串每次增长或者缩短,程序都要队保存这个C字符串的数组进行一次内存重分配操作:
- 如果拼接操作,程序在执行这个操作之前,程序需要先通过内存重分配来扩展底层数组的空间大小
- 如果是缩短操作,需要手动释放不再使用的内存空间
redis简单字符串通过空间预分配和惰性删除解决这两个问题:
- **空间预分配:**如果最终字符串长度小于1MB,分配len和free相等的未使用空间长度;如果修改以后,长度大于1MB,那么len为实际的长度,free为1MB
- **惰性空间释放:**程序不立即使用内存重分配来回收缩短后多出来的字节,而实用free属性来标记不用的空间。
redis5.0的设计
考虑的问题当保存的字符串太短就显得头部很臃肿,因此设计了多个不同的SDS结构体来保存字符串
1. 长度小于32的短字符串
struct __attribute__ ((__packed__)) sdshdr5 {
unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
char buf[];
};
第一个字节flags作为头部,低三位是类型,高五位是字符串长度,可以用来表示0到31长度的字符串
2. 长度大于32的字符串
根据字符串长度,有以下四种不同的结构体
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[];
};
其中len即已用长度,alloc是分配的总长度,flags中,低三位是类型,高五位未使用,然后buf[]就是柔性数组
基本操作
- 创建字符串:首先计算好不同类型的头部和初始长度,然后动态分配内存。需要注意以下3点。
- 创建空字符串时,SDS_TYPE_5被强制转换为SDS_TYPE_8
- 长度计算时有“+1”操作,是为了算上结束符“\0”
- 返回值是指向sds结构buf字段的指针
- 释放字符串:通过对s的偏移,可定位到SDS结构体的首部,然后调用s_free释放内存
- 拼接字符串:如果不需要扩容,直接拼接,然后返回;如果需要扩容,扩容后进行拼接,然后返回。
链表
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;
感觉链表的实现不是很复杂
跳跃表
嘿嘿嘿这就是个跳跃表,下次再详细讲结构,这边先总结它是怎么实现的
跳跃表具有以下几个概念术语:
- 层,每个节点都有一个层序数组,每层带有两个属性,前进指针和跨度。前进指针用于访问位于表尾方向的其他节点,而跨度则记录了前进指针和当前节点的距离
- 分值,跳变中节点以分值作为关键字来排序
跳跃表
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;
typedef struct zset { //有序集合
dict *dict;
zskiplist *zsl;
} zset;
跳跃表操作
创建跳跃表
- 创建跳跃表结构体对象zsl。
- 将zsl的头节点指针指向新创建的头节点。
- 跳跃表层高初始化为1,长度初始化为0,尾节点指向NULL。
头节点是一个特殊的节点,不存储有序集合的member信息。头节点是跳跃表中第一个插入的节点,其level数组的每项forward都为NULL,span值都为0。
创建节点
- 生成层高:节点层高的最小值为1,最大值是ZSKIPLIST_MAXLEVEL,Redis5中节点层高的值为64。Redis通过zslRandomLevel函数随机生成一个1~64的值,作为新建节点的高度,值越大出现的概率越低。节点层高确定之后便不会再修改。
- 创建节点:跳跃表的每个节点都是有序集合的一个元素,在创建跳跃表节点时,待创建节点的层高()、分值、member等都已确定。对于跳跃表的每个节点,我们需要申请内存来存储。
插入节点
-
查找要插入的位置
x = zsl->header; for (i = zsl->level-1; i >= 0; i--) { //逐层遍历,计算插入节点中,每层的前一个节点、从头节点到达插入节点的跨度 rank[i] = i == (zsl->level-1) ? 0 : rank[i+1]; //初始化rank(跨度) while (x->level[i].forward && (x->level[i].forward->score < score || //当前节点分值比较小 (x->level[i].forward->score == score && //当前节点分值相等,但是字典序在插入节点之前 sdscmp(x->level[i].forward->ele,ele) < 0))) { rank[i] += x->level[i].span; //累加rank x = x->level[i].forward; //指针偏移 } update[i] = x; //记录前一个节点 }
-
调整跳跃表高度
level = zslRandomLevel(); //找个随机高度 for (i = zsl->level; i < level; i++) { //从当前高度迭代到新高度 rank[i] = 0; update[i] = zsl->header; //前一个节点是header update[i]->level[i].span = zsl->length; //前一个节点的跨度就是rank } zsl->level = level; //新的高度
-
插入节点
x = zslCreateNode(level,score,ele); //创建一个新的节点 for (i = 0; i < level; i++) { x->level[i].forward = update[i]->level[i].forward; //新节点更新后继 update[i]->level[i].forward = x; //新节点的前一个节点更新后继 x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]); //新节点更新跨度 update[i]->level[i].span = (rank[0] - rank[i]) + 1; //新节点的前一个节点更新跨度 }
-
调整backward
x->backward = (update[0] == zsl->header) ? NULL : update[0]; //更新新节点的backward if (x->level[0].forward) //更新新节点后继的backward x->level[0].forward->backward = x; else zsl->tail = x; //更新尾节点 zsl->length++; //更新长度 return x;
删除节点
-
查找要删除的节点,和前面相同,记录了各层的前驱和rank
-
更新跨度和后继
void zslDeleteNode(zskiplist *zsl, zskiplistNode *x, zskiplistNode **update) { int i; for (i = 0; i < zsl->level; i++) { if (update[i]->level[i].forward == x) { update[i]->level[i].span += x->level[i].span - 1; //更新待删除节点的i层前一个节点的跨度 update[i]->level[i].forward = x->level[i].forward; //更新待删除节点的i层前一个节点后继 } else { update[i]->level[i].span -= 1; } } }
删除跳跃表
获取到跳跃表对象之后,从头节点的第0层开始,通过forward指针逐步向后遍历,每遇到一个节点便将释放其内存。当所有节点的内存都被释放之后,释放跳跃表对象,即完成了跳跃表的删除操作。代码如下。
void zslFree(zskiplist *zsl) {
zskiplistNode *node = zsl->header->level[0].forward, *next;
zfree(zsl->header);
while(node) {
next = node->level[0].forward;
zslFreeNode(node);
node = next;
}
zfree(zsl);
}
压缩列表
压缩列表ziplist本质上就是一个字节数组,是Redis为了节约内存而设计的一种线性数据结构,可以包含多个元素,每个元素可以是一个字节数组或一个整数。
Redis的有序集合、散列和列表都直接或者间接使用了压缩列表。当有序集合或散列表的元素个数比较少,且元素都是短字符串时,Redis便使用压缩列表作为其底层数据存储结构。列表使用快速链表(quicklist)数据结构存储,而快速链表就是双向链表与压缩列表的组合。
整体表结构
压缩列表的结构为:
- zlbytes: 压缩列表的字节长度,占4个字节,因此压缩列表最多有 2 32 − 1 2^{32 -1} 232−1个字节。
- zltail: 压缩列表尾元素相对于压缩列表起始地址的偏移量,占4个字节。
- zllen: 压缩列表的元素个数,占2个字节。zllen无法存储元素个数超过65535( 2 16 − 1 2^{16 -1} 216−1)的压缩列表,必须遍历整个压缩列表才能获取到元素个数。
- entryX: 压缩列表存储的元素,可以是字节数组或者整数,长度不限。entry的编码结构将在后面详细介绍。
- zlend: 压缩列表的结尾,占1个字节,恒为0xFF。
列表元素结构
压缩列表元素的编码结构
previous_entry_length
previous_entry_length字段表示前一个元素的字节长度,占1个或者5个字节
- 当前一个元素的长度小于254字节时,用1个字节表示;
- 当前一个元素的长度大于或等于254字节时,用5个字节来表示。而此时previous_entry_length字段的第1个字节是固定的0xFE,后面4个字节才真正表示前一个元素的长度。
假设已知当前元素的首地址为p,那么p-previous_entry_length就是前一个元素的首地址,从而实现压缩列表从尾到头的遍历。
encoding
encoding字段表示当前元素的编码,即content字段存储的数据类型(整数或者字节数组),数据内容存储在content字段。为了节约内存,encoding字段同样长度可变。
content
如上述所说,每个压缩列表节点可以保存一个字节数组或者一个整数值。其中,字节数组可以是以下三种长度之一:
- 长度小于等于63(2的6次方-1)字节的字节数组
- 长度小于等于16383(2的14次方-1)字节的字节数组
- 长度小于等于2的32次方-1字节的字节数组。
而整数值可以是以下六种长度之一
- 4位长,介于0至12之间的无符号整数
- 1字节长的有符号整数
- 3字节长的有符号整数
- int16_t类型整数
- int32_t类型整数
- int64_t类型整数
**结构体zlenty:**对于压缩列表的任意元素,获取前一个元素的长度、判断存储的数据类型、获取数据内容都需要经过复杂的解码运算。解码后的结果应该被缓存起来,为此定义了结构体zlentry,用于表示解码后的压缩列表元素。
typedef struct zlentry {
unsigned int prevrawlensize;
unsigned int prevrawlen;
unsigned int lensize;
unsigned int len;
unsigned char encoding;
unsigned int headersize;
unsigned char *p;
} zlentry;
结构体zlentry定义了7个字段
回顾压缩列表元素的编码结构,可变因素实际上不止3个:previous_entry_length字段的长度(prevrawlensize)、previous_entry_length字段存储的内容(prevrawlen)、encoding字段的长度(lensize)、encoding字段的内容(len表示元素数据内容的长度,encoding表示数据类型)和当前元素首地址(p);而headersize则表示当前元素的首部长度,即previous_entry_length字段长度与encoding字段长度之和。
函数zipEntry用来解码压缩列表的元素,存储于zlentry结构体。
void zipEntry(unsigned char *p, zlentry *e) {
ZIP_DECODE_PREVLEN(p, e->prevrawlensize, e->prevrawlen); //解码previous_entry_length字段,此时入参ptr指向元素首地址。
ZIP_DECODE_LENGTH(p + e->prevrawlensize, e->encoding, e->lensize, e->len); //解码encoding字段逻辑,此时入参ptr指向元素首地址偏移previous_entry_length字段长度的位置。
e->headersize = e->prevrawlensize + e->lensize;
e->p = p;
}
散列表、字典
字典又称散列表,是用来存储键值(key-value)对的一种数据结构,在很多高级语言中都有实现,如PHP的数组。但是C语言没有这种数据结构,Redis是K-V型数据库,整个数据库是用字典来存储的,对Redis数据库进行任何增、删、改、查操作,实际就是对字典中的数据进行增、删、改、查操作。
根据Redis数据库的特点,便可知字典有如下特征。
- 可以存储海量数据,键值对是映射关系,可以根据键以O(1)的时间复杂度取出或插入关联值。
- 键值对中键的类型可以是字符串、整型、浮点型等,且键是唯一的。例如:执行set test"hello world"命令,此时的键test类型为字符串,如test这个键存在数据库中,则为修改操作,否则为插入操作。
- 键值对中值的类型可为String、Hash、List、Set、SortedSet。
redis的散列表实现和java相比真的是太简单了
typedef struct dictht {
dictEntry **table; /*指针数组,用于存储键值对*/
unsigned long size; /*table数组的大小*/
unsigned long sizemask; /*掩码 = size - 1 */
unsigned long used; /*table数组已存元素个数,包含next单链表的数据*/
} dictht;
typedef struct dictEntry {
void *key; /*存储键*/
union {
void *val; /*db.dict中的val*/
uint64_t u64;
int64_t s64; /*db.expires中存储过期时间*/
double d;
} v; /*值,是个联合体*/
struct dictEntry *next; /*当Hash冲突时,指向冲突的元素,形成单链表*/
} dictEntry;
typedef struct dict {
dictType *type; /*该字典对应的特定操作函数*/
void *privdata; /*该字典依赖的数据*/
dictht ht[2]; /*Hash表,键值对存储在此*/
long rehashidx; /*rehash标识。默认值为-1,代表没进行rehash操作;不为-1时,代表正进行rehash操作,存储的值表示Hash表ht[0]的rehash操作进行到了哪个索引值*/
unsigned long iterators; /* 当前运行的迭代器数*/
} dict;
typedef struct dictType {
uint64_t (*hashFunction)(const void *key); /*该字典对应的Hash函数*/
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;
操作
初始化
在redis-server启动中,整个数据库会先初始化一个空的字典用于存储整个数据库的键值对。初始化一个空字典,调用的是dict.h文件中的dictCreate函数
dictCreate函数初始化一个空字典的主要步骤为:1. 申请空间、2. 调用_dictInit函数,3. 给字典的各个字段赋予初始值。初始化后,一个字典内存占用情况如图5-9所示。
添加元素
上述命令是给Server的空数据库添加第一对键值对,Server端收到命令后,最终会执行到setKey(redisDbdb,robjkey,robj*val)函数,前文介绍字典的特性时提到过,每个键必须是唯一的,所以元素添加需要经过这么几步来完成:先查找该键是否存在,存在则执行修改,否则添加键值对。而setKey函数的主要逻辑也是如此,其主要流程如下
- 调用dictFind函数,查询键是否存在,是则调用dbOverwrite函数修改键值对,否则调用dbAdd函数添加元素。
- dbAdd最终调用dict.h文件中的dictAdd函数插入键值对。
扩容
随着Redis数据库添加操作逐步进行,存储键值对的字典会出现容量不足,达到上限,此时就需要对字典的Hash表进行扩容,扩容对应的源码是dict.h文件中的dictExpand函数。
扩容主要流程为:①申请一块新内存,初次申请时默认容量大小为4个dictEntry;非初次申请时,申请内存的大小则为当前Hash表容量的一倍。②把新申请的内存地址赋值给ht[1],并把字典的rehashidx标识由-1改为0,表示之后需要进行rehash操作。此时字典的内存结构示意图为图5-11所示。
扩容后,字典容量及掩码值会发生改变,同一个键与掩码经位运算后得到的索引值就会发生改变,从而导致根据键查找不到值的情况。解决这个问题的方法是,新扩容的内存放到一个全新的Hash表中(ht[1]),并给字典打上在进行rehash操作中的标识(即rehashidx!=-1)。此后,新添加的键值对都往新的Hash表中存储;而修改、删除、查找操作需要在ht[0]、ht[1]中进行检查,然后再决定去对哪个Hash表操作。除此之外,还需要把老Hash表(ht[0])中的数据重新计算索引值后全部迁移插入到新的Hash表(ht[1])中,此迁移过程称作rehash
rehash
rehash除了扩容时会触发,缩容时也会触发。Redis整个rehash的实现,主要分为如下几步完成。
1)给Hash表ht[1]申请足够的空间;扩容时空间大小为当前容量2,即d->ht[0].used2;当使用量不到总空间10%时,则进行缩容。缩容时空间大小则为能恰好包含d->ht[0].used个节点的2^N次方幂整数,并把字典中字段rehashidx标识为0。
2)进行rehash操作调用的是dictRehash函数,重新计算ht[0]中每个键的Hash值与索引值(重新计算就叫rehash),依次添加到新的Hash表ht[1],并把老Hash表中该键值对删除。把字典中字段rehashidx字段修改为Hash表ht[0]中正在进行rehash操作节点的索引值。
3)rehash操作后,清空ht[0],然后对调一下ht[1]与ht[0]的值,并把字典中rehashidx字段标识为-1。
执行插入、删除、查找、修改等操作前,都先判断当前字典rehash操作是否在进行中,进行中则调用dictRehashStep函数进行rehash操作(每次只对1个节点进行rehash操作,共执行1次)。除这些操作之外,当服务空闲时,如果当前字典也需要进行rehsh操作,则会调用incrementallyRehash函数进行批量rehash操作(每次对100个节点进行rehash操作,共执行1毫秒)。在经历N次rehash操作后,整个ht[0]的数据都会迁移到ht[1]中,这样做的好处就把是本应集中处理的时间分散到了上百万、千万、亿次操作中,所以其耗时可忽略不计。
迭代器遍历
遍历数据库的原则为:①不重复出现数据;②不遗漏任何数据。熟悉Redis命令的读者应该知道,遍历Redis整个数据库主要有两种方式:全遍历 (例如keys命令)、间断遍历 (hscan命令)
全遍历
typedef struct dictIterator {
dict *d; //迭代的字典
int index; //当前迭代到Hash表中哪个索引值
int table, safe; //table用于表示当前正在迭代的Hash表,即ht[0]与ht[1],safe用于表示当前创建的是否为安全迭代器
dictEntry *entry, *nextEntry;//当前节点,下一个节点
long long fingerprint;//字典的指纹,当字典未发生改变时,该值不变,发生改变时则值也随着改变
} dictIterator;
整个数据结构占用了48字节,其中d字段指向需要迭代的字典;index字段代表当前读取到Hash表中哪个索引值;table字段表示当前正在迭代的Hash表(即ht[0]与ht[1]中的0和1);safe字段表示当前创建的迭代器是否为安全模式;entry字段表示正在读取的节点数据;nextEntry字段表示entry节点中的next字段所指向的数据。
fingerprint字段是一个64位的整数,表示在给定时间内字典的状态。在这里称其为字典的指纹,因为该字段的值为字典(dict结构体)中所有字段值组合在一起生成的Hash值,所以当字典中数据发生任何变化时,其值都会不同。
迭代器遍历数据分为两类:
- 普通迭代器,只遍历数据;
- 安全迭代器,遍历的同时删除数据。
普通迭代器
- 调用dictGetIterator函数初始化一个普通迭代器,此时会把iter->safe值置为0,表示初始化的迭代器为普通迭代器
- 循环调用dictNext函数依次遍历字典中Hash表的节点,首次遍历时会通过dictFingerprint函数拿到当前字典的指纹值
- 当调用dictNext函数遍历完字典Hash表中节点数据后,释放迭代器时会继续调用dictFingerprint函数计算字典的指纹值,并与首次拿到的指纹值比较,不相等则输出异常"===ASSERTION FAILED===",且退出程序执行
普通迭代器通过步骤1、步骤3的指纹值对比,来限制整个迭代过程中只能进行迭代操作,即迭代过程中字典数据的修改、添加、删除、查找等操作都不能进行,只能调用dictNext函数迭代整个字典,否则就报异常,由此来保证迭代器取出数据的准确性。
安全迭代器
安全迭代器和普通迭代器迭代数据原理类似,也是通过循环调用dictNext函数依次遍历字典中Hash表的节点。安全迭代器确保读取数据的准确性,不是通过限制字典的部分操作来实现的,而是通过限制rehash的进行来确保数据的准确性,因此迭代过程中可以对字典进行增删改查等操作。
原理上很简单,如果当前字典有安全迭代器运行,则不进行渐进式rehash操作,rehash操作暂停,字典中数据就不会被重复遍历,由此确保了读取数据的准确性。
当Redis执行部分命令时会使用安全迭代器迭代字典数据,例如keys命令。keys命令主要作用是通过模式匹配,返回给定模式的所有key列表,遇到过期的键则会进行删除操作。Redis数据键值对都存储在字典中,因此keys命令会通过安全迭代器来遍历整个字典。安全迭代器整个迭代过程也较为简单,主要分如下几个步骤。
- 调用dictGetSafeIterator函数初始化一个安全迭代器,此时会把iter->safe值置为1,表示初始化的迭代器为安全迭代器,safe字段置为1。
- 循环调用dictNext函数依次遍历字典中Hash表的节点,首次遍历时会把字典中iterators字段进行加1操作,确保迭代过程中渐进式rehash操作会被中断执行。
- 当调用dictNext函数遍历完字典Hash表中节点数据后,释放迭代器时会把字典中iterators字段进行减1操作,确保迭代后渐进式rehash操作能正常进行。
间断遍历
当数据库中有海量数据时,执行keys命令进行一次数据库全遍历,耗时肯定不短,会造成短暂的Redis不可用,所以在Redis在2.8.0版本后新增了scan操作,也就是“间断遍历”。而dictScan是“间断遍历”中的一种实现,主要在迭代字典中数据时使用,例如hscan命令迭代整个数据库中的key,以及zscan命令迭代有序集合所有成员与值时,都是通过dictScan函数来实现的字典遍历。dictScan遍历字典过程中是可以进行rehash操作的,通过算法来保证所有的数据能被遍历到。
整数集合
整数集合(intset)是一个有序的、存储整型数据的结构。我们知道Redis是一个内存数据库,所以必须考虑如何能够高效地利用内存。当Redis集合类型的元素都是整数并且都处在64位有符号整数范围之内时,使用该结构体存储。
在两种情况下,底层编码会发生转换。一种情况为当元素个数超过一定数量之后(默认值为512),即使元素类型仍然是整型,也会将编码转换为hashtable,该值由如下配置项决定:
set-max-intset-entries 512
另一种情况为当增加非整型变量时,例如在集合中增加元素’a’后,testSet的底层编码从intset转换为hashtable
结构体
typedef struct intset {
uint32_t encoding;//编码类型
uint32_t length;//元素个数
int8_t contents[];//柔性数组,根据encoding字段决定几个字节表示一个元素
} intset
encoding:编码类型,决定每个元素占用几个字节。有如下3种类型。
- INTSET_ENC_INT16:当元素值都位于INT16_MIN和INT16_MAX之间时使用。该编码方式为每个元素占用2个字节。
- INTSET_ENC_INT32:当元素值位于INT16_MAX到INT32_MAX或者INT32_MIN到INT16_MIN之间时使用。该编码方式为每个元素占用4个字节。
- INTSET_ENC_INT64:当元素值位于INT32_MAX到INT64_MAX或者INT64_MIN到INT32_MIN之间时使用。该编码方式为每个元素占用8个字节。
intset结构体会根据待插入的值决定是否需要进行扩容操作。扩容会修改encoding字段,而encoding字段决定了一个元素在contents柔性数组中占用几个字节。所以当修改encoding字段之后,intset中原来的元素也需要在contents中进行相应的扩展。只要待插入的值导致了扩容,则该值在待插入的intset中不是最大值就是最小值
升级
当插入的时候,如果新元素的类型比所有元素类型都要长时,要先进行升级
- 根据新元素的类型,扩展整数集合底层数组的空间大小
- 所有元素转换类型
- 添加新元素
引发升级的新元素要不比所有的元素都小(一个巨小的负数),要不比所有元素都大(一个巨大的正数),所以它要不在头,要不在尾。
操作
查询
基于二分查找
插入
- 编码是否满足,不满足升级
- 检索,找到就不添加,没找到就添加
- 扩展
- 挪动位置,进行插入
- 长度加1
删除
- 找到元素
- 通过移动内存把元素覆盖掉
对象
redis提供了五大基本数据对象(现在好像不止了)供客户端使用
- 字符串对象
- 列表对象
- 哈希对象
- 集合对象
- 有序集合对象
redis通过底层的数据结果组合实现了这几种对象。
每个对象都由一个redisObject结构表示,该结构中和保存数据有关的三个属性分别是type属性、encoding属性和ptr属性
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;
对象的ptr指针指向对象的底层实现数据结构,而这些数据结构由对象的encoding属性决定
Redis使用对象来表示数据库中的键和值,每当我们在redis的数据库中新创建一个键值对的时候,我们至少会创建两个对象,一个用作键值对的键(键对象),另一个对象作键值对的值(值对象)。其中,键总是一个字符串对象,而值可以是其他任意对象,因此
- 当我们称呼一个数据库键为"字符串键"的时候,指的是,它的值是字符串
- 当我们称呼一个数据库键为“列表键”的时候,指的是,它的值是列表
诸如此类
TYPE命令也类似,对一个数据库键执行TYPE命令时,命令返回键对应的值对象的类型
不同类型值对象的TYPE命令输出
对象 | 对象type属性的值 | Type命令的输出 |
---|---|---|
字符串对象 | REDIS_STRING | string |
列表对象 | REDIS_LIST | list |
哈希对象 | REDIS_HASH | hash |
集合对象 | REDIS_SET | set |
有序集合对象 | REDIS_ZSET | zset |
内存回收
因为C语言不具备自动内存回收功能,所以redis在自己的对象系统中构建了一个引用技术(reference counting)技术实现的内存回收机制,通过这一机制,程序可以通过跟踪对象的引用技术信息,在适当的时候自动释放对象,并进行内存回收
- 当创建一个新对象时,引用技术的值会被初始化为1
- 当对象被一个新的程序使用时,它的引用计数会被增1
- 当对象不再被一个程序使用时,它的引用计数会被减1
- 当对象的引用计数值变为0时,对象所占用的内存会被释放
对象共享
只对int编码的字符串对象进行共享
对象的空转时长
redisObject结构包含一个lru属性,该属性记录对象最后一次被程序访问的时间。如果内存回收算法是volatile-lru或者allkeys-lru,那么服务器占用的内存数高于限制时,空转时间长的会优先被服务器释放,从而回收内存。