本文共 8787 字,大约阅读时间需要 29 分钟。
由于C字符串存在大量问题,所以在Redis中,并没有使用C风格字符串,而是自己构建了一个简单动态字符串即SDS(simple dynamic string)
struct sdshdr { // buf 中已占用空间的长度 int len; // buf 中剩余可用空间的长度 int free; // 数据空间 char buf[];};
为解决C字符串缓冲区溢出问题以及长度计算问题,SDS中引入了len来统计当前已使用空间长度,free来计算剩余的空间长度
C字符串的主要缺陷就是因为它没有记录自己的长度,而如果在需要了解长度时,就只能通过O(N)的效率进行一次遍历
由于Redis作为一个高效的内存数据库,用于速度要求严苛,插入删除频繁
的场景,为了提高内存分配的效率,防止大量使用内存重分配而调用系统函数导致的性能损失问题(用户态和内核态的切换),Redis主要依靠空间预分配和惰性空间释放来解决这个问题为减少空间分配的次数,当需要进行空间拓展时,不仅仅会为SDS分配修改所必须要的空间,并且会为SDS预分配额外的未使用空间。
预分配未使用空间的策略如下
当我们对SDS进行删除操作时,并不会立即回收删除后空余的空间,而是将空余空间以free字段记录下来,以备后面使用。
这样做的目的在于防止因为空间缩短后因为再度插入导致的空间拓展问题。 并且如果有需求需要真正释放空间,Redis也提供了对应的API,所以不必担心会因为惰性的空间释放而导致的内存浪费问题。比起 C 字符串, SDS 具有以下优点:
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;
同时为了实现多态与泛型,链表中还提供了dup,free,match属性来设置相关的函数,使得链表支持不同类型的值的存储
Redis的字典底层采用了哈希表来进行实现。
首先看看字典底层哈希表的结构
typedef struct dictht { // 哈希表数组 dictEntry **table; // 哈希表大小 unsigned long size; // 哈希表大小掩码,用于计算索引值 // 总是等于 size - 1 unsigned long sizemask; // 该哈希表已有节点的数量 unsigned long used;} dictht;
为解决哈希冲突,Redis字典采用了链地址法来构造了哈希桶的结构,也就是哈希数组中的每个元素都是一个链表。
下面来看看哈希节点的结构
typedef struct dictEntry { // 键 void *key; // 值 union { void *val; uint64_t u64; int64_t s64; } v; // 指向下个哈希表节点,形成链表 struct dictEntry *next;} dictEntry;
可以看到,为保证键值对适用于多重类型,key值使用的时void的形式,而value使用了64位有符号整型和64位无符号整型,void指针的一个联合体,每个节点使用next来链接成一个链表
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 *privdata, void *obj);} dictType;
为保证字典具有多态及泛型,dictType中提供了如哈希函数以及K-V的各种操作函数,使得字典适用于多重情景
/* * 字典 */typedef struct dict { // 类型特定函数 dictType *type; // 私有数据 void *privdata; // 哈希表 dictht ht[2]; // rehash 索引 // 当 rehash 不在进行时,值为 -1 int rehashidx; /* rehashing not in progress if rehashidx == -1 */ // 目前正在运行的安全迭代器的数量 int iterators; /* number of iterators currently running */} dict;
从字典的结构中,我们可以看到里面同时存放了两个哈希表,以及一个rehashidx属性。
这就牵扯到了字典的核心之一,rehash。Redis作为一个插入频繁且对效率要求高的数据库,当插入的数据过多时,就会因为哈希表中的负载因子过高而导致查询或者插入的效率降低,此时就需要通过rehash来进行重新扩容并重新映射。
但是如果只是用一个哈希表,映射时就会导致数据库暂时不可用,作为一个使用频繁的数据库,短期的停机几乎是不可容许的问题,所以Redis设计时采用了双哈希的结构,并采用了渐进式rehash的方法来解决这个问题。rehash的步骤如下
由于数据库中可能存在大量的数据,而rehash的时候又过长,为了避免因为rehash造成的服务器停机,rehash的过程并不是一次完成的,而是一个多次的,渐进式的过程。
并且如果此时新插入节点,都会统一的防止在新表ht[1]中,防止对ht[0]的rehash造成干扰,保证ht[0]节点的只减少不增加
跳表是一个较为少见的数据结构,如果不了解的可以看看我之前的博客
由于跳表的实现简单且性能可与平衡树相媲美,对于大量插入删除的数据库来说,跳表只需要进行简单的链表插入和索引的选拔,而不像平衡树一样需要进行整体平衡的维持。并且由于在范围查找上的效率远远强于平衡树,所以Redis底层选取跳表来作为有序集合的底层之一。typedef struct zskiplistNode { // 成员对象 robj *obj; // 分值 double score; // 后退指针 struct zskiplistNode *backward; // 层 struct zskiplistLevel { // 前进指针 struct zskiplistNode *forward; // 跨度 unsigned int span; } level[];} zskiplistNode;
跳跃表的查询从最顶层出发,通过前进指针来往后查找,通过比较节点的分数来判断当前节点是否与索引匹配,如果查找不到则进入下层继续查找,并记录下跨越的层数span来进行排位。
/* * 跳跃表 */typedef struct zskiplist { // 表头节点和表尾节点 struct zskiplistNode *header, *tail; // 表中节点的数量 unsigned long length; // 表中层数最大的节点的层数 int level; } zskiplist;
跳跃表通过保存表头和表尾节点,来快速访问表头和表尾。并且保存了节点的数量来实现O(1)的长度计算
整数集合时集合键的底层实现之一,当集合中的元素全部都是整数值的时候,并且集合中元素不多时,Redis就会使用整数集合来作为集合键的底层结构。整数集合具有去重且排序的特性
typedef struct intset { // 编码方式 uint32_t encoding; // 集合包含的元素数量 uint32_t length; // 保存元素的数组 int8_t contents[];} intset;
在上面的结构中,虽然content数组的类型是一个8bit的整型,但是数据真正存储的方式并不是这个类型,而是根据encoding来决定具体的类型,8bit只是作为一个基本单位来进行使用。
例如此时encoding设置为INTSET_ENC_INT16时,数组存储的格式就有每个元素16bit
升级的流程
新元素的插入位置
由于会引起升级的元素的类型都必顶比数组中的所有数据都大,所以也就决定了其要么比所有数据都大,要么比所有数据都小(负数),所以插入位置只能是首部和尾部插入元素
在整数列表中升级是一个不可逆的过程,即使将所有高类型的数据删除了,也不会进行降级。
理由是防止因为降级后再次升级带来的大量数据挪动的问题,在保证了效率的同时,也带来了一定程度上的空间浪费(非必要时尽量不要升级)压缩列表(ziplist)是列表键和哈希键的底层实现之一。
当一个列表键只包含少量列表项, 并且每个列表项要么就是小整数值, 要么就是长度比较短的字符串, 那么 Redis 就会使用压缩列表来做列表键的底层实现。主要核心就是为了节约空间
整数的类型可以是以下六种之一:
字节数组可以是以下三种之一:
而压缩列表节点又有三个属性组成,分别是previous_entry_length,encoding,content。
previous_entry_length
这个属性记录了压缩列表前一个节点的长度,该属性根据前一个节点的大小不同可以是1个字节或者5个字节。小于254字节时的表示
encoding
encoding通过以下规则来记录content的类型content
content属性负责保存节点的值,值的具体类型由上一个字段encoding来决定。例如存储字节数组,00表示类型为字节数组,01011表示长度为11
当添加或删除节点时,可能就会因为previous_entry_length的变化导致发生连锁的更新操作。
即使存在这种情况,但是并不影响我们使用压缩列表
转载地址:http://gfmsz.baihongyu.com/