类型 | 编码 | 对象 |
---|---|---|
REDIS_STRING | REDIS_ENCODING_INT | 使用整数值实现的字符串对象 |
REDIS_STRING | REDIS_ENCODING_EMBSTR | 使用embstr编码的简单动态字符串实现的字符串对象 |
REDIS_STRING | REDIS_ENCODING_RAW | 使用简单动态字符串实现的字符串对象 |
REDIS_LIST | REDIS_ENCODING_ZIPLIST | 使用压缩列表实现的 列表对象 |
REDIS_LIST | REDIS_ENCODING_LINKEDLIST | 使用双端链表实现的 列表对象 |
REDIS_HASH | REDIS_ENCODING_ZIPLIST | 使用压缩列表实现的哈希对象 |
REDIS_HASH | REDIS_ENCODING_HT | 使用字典实现的哈希对象 |
REDIS_SET | REDIS_ENCODING_INTSET | 使用整数集合实现的集合对象 |
REDIS_SET | REDIS_ENCODING_HT | 使用字典实现的集合对象 |
REDIS_ZSET | REDIS_ENCODING_ZIPLIST | 使用压缩列表实现的有序集合对象 |
REDIS_ZSET | REDIS_ENCODING_SKIPLIST | 使用跳跃表和字典实现的有序集合对象 |
1.1 数据结构
struct sdshdr {
//记录buf数组中已使用字节数量
//也就是sds中保存的字符串长度
int len;
//buf数组中未使用字节的数量
int free;
//字节数组,保存字符串
char buf[];
};
如上图:SDS中保存的字符串长度为5,未使用空间(可用空间)为0。且SDS遵循C字符串以’\0’结尾的惯例,保存‘\0’的1字节空间不计算在SDS的len属性里面,并为其额外分配1字节空间,以及添加其到字符串末尾等操作都是SDS函数自动完成,这样做可以直接重用C中的字符串库函数。
1.2 优化策划
由于SDS中的free属性,SDS中的数组长度不一定就是字符数量加一(’\0’),数组里面可以包含未使用的空间,由free记录,通过未使用空间SDS实现了空间预分配与惰性空间释放两种优化策略。
空间预分配:如果需要对SDS进行空间扩展,程序不仅会为其分配修改所必须要的空间还会为其分配额外的未使用空间,额外分配的未使用空间大小由以下公式决定。
(1) 如果对SDS修改后,SDS长度(即len大小)将小于1MB,那么程序会分配与len属性同样大小的未使用空间,即free与len大小相同。例,修改后的SDS长度(len)为13,那么程序也会多分配13字节的未使用空间(free),SDS的buf实际长度为13+13+1=27字节(额外的1字节用来存储‘\0’)
(2)如果对SDS修改后,SDS长度将大于等于1MB,那么程序会分配1MB的未使用空间。例如,修改后SDS长度(len)变为30MB,那么程序会多分配1MB的未使用空间(free),SDS的buf实际长度为30MB+1MB+1byte。
惰性空间释放:SDS的API需要缩短保存的字符串时,程序不会立即使用内存重分配来回收缩短后多出来的空间,而是使用free记录这些未使用的空间,这样避免了缩短字符串时所需的内存重分配操作,并未将来可能有的增长操作提供了优化,同时,SDS也提供了相应的API让我们再有需要时真正地释放未使用的空间。
1.3 二进制安全
由于C字符串是以空字符来表示结尾,所以C字符串中不能包含该字符,这个限制使C字符串只能保存文本数据,而不能保存图片、音频、视频、压缩文件这样的二进制数据,而由于SDS中的len属性,从而没有这些限制,Redis可以用其保存一系列的二进制数据。
1.4 兼容部分C字符串函数
虽然SDS的API都是二进制安全的,但他们一样遵循C字符串以空字符结尾的惯性,这样就可以让那些保存文本数据(其他类型的数据中可能含有空字符)的SDS可以重用一部分
1.5 对比C字符串SDS的优点
2.1 数据结构
typedef struct listNode{
//前置节点
struct listNode *prev;
//后置节点
struct listNode *next;
//节点的值
void *value;
}listNode;
typedef struct list{
//表头节点
listNode *head;
//表尾节点
listNode *tail;
//链表中包含的节点数量
unsigned long len;
//节点值复制函数
void *(*dup)(void *ptr);
//节点值释放函数
void (*free)(void *ptr);
//节点值对比函数
int (*match) (void *ptr, void *key);
}list;
Redis字典使用哈希表作为底层实现,采用链地址法解决哈希冲突,以下分别为哈希表节点,哈希表以及字典的数据结构。
3.1 数据结构
//哈希表节点
typedef struct dictEntry{
//键
void *key;
//值
union{
void *val;
uint64_t u64;
int64_t s64;
}v;
//指向下个哈希表节点,形成链表
struct dictEntry *next;
}dictEntry;
//哈希表
typedef struct dictht{
//哈希表数组
dictEntry **table;
//哈希表大小
unsigned long size;
//哈希表大小掩码,用于计算索引
//总是等于size-1
unsigned long sizemask;
//该哈希表已有节点的数量
unsigned long used;
}dictht;
//字典
typedef struct dict{
//类型特定函数
dictType *type;
//私有数据
void *privdata;
//哈希表
//ht数组包含两个哈希表,一般情况下,字典只使用ht[0]哈希表,ht[1]只会在对ht[0]进行rehash时使用
dictht ht[2];
//rehash索引,他记录了rehash目前的进度
//当没有在进行rehash时,值为-1
int trehashidx;
}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 *pravdata, void *obj):
};
如上图所示,为一个普通状态下(没有进行rehash)的字典,size表示哈希表的数组大小为4,used表示共有两个元素。
3.2 哈希算法
当需要将一个新的键值对添加到字典里或根据键查找相应的值,就需要根据键计算出哈希值和索引值,然后在进行后续操作。
Redis计算哈希值和索引值的方法如下:
3.3 rehash(重新散列)
随着操作不断进行,哈希表保存的键值对会逐渐的增多或减少,为了使哈希表的负载因子维持在一个合理的范围,当哈希表保存的键值对过多或过少时,就需要对哈希表的大小进行相应的扩展或者收缩。
扩展和收缩通过rehash操作来完成,步骤如下:
为字典的ht[1]哈希表分配空间,空间大小取决于要执行的操作以及ht[0]当前包含的键值对数量(ht[0].used属性)
将保存在ht[0]中的所有键值对rehash到ht[1]上面,即重新计算哈希值与索引值,然后根据索引值将键值对放到指定位置。
当将所有的键值对都迁移到ht[1]之后,释放ht[0],将ht[1]置为ht[0],并在ht[1]新创建一个新的哈希表,为下一次rehash做准备。
注: 负载因子 = 键值对个数/哈希表大小
3.4 渐进式rehash
因为扩展和收缩哈希表需要将ht[0]中保存的所有键值对rehash到ht[1]中,但是,这个rehash动作并不是一次性,集中式的完成,而是分多次、渐进式的完成,因为如果保存的键值对数量过多,如果一次性rehash,庞大的计算量可能会导致服务器在一段时间内停止服务。
步骤如下:
注: 在rehash期间,字典会同时使用两个哈希表ht[0]和ht[1],删除、查找、更新等操作在两个哈希表进行,而新添加到字典的键值对一律保存到ht[1]上。
优点: 采取分而治之的方式,将所需计算工作均摊到对字典的每个添加、删除、查找、更新操作,从而避免集中式rehash而带来的庞大计算量。
4.1 简介: 跳跃表是一种有序数据结构,实质是一种可以进行二分查找的有序链表。它通过在每个节点中维持多个指向其他节点的指针(多级索引),从而达到快速访问节点的目的。其查找的平均时间复杂度为O(logN),最坏O(N)。还可以通过顺序操作来批量处理节点,在大部分情况下,他的效率可以媲美平衡树且实现更简单。
4.2 数据结构
//跳跃表节点
typedef struct zskiplistNode{
//后退指针
struct zskiplistNode *backward;
//分值
double score;
//成员对象
robj *obj;
//层
struct zskiplistLevel {
//前进指针
struct zskiplistNode *forward;
//跨度
unsigned int span;
}level[];
}zskiplistNode;
注: 每个节点都包含一个level数组,数组大小只有当创建新节点的时候才会根据幂次定律随机生成一个介于1和32之间的值作为level数组的大小,加入大小为4,表示这个节点在前四层都出现。C语言数组索引从0开始,所以第一层为level[0]。跨度表示两个节点之间的距离。可以用来计算rank排名。
//跳跃表
typedef struct zskiplist{
//表头节点和表尾节点
struct zskiplistNode *header;
struct zskiplistNode *tail;
//节点数量
unsigned long long length;
//表中层数最大的节点的层数
int level;
}zskiplist;
如上图所示为一个包含三个节点的跳跃表(头节点不算,类似于负无穷),其中第一个节点层数组大小为为4,第二个节点大小为2,第三个大小为5,分别表示第一个节点在前四层都有,第二个节点在前两层,而第三个节点在前5层。可表示为下图:
跳跃表最大的特点就是在链表上可以使用二分查找,假设现在要找分值为2的节点,从最大层开始查找(跳跃表中记录了最大层数(当前为5)),从第五层开始找,先从头节点开始,如果后置节点存在且分数小于等于想要查找的,那么当前节点等于后置节点继续,代码如下:
zskiplistNode *pcur = zsl->header;
for(i = zsl->level - 1; i >= 0; --i)
{
while(pcur->level[i].forward && pcur->level[i].forward->score <= score)
{
if(pcur->level[i].forward->score == score)
//找到了
else
pCur = pCur->level[i].forward;
}
}
查找过程简述如下:
5.1 简介:整数集合就是Redis用来保存整数值的集合抽象的数据结构,底层实现其实就是一个动态有序(从小到大)的字节数组(根据编码将几个字节视为一个元素)。它可以存储int_16、int_32、int_64的整数值,并且不会有重复元素。
5.2 数据结构
typedef struct intset{
//编码方式
uint32_t encoding;
//元素数量
uint32_t length;
//保存元素的数组
int8_t contents[];
};
5.3 升级
升级整数集合并添加新元素分为三步:
注:如果添加新元素导致升级,那么新元素一定比现有所有元素大或者小,也就是说,新元素一定是放在底层数组第一个或最后一个。整数集合并不支持降级。
5.4 整数集合的优点
6.1 简介:
属性 | 长度 | 作用 |
---|---|---|
zlbytes | 4字节 | 记录压缩列表所占字节总数 |
zltail | 4字节 | 记录尾节点距起始地址有多少字节,可以通过该属性不用遍历确定尾节点地址 |
zllen | 2字节 | 当该值小于65535时,表示压缩列表的节点数量,等于时,数量需要遍历压缩列表才能得出 |
entryX | 不定 | 压缩列表的节点,节点长度由保存内容决定 |
zlend | 1字节 | 特殊值0xff(十进制255),标记压缩列表的末端 |
注:一个节点的长度包含这三个部分。
6.3 连锁更新
如上图,big节点大于等于254字节,e1到eN都小于254,且small也小于,当删除small后,也会引发连锁更新。