SDS(Simple Dynamic String,动态字符串)
-
数据结构
len:字符串长度alloc:分配的空间长度。字符串在变更的的时候有可能会变大,所以可以根据alloc - len去计算空时是否够用,是否需要扩容。flags:sds类型(sdshdr5,sdshdr8,sdshdr16,sdshdr32,sdshdr64)buf[]:字节数组
-
扩容情况
- 如果当前sds长度小于1MB,就会进行
翻倍扩容,原长度的2倍 - 如果当前sds长度耽于1MB,扩容至
原长度 + 1MB
- 如果当前sds长度小于1MB,就会进行
-
flsgs 类型
- 一共有五种,区别在于他们的数据结构中的len、alloc成员变量的数据类型不同
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len;
uint8_t alloc;
unsigned char flags;
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
uint16_t len;
uint16_t alloc;
unsigned char flags;
char buf[];
};
- sdshdr8和sdshdr16,设计他们的时候长度和空间分配的时候不能超过2^8和2^16,以此类推。
- 这样设计可以灵活的保存不同大小的字符串,节省空间。
链表
- 数据结构
typeof struct list {
listNode *head // 指向链表的头节点
listNode *tail // 指向链表的尾节点
unsigned long len // 链表的元素个数
listTypeMethods *methods; // 操作链表的函数(dup、free、match)
........
} list;
typeof struct listNode {
void *value; // 节点存储的实际数据
struct listNode *prev; // 指向前一个节点
struct listNode *next; // 指向下一个节点
dup、free、match
} listNode;
- 在listNode节点的结构中有指向上一个节点的下一个节点的指针,他们都可以直线NULL,所以是一个无环链表
- listNode节点value使用的void指针保存节点,可以通过list的dup、free、match函数为该节点设置该节点类型的特定函数。
压缩列表
-
压缩链表设计特点是一种紧凑的数据结构,占用的是一片连续的内存空间。可以利用CPU缓存,针对不同长度的数据进行相应的编码,节省内存开销。
-
设计结构
- zlbytes:记录整个压缩列表占用内存的字节数
- zltail:记录列表尾部节点地址距离起始地址,列表尾的偏移量(尾部条目的偏移量)
- 设计目的:提供直接访问压缩列表尾部的能力,如果执行rpush、rpop操作的时候,就能避免每次从头部遍历
- zllen:列表中的元素数量
- zlend:列表的结束点,固定OXFF(255),列表的结束标志。列表的终点
- 设计目的:在遍历压缩列表的时候知道何时结束,避免多余的遍历。
- entry:列表节点
- prevlen:记录前一个节点的长度,目的是为了实现从后向前遍历
- encoding:记录当前节点实际数据的类型和长度,类型主要有两种:字符串和整型
- data:当前节点的实际数据,长度和类型由encoding决定
-
entry中的prevlen记录的是上一个节点的长度
- 前一个节点 小于254,prevlen占用1字节空间来保存长度
- 前一个节点 大于等于254,prevlen总用5个字节空间来保存长度
-
entry中的encoding记录的是实际数据的长度和类型
- 如果节点数是整数,则会使用1字节进行编码。确认整型之后就可以确认整型的大小了。
- 如果节点是字符串,根据字符串大小,encoding会根据使用的字节数进行编码。encoding编码的前两个bit位表示数据的类型,后续的其他bit位表示字符串长度
压缩列表是一定缺陷的,空间扩展的时候要重新分配内存。在每个节点中都有一个字段去记录上一个节点的长度,如果当前有一个节点之前的长度为254,那么他下一个节点只需要用1字节去记录,但是如果当前节点有修改,长度超过254的时候,那么他的下一个节点就要使用5个字节去记录,就会导致后边的所有节点都要扩展。
哈希表
- 数据结构
typedef struct dict {
dictType *type; // 字典类型,支持自定义类型(如字符串、哈希等)
void *privdata; // 私有数据
dictEntry **table; // 哈希表的数组
unsigned long size; // 哈希表大小
unsigned long used; // 当前哈希表已使用的槽数
dictht ht[2];
long rehashidx;
} dict;
typedef struct dictEntry {
void *key; // 键
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v; // 值
struct dictEntry *next; // 下一个链表元素
} dictEntry;
-
首先
dict是一个哈希表的结构,dictEntry是哈希表的每一个元素 -
dict中的dictEntry是一个数组,存放每一个元素的dictEntry -
dictEntry有一个指针,指向下一个元素的指针,这里是为了解决hash冲突的。- 哈希冲突:在进行哈希计算的时候,有可能两个key计算出来的hash值是一样的,这个时候就会出现一个坑需要占两个key的情况。这个时候就可以使用next这个指向下一个桶。这样就可以解决哈希冲突,这样就形成了一个哈希链表。
- 但是如果链表太长的情况下会退化成链表的情况,这个时候就需要进行扩容。
-
扩容机制(rehash)
- redis在定义dict结构体的时候,定义了两个hash表,正常情况下只会写入第一个hash表
- 如果触发扩容的情况下就需要将数据迁移到第二个hash表上,第二个hash表是第一个hash表的两倍
- 触发时机
负载因子 = hash表已保存节点的数量级 / hash表大小- 负载因子 >= 1:并且redis没有进行fork子进程的时候,也就是没有执行RDB快照或AOF重写的时候,就会进行rehash
- 负载因子 >= 5:这个时候说明hash冲突已经非常严重了,无论是否进行RDB或AOF都要进行rehash
-
扩容方式
- 采用渐进式扩容,如果一次性全部迁移的情况下有可能会发生性能抖动,所以采用渐进式的方式
- 当对当前hash进行增、删、改的时候就会顺序将原hash表上的所有key-value迁移到新的hash表上
- 当请求到一定量的时候就可以全部迁移至新的hash表。
- 在迁移过程中如果新增加的数据只会添加到新的hash表中吗,查询的情况就会先去旧的hash表中着,找不到就去新的hash表中着。
- 这样旧的hash表就会慢慢变空了
这里和go中的map扩容机制有点相同,当到达一定阈值的时候,也就就是看负载因子,然后进行渐进式扩容。
整数集合
- 底层来说他也是一片连续的内存空间
typedef struct intset {
uint32_t encoding; // 编码类型(INT16, INT32, INT64)
uint32_t length; // 元素个数
int8_t contents[]; // 实际存储元素的数组
} intset;
-
整数集合升级
- 当前集合元素中的类型都是int16的类型,如果这个时候插入一个int32元素的数据这个时候就会触发升级
- 这个时候就要进行扩展空间,这个时候就要按照新元素的类型进行分割,存放数据。
-
跳表
- Zset底层使用就数据结构就是跳表
typedef struct zskiplist {
struct zskiplistNode *header, *tail; // 头节点和尾节点
unsigned long length; // 节点总数
int level; // 当前跳表最大层数
} zskiplist;
typedef struct zskiplistNode {
robj *obj; // 成员对象(key)
double score; // 排序用的分数
struct zskiplistNode *backward; // 后退指针(可逆)
struct zskiplistLevel {
struct zskiplistNode *forward; // 前进指针
unsigned int span; // 跨度(用于计算排名)
} level[]; // 变长数组,支持多层索引
} zskiplistNode;
Level 3: A ───────────────→ D
Level 2: A ──────→ C ─────→ D
Level 1: A → B → C → D
- 一层一层的,感觉有点像二叉树,又不是二叉树的感觉
level[]中每一个元素元素代表跳表的一层,zskiplistNode是指向下一个跳表的指针,span表示跨度用来记录两个点之间的距离。- 每一层都包含多个节点,每一个节点通过指针进行连接。
- 每个跳表的节点都有一个后向指针
backward指向前一个节点,目的是为了方便跳表从尾节点开始访问,倒序方便。 - 两个相邻两层节点的比例为2:1,但是创建节点的时候,是随机生成每个节点数的,并不是很严格的执行按照2:1的比例。
- 层数的最大限制为64。
- 在创建节点的时候,会生成发范围为0-1的一个随机数,如果这个随机数小于0.25那么层数就是一层,然后继续生成下一个随机数,直到随机数的结果大于0.25结束,最终确定该节点的层数。
- 跳表与平衡树
- 从内存的上比较,跳表比平衡树更灵活一些。
- 范围性查找的时候,跳表的比平衡树操作更简单
- 从算法角度去比较,跳表比平衡树要简单得多
quicklist
- 结构:双向链表 + 压缩列表
typedef struct quicklist {
quicklistNode *head; // 头节点
quicklistNode *tail; // 尾节点
unsigned long count; // 总元素个数(所有节点中所有 entry 总数)
unsigned long len; // quicklistNode 节点个数(即链表长度)
int fill; // 压缩控制策略(每个 node 的最大 entry 数)
unsigned int compress; // 压缩深度(靠近两端的 node 不压缩)
} quicklist;
typedef struct quicklistNode {
struct quicklistNode *prev;
struct quicklistNode *next;
unsigned char *zl; // 指向 ziplist 或 listpack 的数据
unsigned int sz; // 当前 ziplist/listpack 的字节大小
unsigned int count; // 当前压缩列表中的 entry 数
unsigned char encoding : 2; // 编码方式:ZIPLIST / LISTPACK
unsigned char container : 2; // 存储方式:RAW / PLAIN
unsigned char recompress : 1; // 标记该节点是否被压缩过
void *extra; // 预留
} quicklistNode;
- 这里在
qicklistNode中的*zl保存一个压缩列表 - 在
quicklist用的是链表 - 插入数据的时候不像链表直接创建一个节点,而是会检查插入位置的压缩列表是否能容该元素,如果可以直接插入,如果不可以才会去创建
- quicklist会控制quicklistNode结构的压缩列表的大小或元素个数,规避掉连锁更新的风险
以上就是差不多的redis底层数据结构。