要问redis为什么这么快,那和它底层高效率的数据结构脱不了关系。
1、SDS简单动态字符串
源码如下所示
c
struct sdshdr{
//记录buf数组中已使用的字节的数量,也是SDS所保存字符串的长度
int len;
//记录buf数组中未使用字节的数量
int free;
//字节数组,用于保存字符串
char buf[];
}
为什么redis不直接使用C语言的字符串?因为C语言自带的字符串以'\0'作为结束符,只能保存文本数据,而SDS根据len这个字段来判断字符串是否结束的,可以保存图片、视频这样的二进制数据。
SDS可以直接记录了当前字符串的长度,获取字符串长度为时间复杂度O(1),而C语言的strlen()函数为O(n)
空间预分配:在扩展时会额外扩展一部分空间,而不仅仅是扩展至所需大小,防止频繁扩展
- 若SDS的长度小于1M,那么分配和len属性同样大小的未使用空间
- 若大于1M,那么分配1M大小的未使用空间
惰性空间释放:
在缩短SDS保存到字符数组时,不用立即回收所需内存,而是记录在free属性里,用来以后使用
2、链表
链表节点listNode
c
typedef struct listNode{
//前置节点
struct listNode *prev;
//后置节点
struct listNode *next;
//节点值
void *value;
}listNode;
链表头节点
c
typedef struct list{
//表头结点
listNode *head;
//表尾节点
listNode *tail;
//链表所包含的节点数量
unsigned long len;
//节点值赋值函数,用于复制节点所保存的值
void *(*dup)(void *ptr);
//节点值释放函数,用于释放链表节点所保存的值
void *(*free)(void *ptr);
//节点值对比函数,用于比较链表节点所保存的值和另一个输入值是否相等
void *(*match)(void *ptr,void *key);
}list;
特性
双端:链表节点带有prev和next指针
无环:表头结点的prev和表尾节点的next都指向NULL,对链表的访问以NULL为终点。
带表头和表尾节点:获取表头和表尾结点的时间复杂度为O(1);
多态:用void* 保存节点值,可以保存各种不同的值。
3、字典
字典dict
c
typedef struct dict{
//类型特定函数
dictType *type;
//私有数据
void *privdata;
//哈希表,ht[0]用作哈希表,ht[1]只在rehash期间使用,在rehash开始时分配内存
dictht ht[2];
//rehash索引
//当rehash不在进行时,值为-1;
int trehashidx;
}dict;
哈希表dictht
c
typedef struct dictht{
//哈希表数组,指向一个dictEntry结构数组
dictEntry **table;
//哈希表大小
unsigned long size;
//哈希表大小掩码,用于计算索引值,总是等于size-1
unsigned long sizemask;
//该hash表已有节点数量
unsigned long used;
}dictht;
哈希表节点dictEntry
c
typedef struct dictEntry{
//键
void *key;
//值
union{
void *val;
uint64_t u64;
int64_t s64;
}v;
//指向下个哈希表节点,形成链表,用来解决hash冲突
struct dectEntry *next;
}dictEntry;
rehash
扩展与收缩条件
-
扩展条件
- 服务器目前没有在执行BGSAVE命令或BGREWRITEAOF命令,并且哈希表的负载因子大于等于1;
- 服务器目前正在执行BGSAVE命令或BGREWRITEAOF命令,并且哈希表的负载因子大于等于5
- 扩容大小为第一个大于等于已使用节点*2的2的n次幂大小
-
收缩条件
- 当哈希表负载因子小于0.1时开始执行收缩操作
- 收缩大小为第一个大于等于已使用节点的2的n次幂大小
渐进式rehash
- 为ht[1]分配空间,让字典同时持有ht[0]和ht[1]两个哈希表
- 在字典中维持一个索引计数器变量rehashidx,并将它的值设为0,标识rehash工作正式开始
- 在rehash期间,每次对字典执行增删改查时,都会顺带将ht[0]哈希表在rehashidx索引上的所有键值对rehash到ht[1],当rehash完成后,rehashidx的值增一
- 随着操作的不断执行,ht[0]的所有键值对操作全部迁移完成,将rehashidx变为-1,并将ht[1]设为ht[0];
判断迁移完成
- Redis 使用一个叫
trehashidx
的索引来记录当前迁移的桶 - 当
trehashidx
小于ht[0].size
时,说明迁移还在进行中 - 当
trehashidx >= ht[0].size
时,意味着迁移已完成
注: 在rehash期间,删除、更新、查找会在两个哈希表上进行,现在ht[0]查找,ht[0]没有再去ht[1]查找。添加操作只会在ht[1]时进行。
4、 跳跃表
跳跃表节点zskiplistNode
c
typedef struct zskiplistNode{
//后退指针
struct zskiplistNode *backward;
//分值,列表中的所有节点都按分值大小从小到大排序
double score;
//成员对象,指向一个字符串对象
robj *obj;
//层
struct zskiplistLevel{
//前进指针
struct zskiplistNode *forward;
//跨度
unsigned int span;
}level[];
}zskiplistNode;
zskiplist
c
typedef struct zskiplist {
// 跳表头节点(虚拟节点,无实际数据)
struct zskiplistNode *header;
// 跳表中元素的数量
unsigned long length;
// 最后一个节点
struct zskiplistNode *tail;
// 当前最高层数
int level;
} zskiplist;
层数生成
生成一个节点时,该节点的层数是随机生成的,最大为32,层数越高概率越小。
5、 压缩列表
c
typedef struct {
unsigned char *zl; // 指向列表字节数组
unsigned long long zlbytes; // 结构体总字节数
unsigned int zltail; // 结构体中尾部元素偏移
unsigned int zllen; // 元素数量(可能会用特殊值表示)
unsigned int zlend; // 结构体末尾偏移(通常为zlbytes-1)
} dictZiplist;
压缩列表节点构成
previous_entry_length | encoding | content |
---|
previous_entry_length:以字节为单位,记录前一个节点的长度
- 若前一个节点长度小于254字节,那么previous_entry_length用1字节空间记录
- 若前一个节点长度大于等于254字节,那么previous_entry_length用5字节空间记录
encoding:记录了节点的content属性所保存数据的类型和长度,最高两位作为类型标志位,其他位表示长度
content:记录节点所保存的值
连锁更新
指的是因为前一个节点的更新引起长度变化,而导致的后续节点的连续更新。
例:在一个压缩列表中保存着多个连续的、长度介于250字节到253字节之间的节点e1至eN,此时每个节点的previous_entry_length都占用1字节,若这时某处因为变更(更新、插入或删除)出现在一个长度大于等于254的节点,那么该处的后一个节点为了记录这个长节点,自身的previous_entry_length就要进行扩容,扩容之后自身的长度就要大于等于254字节,后续的节点也需要像这个节点一样进行扩容,导致连锁更新。
6、quicklist
quicklist
c
typedef struct quicklist{
//链表头
quicklistNode *head;
//链表尾
quicklistNode *tail;
//所有压缩列表中的元素个数
unsigned long count;
//quicklistNoded的个数
unsigned long len;
}quicklist;
节点quicklistNode
c
typedef struct quicklistNode{
//前一个结点指针
struct quicklistNode *prev;
//后一个结点指针
struct quicklistNode *next;
//指向的压缩列表
unsigned char *zl;
//压缩列表的字节大小
unsigned int sz;
//压缩列表的元素个数
unsigned int count;
}
每个quicklist节点都指向了一个压缩列表,这有效防止连锁更新,即使发生也只是发生在一个quicklistNode 里面。
每个节点的ziplist最大长度:由ZIPLIST_MAX_SIZE
参数控制,默认为 1024
字节(即1KB)。可以通过ziplist-max-ziplist-value进行修改配置
当前ziplist
空间达到最大限制(如1024字节)后,Redis会创建一个新的节点 ,并将新元素写入到新节点的ziplist
中。
7、listpack
数据结构和压缩列表差不多,但每个entry里面存放的len用来记录自身的长度,不再记录前一个节点的长度。
压缩列表节点构成
encoding | data | len |
---|
因为里面只存储自身的长度,所以自身的长度改变也只会影响自身,不会扩散至其他节点。解决了连锁更新问题。
8、整数集合
c
typedef struct intset{
//编码方式
uint32_t encoding;
//集合包含的元素数量
uint32_t length;
//保存元素的数组
int8_t contenes[];
}intset;
当一个集合只包含整数值元素,并且这个集合的元素数量不多时,Redis会使用整数集合作为底层实现。元素在数组中有序存放
例如 SADD number 1 3 5 7 9
升级
当添加一个新元素时,新元素的类型比整数集合现有所有元素都要长时,整数集合需要先进行升级,然后才将新元素添加到整数集合中。
- 根据新元素的类型,扩展整数集合底层数组的空间大小,并为新元素分配空间
- 将底层数组现有的所有元素都转换成与新元素相同的类型,并将转换后的元素放到正确的位置上,且需要保证底层有序性不变。
- 将新元素添加到底层数组里面。
redis中各个对象的长度不同时所使用的数据结构也是不同的。
对象类型 | 小元素底层结构 | 阈值 | 大元素底层结构 |
---|---|---|---|
String | 整形集合 | 保存的是整数值 | SDS |
Hash | ziplist | 元素个数小于512,且每个元素均大小均小于64字节 | 字典 |
List | ziplist | 元素个数小于512,且每个元素均大小均小于64字节 | list(在3.2之后任何情况那个都是quicklist) |
Set | 整数集合 | 集合中的元素都是整数且数量小于512个 | 字典 |
ZSet | 压缩列表 | 元素个数小于128,且每个元素均大小均小于64字节 | 跳表+字典 |
注:Redis 7.0之后ziplist全部被quick list进行取代。