Redis底层数据结构

Redis数据结构

  • Redis介绍
    • redis简介
    • redis的数据结构
      • 字符串
      • 列表
        • 压缩列表
        • 链表
      • 哈希
      • 集合
        • 整数集合
      • 有序集合
        • 压缩列表
        • skiplist&dict
          • 跳表
    • 总结

Redis介绍

redis简介

redis作为一个非关系型数据库,因为其速度快、数据结构多、可持久化而被广泛运用在缓存应用中,本文参照Redis的设计与实现一书,对redis的底层数据结构做简单的介绍。

redis的数据结构

redis有五种数据结构,分别是字符串、列表、哈希、集合、有序集合。接下来对着五种数据结构做简单介绍

字符串

Redis没有直接使用C语言传统的字符串表示,而是自己构建了一种名为简单动态字符串的抽象类型(SDS)。

SDS用shs.h/sdshdr结构来表示:

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

注解:

  1. buf属性是一个char类型的数组,如果保存了redis这个字符串,则buf数组的前五个分别保存了’R’,‘e’,‘d’,‘i’,‘s’五个字符,而最后一个字节则保存了空字符’\0’。SDS仍然遵循了C字符串以空字符结尾的惯例,这个空字符的字节空间不计算在SDS的len属性里面,也不占用free数组的空间,对SDS的使用者来说是完全透明的。
  2. buf属性称为字节数组–这个数组来保存一系列二进制数据,而不是保存字符

SDS的优势:

  1. 常数获取字符串长度(直接获取len字段即可)
  2. 杜绝缓冲区溢出:当SDS需要修改时,SDSAPI会先检查SDS的空间是否满足修改所需的要求,如果不满足的话,API会自动将SDS的空间扩展至执行修改所需的大小。
  3. 减少内存分配次数(空间预分配和惰性空间释放)
  4. 二进制安全
    1. 二进制安全:指在一个二进制文件上所执行的不更改文件内容的功能或操作
    2. 所有SDSAPI都会以处理二进制的方式来处理SDS存放在buf数组里的数据,程序不会对其中的数据做任何限制、过滤、或者假设,数据在写入时是什么样的,它被读取时就是什么样
    3. C语言在获取字符串长度时,会遍历获取遇到空字符就会认为字符串结束,所以只能保存文本数据,而不能保存像图片、音频、视频、压缩文件这样的二进制数据。SDS是通过本身的len属性获取长度的,所以SDS是二进制安全的,可以保存任意格式的二进制数据

列表

Redis针对列表中数据量的大小,分别采用了两种实现方式:压缩列表和链表;

压缩列表

当一个列表键只包含少量列表项,并且每个列表项要么就是小整数值,要么就是长度比较短的字符串,那么Redis就会使用压缩列表来做列表建的底层实现。

所有元素长度小于64bytes并且元素数量小于512

压缩是为了节约内存而开发的。

该结构是由一系列特殊编码的连续内存块组成的顺序型数据结构。一个压缩列表可以包含任意多个节点(entry),每个节点可以保存一个字节数组或者一个整数值。

zlbytes zltail zllen entry1 entry2 entryN zlend
记录整个压缩列表占用的内存字节数 记录压缩列表表尾结点距离压缩列表的起始地址有多少字节 记录了压缩列表包含的节点数量 列表节点 列表节点 列表节点 特殊值0xFF(十进制255),用于标记压缩列表的末端

链表

当一个列表键包含了数量比较多的元素,又或者列表中包含的元素都是比较长的字符串时,Redis就会使用链表作为列表建的底层实现。

列表使用adlist.h/list来持有链表:

typedef struct list {
	listNode *head;      // 表头节点
	listNode *tail;         // 表尾节点
	unsigned    long   len;    //链表所包含的节点数量
	listNode   node      //节点
	void *(*dup)  (void *ptr)    //节点值复制函数
	void (*free) (void *ptr)      //节点值释放函数
	int  (*match) (void *ptr, void *key)   //节点值对比函数
}  list;

list结构为链表提供了表头指针head、表尾指针tail,以及链表长度计数器len,而dup、free和match成员则是用于实现多态链表所需的类型特定函数。

链表节点使用的是adlist.h/listNde结构来表示:

typedef struct listNode {
	struct  listNode *prev;        		// 前置节点
	struct  listNode *next;				//后置节点
	void    *value;							//节点的值
}

多个listNode可以通过prev和next指针组成双端链表。

Redis的链表实现的特性:

  1. 双端:链表节点带有prev和next指针
  2. 无环:表头节点的prev指针和表尾节点的next指针都指向NULL
  3. 带表头指针和表尾指针
  4. 带链表长度计数器
  5. 多态:链表节点使用void *指针来保存节点的值,并且可以通过list结构的dup、free、match三个属性为节点值设置类型特定函数,所以链表可以用于保存各种不同类型的值。

哈希

当一个哈希键只包含少量键值对,并且每个键值对的键和值要么就是小整数值,要么就是长度比较短的字符串,那么redis就会使用压缩列表来实现哈希键的底层实现。(所有元素长度小于64bytes并且元素数量小于512)

当一个哈希键包含的键值对比较多,又或者键值对中的元素都是比较长的字符串时,Redis就会使用字典作为哈希键的底层实现。

Redis的字典使用哈希表作为底层实现,一个哈希表里面可以有多个哈希表节点,而每个哈希表节点就保存了字典中的一个键值对。

Redis字典所使用的哈希表由dict.h/dictht结构定义:

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

