redis 常见数据类型实现原理

redis 常用的数据结构有以下五种:

  • String
  • List
  • Hash
  • Set
  • Sorted Set

之前简单的介绍了 String 实现原理 SDS,本篇我打算从全局层面一起介绍:

无论什么类型的数据结构,在 Redis 中都可以通过 RedisObject 来表示,该类型主要包含以下三个属性:

  • type:类型,对应上面提到五种数据类型,如:REDIS_STRING、REDIS_LIST 等等
  • encoding:编码格式
  • ptr:指针,指向具体的数据类型

Redis 中包含 incr 加一操作,也就是说可以存储 int 类型数据,然而实际使用时都使用 String 字符串来存储,此时实际就是通过 encoding 属性标记它为 int 类型数据,它的常见属性值如下:

#define REDIS_ENCODING_RAW 0            // 编码为字符串
#define REDIS_ENCODING_INT 1            // 编码为整数
#define REDIS_ENCODING_HT 2             // 编码为哈希表
#define REDIS_ENCODING_ZIPMAP 3         // 编码为 zipmap
#define REDIS_ENCODING_LINKEDLIST 4     // 编码为双端链表
#define REDIS_ENCODING_ZIPLIST 5        // 编码为压缩列表
#define REDIS_ENCODING_INTSET 6         // 编码为整数集合
#define REDIS_ENCODING_SKIPLIST 7       // 编码为跳跃表

实际通过 type 和 encoding 基本就可以确定 ptr 指针指向的数据类型,数据类型主要包含以下几种:SDS(简单动态字符串)、HashTable(哈希表、字典)、LinkedList(双端链表)、ZipList(压缩列表)、IntSet(整数集合)、SkipList(跳跃表)

SkipList 不只包含跳跃表,它实际包含一个字典和一个跳跃表,这点下面再细说

类型 + 编码和数据类型的对应关系如下图所示:
redis 常见数据类型实现原理_第1张图片
简单画个图总结一下:
redis 常见数据类型实现原理_第2张图片
一般情况下,ZipList 压缩列表总是在集合长度小于 512,元素长度都小于 64 字节时使用(ZSet 例外,长度大于 128 就用 SkipList)


有了上面的介绍,下面分别来看不同数据结构的源码:

SDS:

struct sdshdr{
 int len;/*字符串长度*/
 int free;/*未使用的字节长度*/
 char buf[];/*保存字符串的字节数组*/
}

和传统的 c 语言字符数组相比,SDS 具有以下优势:

  1. O(1) 获得字符数组长度
  2. 避免缓冲区溢出
  3. 内存预分配、惰性释放
  4. 可以存二进制数据
  5. 可以复用一部分 C 字符数组

ZipList:

压缩列表,本质上就是一个字节数组,是 Redis 为了节约内存而设计的一种线性数据结构,可以包含任意多个元素,每个元素可以是一个字节数组或一个整数
redis 常见数据类型实现原理_第3张图片

  • zlbytes:压缩列表的字节长度,占 4 个字节,因此压缩列表最长 ( 2 ^ 32 ) - 1 字节
  • zltail:压缩列表尾元素相对于压缩列表起始地址的偏移量,占 4 个字节
  • zllen:压缩列表的元素数目,占两个字节
  • entryX:压缩列表存储的若干个元素,可以为字节数组或者整数
  • zlend:压缩列表的结尾,占一个字节,恒为0xFF

HashTable:

Redis HashTable 与 java 中的 hashMap 类似,都是数组 + 链表,它的源码如下:

typedf struct dict{
    dictType *type;//类型特定函数,包括一些自定义函数,这些函数使得key和value能够存储
    void *private;//私有数据
    dictht ht[2];//两张hash表 
    int rehashidx;//rehash索引,字典没有进行rehash时,此值为-1
    unsigned long iterators; //正在迭代的迭代器数量
}dict;

typedef struct dictht{
     //哈希表数组
     dictEntry **table;
     //哈希表大小
     unsigned long size;
     //哈希表大小掩码,用于计算索引值
     //总是等于 size-1
     unsigned long sizemask;
     //该哈希表已有节点的数量
     unsigned long used; 
}dictht;

typedf struct dictEntry{
    void *key;//键
    union{
        void val;
        unit64_t u64;
        int64_t s64;
        double d;
    }v;//值
    struct dictEntry *next;//指向下一个节点的指针
}dictEntry;

举个简单的例子:
redis 常见数据类型实现原理_第4张图片
redis HashTable 扩容和缩容的策略如下:

  • 服务器目前没有执行 BGSAVE、BGREWRITEAOF命令,并且负载因子大于等于1
  • 服务器目前正在执行 BGSAVE、BGREWRITEAOF命令,并且负载因子大于等于5

负载因子:已保存元素个数 / 哈希表大小

ht[0] 和 ht[1] 用于渐进式 hash 算法:也就是说扩容和收缩不是一次性,集中式地完成,而是通过多次逐渐地完成的

过程:ht[0] 和 ht[1] 会同时保存数据,ht[0] 指向旧哈希表,ht[1] 指向新哈希表,每次对字典执行添加、删除、查找或者更新操作时,程序除了执行指定的操作以外,还会顺带将 ht[0] 的元素迁移到 ht[1] 中

除了字段操作,redis 存在定时任务进行迁移

rehashidx 为 0 表示迁移开始,为 -1 表示迁移完成

好处:当元素特别大时,不会因为一下子迁移过多元素导致 redis 性能急剧下降

缺点:维护两个 hash 表,内存占用大,可能触发内存淘汰策略


LinkedList:

双向链表,就类似 java 中的 LinkedList,不过相比 LinkedList,它做了改进:

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;

redis 常见数据类型实现原理_第5张图片
存在指针分别指向头和尾,还包含属性记录 LInkedList 长度


IntSet:

如果 value 可以转成整数值,并且长度不超过 512 的话就使用 intset 存储,否则采用 HashTable

typedef struct intset{
    uint32_t encoding;//编码方式

    uint32_t length;//集合包含的元素数量

    int8_t contents[];//保存元素的数组
}intset;

redis 常见数据类型实现原理_第6张图片
使用 HashTable 时就像 java 中的 HashSet 和 HashMap 的关系


SkipList:

跳跃表,一个跳跃表包含一个 ZSkipList 和 一个 HashTable

redis 常见数据类型实现原理_第7张图片
这里既使用跳跃表,又使用字典的原因在于:单查询使用字典效率极高,范围查询使用跳跃表,效率极高。一种通过空间换时间的思路

跳跃表的核心在于跳跃,对于有序链表,每次跳过一个元素太慢了,通过跳跃表每次跳过多个,提高查询效率


参考:画图找源码太慢了,所以图和源码基本都是从其它地方复制过来的

https://zhuanlan.zhihu.com/p/344918922

你可能感兴趣的:(redis,redis,数据结构,数据库)