Redis底层 | 青训营笔记

176 阅读6分钟

“这是我参与「第三届青训营 -后端场」笔记创作活动的第3篇笔记

Redis底层数据结构

在青训营课程中,缓存是提升系统性能的重要组件,Redis作为缓存中间件中的重要一员,今天我们来聊聊Redis中的底层数据结构。

1.SDS

简单动态字符串,是Redis所构建的一个抽象类型,主要有两个作用

  • 默认的字符串保存方式
  • 缓冲区(AOF的AOF缓冲区就使用它)

1.1 定义

struct sdshdr{
   int len; //记录buf数组中已使用字节的数量,是SDS的长度
   int free; //记录buf中未使用的字符串数量
   char buf[]; //用来保存字符串
}

1.2 SDS与C字符串区别

  1. O(1)时间复杂度获取长度,也就是使用sdshdr->len属性获取
  2. 杜绝缓冲区溢出,当进行字符串修改的时候,会先判断SDS的空间是否满足修改所需的要求,如果不满足,API自动将SDS的空间扩展至所需的大小,然后进行修改。
  3. 减少了修改字符串重分配次数,使用了空间预分配惰性空间释放两种优化策略。
  4. 二进制安全,C字符串只能保存符合ASCII编码的文本数据,对字符串结尾的识别只有‘\0’,也就是说如果字符串buf数组包含'\0',那么而SDS是通过sdshdr->len来获取字符串长度。

空间预分配:当对SDS进行扩展的时候,程序不仅会为SDS分配所必须的空间,还会为SDS分配未使用的空间。

  • 对SDS修改后,如果长度小于1MB,那么就会free属性就会分配和len属性同样大小的长度
  • 对SDS修改后,如果长度大于1MB,那么会额外分配1MB的空间。

惰性空间释放:当需要缩点SDS字符长度的时候,不会立即释放空闲的字符串,而是会使用free记录起来。

2.链表

2.1 定义

每个链表节点由adlist.h/listNode定义

type struct listNode{
     struct listNode *prev;
     struct listNode *tail;
     void *value;
}listNode;

使用adlist.h/list来持有并管理链表

typedef struct list{
     listNode* head; //链表头
     listNode* tail; //链表尾
     unsigned long len; //链表长度
     void *(*dup) (void *ptr); //节点复制函数
     void (*free) (void *ptr); //节点释放函数
     int (*match) (void *ptr, void *key) //节点值比对函数 
}
  • 双端无环
  • 获取长度是O(1)的
  • 获取表头结点和表尾节点是O(1)的
  • 多态:使用void*指针来保存节点值,并通过list结构的dup、free、match来保存不同的值。

3.字典

字典是保存键值对的抽象数据结构。

3.1 定义

//哈希表
typedef struct dicht{
     //用来保存 Entry 指针的数组
     dictEntry **table;
     //哈希表的大小
     unsigned long size;
     //哈希表的大小掩码,用于计算索引值, 总是等于size-1
     unsigned long sizemark;
     //该hash表的已有节点的数量
     unsigned long used;
}dicht 
typedef struct dictEntry{
     void *key;
     //val
     union{
          void *val;
          uint64_tu64;
          int64_ts64;
     } v;
     //hash冲突的时候,指向下个节点的指针
     struct dictEntry *next;
}
typedef struct dict{
     //类型特定函数,type包含了该类型的一些api
     dictType *type;
     //保存了穿给特定函数的可选参数
     void *privdata;
     //hash表的个数,ht[1]只有在ht[0] rehash的时候才会使用
     dictht ht[2];
     //rehash索引, 初始值为 -1
    	int hashidx;
}

3.2 哈希算法

通过哈希值和掩码来确定索引值。具体使用MurmurHash2算法来确定。

hash = dict->type->hashFunction(key); //获取key对应的hash值
index = hash & (dict->ht[x].sizemask); //获取索引值

3.3 哈希冲突

使用拉链发来存储,通过头插法来实现

3-4 rehash

在重新散列过程中,将ht[0]的数据rehash到ht[1],在rehash过程是渐进式的,具体步骤如下:

  • 为ht[1]分配空间
  • 首先将hashidx=0,代表rehash开始
  • 对于每个哈希表的hashidx位置,将其重新hash到ht[1]
  • 全部rehash完成

问题一:为ht[1]分配多少空间?

  1. 如果是扩展操作,那么应该是第一个大于等于ht[0].used*22n2^n
  2. 如果是删除操作,那么应该是第一个大于等于ht[0].used2n2 ^ n

问题二:rehash期间的哈希表操作?

增删改查操作会首先去ht[0]寻找,如果未找到那么去ht[1]寻找。新增的键值对都会保存到ht[1]里面,而ht[0]不再进行任何添加操作。

4.跳跃表

跳跃表由redis.h/zsiplistNoderedis.h/zskiplist两个结构定义。

typedef struct zskiplistNode {
     struct zskiplistLevel{
          struct zskiplistNode *forward;
          unsigned int span;
     }level[];//节点的层
     double score;//节点的权重
     struct zskiplistNode *backward; //回退指针
     robj *obj;//成员对象
}zskiplistNode;

//用于管理节点
typedef struct zskiplist{
     struct zskiplistNode *head, *tail;
     unsigned long length;//节点的数量
     int level; //最大层数  
}

在生成节点的时候,会根据幂次定律随机生成一个介于1~31之间的值作为level层的大小,这也是层的高度。

当查找的时候,会按照下述步骤进行查找:

  1. 首先从最高level的第一节点出发
  2. 遍历该层的所有节点,该level节点的forward也就是下一节点大于target,那么就会下降一层
  3. 下降完成后,继续操作2,直到找到为止

5.压缩链表

5.1 压缩列表组成

压缩列表是列表键和哈希键的底层实现之一。

zlbyteszltailzllenentry1entry2….zlend
记录整个压缩链表的所占字节数记录链表表尾节点距离压缩链表的起始位置由多少字节记录压缩链表的节点数量节点1节点2标记压缩列表的末端

5.1 压缩列表节点Entry组成

previous_entry_lengthencodingcontent
记录压缩列表前一个节点的长度大小此节点记录content属所保存的类型和长度存放字节数组或者整数

previous_entry_length:我们可以通过zltail拿到尾部节点,然后尾节点地址减去前一个长度得到前一个节点的首部地址,以此类推,可以遍历整个列表。

encoding编码格式:

  • 如果content存放的是节点数组,那么编码前两位为00,01,10
  • 如果content存放的是整数,那么编码是以开头为11的整数编码

重要总结

  1. Redis数据库中的每个键值对的键和值都是一个对象。
  2. Redis的对象系统带有引用计数实现的内存回收机制,当一个对象不再被使用时,对象所占用内存就会自动释放。
  3. Redis会共享值为 0-9999的字符串对象
  4. 对象会记录自己最后一次被访问的时间,可以用来计算空转时长。