table属性是一个数组,数组中的每个元素都是一个指向dict.h/dictEntry 结构的指针,每个底dictEntry结构保存着一个键值对。

typedef struct dictEntry {
	void   *key;			//键
	union {
		void  *val;
		uint64_t  u64;
		int64_t  s64;
	} v;
	struct dictEntry *next;		//指向下个哈希表节点,形成链表(拉链法解决哈希表冲突时使用)
}

集合

Redis针对集合中的数据的大小,有两种存储方式:整数集合和哈希表

整数集合

保存的是整数值(long)并且数量小于512,就用整数集合来存储。可以保存int16_t,int32_t,int64_t类型的整数值,并且集合中不会出现重复元素

intset的数据结构:

typedef struct intset {
    uint32_t encoding;  //编码方式
    uint32_t length;  //集合包含的元素数量
    int8_t contents[]; //保存元素的数组
} intset;

整数集合的每个元素都是 contents 数组的一个数组项(item), 各个项在数组中按值的大小从小到大有序地排列, 并且数组中不包含任何重复项。

虽然contents[]声明了int8_t类型的数组,但实际上contents[]并不保存任何int8_t类型的数据,真正的数据类型取决于encoding的类型。

encoding类型有以下三种:

  1. #define INTSET_ENC_INT16 (sizeof(int16_t))
  2. #define INTSET_ENC_INT32 (sizeof(int32_t))
  3. #define INTSET_ENC_INT64 (sizeof(int64_t))

有序集合

Redis对有序集合的数量,分别采用压缩列表和skiplist&dict的来实现

压缩列表

元素数量< 128并且每个元素的长度<64bytes时用压缩列表来存储

每个集合节点使用两个紧邻的ziplist节点保存,第一个节点保存元素成员(member),第二个元素保存元素分值(score).有序集合在压缩列表中按分值从小到达排序

skiplist&dict

skiplist编码的有序集合对象使用zset结构作为底层实现,一个zset结构同时包含一个字典和一个跳跃表:

typedef {
	zskiplist  *zsl;   //跳表 
	dict  *dict;     //字典
}zset;

zset结构中的跳表按照分值从小到大保存了所有集合元素,每个跳表节点都保存一个集合元素。
通过跳跃表,程序可以对zset进行范围型操作,如ZRANK, ZRANGE就是通过跳跃表的API实现的。

跳表

skiplist跳表的结构:

typedef struct zskiplist {

    struct zskiplistNode *header, *tail;      // 表头节点和表尾节点

    unsigned long length;							// 表中节点的数量

    int level;													 // 表中层数最大的节点的层数

} zskiplist;

level用于在O(1)的时间复杂度内获取跳表的层高最大的节点的层数量,表头节点的层高并不计算在内。

一个跳表由多个跳表节点构成:

typedef struct zskiplistNode {

    struct zskiplistNode *backward;					    // 后退指针

    double score;						    			// 分值

    robj *obj;						   				   // 成员对象

    struct zskiplistLevel {				   			   // 层

        struct zskiplistNode *forward;				  // 前进指针

        unsigned int span;				        	  // 跨度

    } level[];

} zskiplistNode;
  1. 分值:是一个 double 类型的浮点数, 跳跃表中的所有节点都按分值从小到大来排序。
  2. 对象:节点的成员对象(obj 属性)是一个指针, 它指向一个字符串对象, 而字符串对象则保存着一个 SDS 值。
    注:在同一个跳跃表中, 各个节点保存的成员对象必须是唯一的, 但是多个节点保存的分值却可以是相同的: 分值相同的节点将按照成员对象在字典序中的大小来进行排序, 成员对象较小的节点会排在前面(靠近表头的方向), 而成员对象较大的节点则会排在后面(靠近表尾的方向)。
  3. 层:
    1. 前进指针:程序可以通过这些前进指针来加快访问其他节点的速度, 一般来说, 层的数量越多, 访问其他节点的速度就越快。一次可以跳过多个节点
    2. 跨度:用于记录两个节点之间的距离,两个节点之间的跨度越大,它们距离越远
  4. 后退指针:用于从表尾向表头方向访问节点: 跟可以一次跳过多个节点的前进指针不同, 因为每个节点只有一个后退指针, 所以每次只能后退至前一个节点

总结

数据结构与对应的实现

类型 编码 对象
REDIS_STRING REDIS_ENCODING_INT 使用整数值实现的字符串对象
REDIS_STRING REDIS_ENCODING_EMBSTR 使用embstr编码的简单动态字符串实现的字符串对象
REDIS_STRING REDIS_ENCODING_RAW 使用简单动态字符串实现的字符串对象
REDIS_LIST REDIS_ENCODING_ZIPLIST 使用压缩列表实现的列表对象
REDIS_LIST REDIS_ENCODING_LINKEDLIST 使用双端链表实现的列表对象
REDIS_HASH REDIS_ENCODING_ZIPLIST 使用压缩列表实现的哈希对象
REDIS_HASH REDIS_ENCODING_HT 使用字典实现的哈希对象
REDIS_SET REDIS_ENCODING_INTSET 使用整数集合实现的集合对象
REDIS_SET REDIS_ENCODING_HT 使用字典实现的集合对象
REDIS_ZSET REDIS_ENCODING_ZIPLIST 使用压缩列表实现的有序集合对象
REDIS_ZSET REDIS_ENCODING_SKIPLIST 使用跳跃表和字典实现的有序集合对象

你可能感兴趣的:(redis)