Redis学习笔记---Redis的底层数据结构

Redis学习笔记—Redis的底层数据结构

1.Redis作为Key-Value存储系统

Redis学习笔记---Redis的底层数据结构_第1张图片

  1. Redis使用ANSI,c语言编写,
  2. Redis中的key是字符串类型,当然也有其他类型,但是都会被转成字符串类型
  3. value的数据类型有
    1. 常用的:string字符串类型、list列表类型、set集合类型、sortedset(zset)有序集合类型、hash类型。
    2. 不常见的:bitmap位图类型、geo地理位置类型。
    3. Redis5.0新增一种:stream类型
    4. Redis中命令是忽略大小写,(set SET),但是key是不忽略大小写的 (NAME name)
  4. Redis没有表的概念,Redis实例所对应的db以编号区分,db本身就是key的命名空间。
  5. 比如:user:1000作为key值,表示在user这个命名空间下id为1000的元素,类似于user表的id=1000的行。

2.RedisDB结构

  1. Redis中存在“数据库”的概念,该结构由redis.h中的redisDb定义。
  2. 当redis 服务器初始化时,会预先分配 16 个数据库,所有数据库保存到结构 redisServer 的一个成员 redisServer.db 数组中
  3. redisClient中存在一个名叫db的指针指向当前使用的数据库,redisClient接收命令发送到redisServer
  4. RedisDB结构体源码
    typedef struct redisDb { 
            //id是数据库序号,为0-15(默认Redis有16个数据库) 
    		int id; 
    		
    		//存储的数据库对象的平均ttl(time to live,生命周期),用于统计
    		long avg_ttl;  
    		
    		//redis中所有的存储都是dict(字典,就是hash),存储数据库所有的key-value
    		dict *dict; 
    
    		//存储key的过期时间 
    		dict *expires; 
    		
    		//blpop 存储阻塞key和客户端对象 
    		dict *blocking_keys;
    		
    		//阻塞后push 响应阻塞客户端 存储阻塞后push的key和客户端对象 
    		dict *ready_keys;
    		
    		//存储watch监控的的key和客户端对象 
    		dict *watched_keys;
    } redisDb;
    
redisDb比较重的属性
  1. id:id是数据库序号,为0-15(默认Redis有16个数据库)
  2. dict:存储数据库所有的key-value
  3. expires:存储key的过期时间

3.RedisObject结构

  1. RedisObject也就是Value是一个对象:包含字符串对象列表对象哈希对象集合对象有序集合对象

  2. 结构信息概览:

    typedef struct redisObject { 
      unsigned type:4;//类型 对象类型 
      unsigned encoding:4;//编码 
      void *ptr;//指向底层实现数据结构的指针 
      //... 
      int refcount;//引用计数 
      //... 
      unsigned lru:LRU_BITS; //LRU_BITS为24bit 记录最后一次被命令程序访问的时间 
    //... 
    }robj;
    
  3. type:表示对象的类型,占 4 位;
    1. REDIS_STRING(字符串)
    2. REDIS_LIST (列表)
    3. REDIS_HASH(哈希)
    4. REDIS_SET(集合)
    5. REDIS_ZSET(有序集合)
  4. 当我们执行type命令时,便是通过读取RedisObjecttype字段获得该字段对象的类型

  5. encoding:表示对象的内部编码,占 4 位
    1. 每个对象有不同的实现编码
    2. Redis可以根据不同的使用场景来为对象设置不同的编码,大大提高了Redis的灵活性和效率,也是为了提高存储效率。
    3. 通过object encoding命令,可以查看对象的Value采用的编码方式
  6. LRU:24位
    1. lru:记录的是对象最后一次被命令程序访问的时间,( 4.0 版本占 24 位,2.6 版本占 22 位)。
    2. 高16位存储一个分钟数级别的时间戳,低8位存储访问计数(lfu : 最近访问次数)
    3. lru----> 高16位: 最后被访问的时间
    4. lfu----->低8位:最近访问次数
  7. refcount:记录的是该对象被引用的次数,类型为整型。
    1. refcount的作用,主要在于对象的引用计数和内存回收。
    2. 当对象的refcount>1时,称为共享对象
    3. Redis为了节省内存,当有一些对象重复出现时,新的程序不会创建新的对象,而是仍然使用原来的对象。
  8. ptr
    1. ptr指针指向具体的数据,
    2. 比如:set hello world,ptr 指向包含字符串 world 的 SDS。

4.RedisObject的7种Type—字符串对象,使用SDS结构存储

  1. SDS结构如下:
struct sdshdr{ 
	//记录buf数组中已使用字节的数量 
	int len; 
	//记录 buf 数组中未使用字节的数量 
	int free; 
	//字符数组,用于保存字符串 
	char buf[]; 
}
  1. SDS类型:Redis不是直接使用了字符串,而是使用了 SDS(Simple Dynamic String)。用于存储字符串和整型数据。
  2. 结构图如下
    Redis学习笔记---Redis的底层数据结构_第2张图片
  3. buf[ ]的长度=len+free+1
SDS的优势
  1. SDSC 字符串的基础上加入了freelen字段,获取字符串长度:SDS 是 O(1),C 字符串是O(n)
  2. SDS 由于记录了长度,在可能造成缓冲区溢出时会自动重新分配内存,杜绝了缓冲区溢出。
  3. 可以存取二进制数据,以字符串长度len来作为结束标识

5.RedisObject的7种Type—跳跃表

  1. 跳跃表是有序集合(sorted-set)的底层实现,效率高,实现简单。
  2. 跳跃表的基本思想:将有序链表中的部分节点分层,每一层都是一个有序链表。
  3. 查找:在查找时优先从最高层开始向后查找,当到达某个节点时,如果next节点值大于要查找的值或next指针指向null,则从当前节点下降一层继续向后查找。
  4. 举例:
    1. 查找元素9,按道理我们需要从头结点开始遍历,一共遍历8个结点才能找到元素9(最底层开始一个一个查询)
    2. 第一次分层:遍历5次找到元素9(倒数第二层开始:0,2,6,8,9)
    3. 第二次分层:遍历4次找到元素9(第二层开始:0,6,8,9)
    4. 第三层分层:遍历4次找到元素9(第一层开始:0,6,8,9)
      Redis学习笔记---Redis的底层数据结构_第3张图片
  5. 这种数据结构,就是跳跃表,它具有二分查找的功能。
  6. 插入
    1. 上面例子中,9个结点,一共4层,是理想的跳跃表。
    2. 通过抛硬币(概率1/2)的方式来决定新插入结点跨越的层数:正面:插入上层,背面:不插入,达到1/2概率(计算次数)
  7. 删除:找到指定元素并删除每层的该元素即可
  8. 跳跃表特点
    1. 每层都是一个有序链表
    2. 查找次数近似于层数(1/2)
    3. 底层包含所有元素
    4. 空间复杂度 O(n) 扩充了一倍
  9. Redis跳跃表的实现:
//跳跃表节点 
typedef struct zskiplistNode { 	
	/* 存储字符串类型数据 redis3.0版本中使用robj类型表示, 但是在redis4.0.1中直接使用sds类型表示 */ 
	sds ele; 
	//存储排序的分值 
	double score;
	//后退指针,指向当前节点最底层的前一个节点 /* 层,柔性数组,随机生成1-64的值 */
	struct zskiplistNode *backward;

struct zskiplistLevel { 
	//指向本层下一个节点 
	struct zskiplistNode *forward; 
	//本层下个节点到本节点的元素个数 
	unsigned int span;
	} level[]; 
} zskiplistNode; 

//链表 
typedef struct zskiplist{ 
	//表头节点和表尾节点 
	structz skiplistNode *header, *tail; 
	//表中节点的数量 
	unsigned long length; 
	//表中层数最大的节点的层数 
	int level; 
}zskiplist;
  1. 完整的跳跃表结构体
    Redis学习笔记---Redis的底层数据结构_第4张图片
  2. 跳跃表的优势
    1. 可以快速查找到需要的节点 O(logn)
    2. 可以在O(1)的时间复杂度下,快速获得跳跃表的头节点、尾结点、长度和高度。
    3. 应用场景:有序集合的实现

6.RedisObject的7种Type—字典

  1. 字典dict又称散列表(hash),是用来存储键值对的一种数据结构。
  2. Redis整个数据库是用字典来存储的。(K-V结构)
  3. 对Redis进行CURD操作其实就是对字典中的数据进行CURD操作。
  4. Redis字典实现包括:字典(dict)、Hash表(dictht)、Hash表节点(dictEntry)
    Redis学习笔记---Redis的底层数据结构_第5张图片
  5. 字典的数据结构:
typedef struct dict { 

	// 该字典对应的特定操作函数 
	dictType *type; 
	
	// 上述类型函数对应的可选参数 
	void *privdata; 
	
	/* 两张哈希表,存储键值对数据,ht[0]为原生 哈希表, ht[1]为 rehash 哈希表 */ 
	dictht ht[2]; 
	
	/*rehash标识 当等于-1时表示没有在 rehash, 否则表示正在进行rehash操作,存储的值表示 hash表 ht[0]的rehash进行到哪个索引值 (数组下标)*/ 
	long rehashidx;
	 
	// 当前运行的迭代器数量 
	int iterators; 
} dict;
  1. type字段,指向dictType结构体,里边包括了对该字典操作的函数指针
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;
  1. Redis字典除了主数据库的K-V数据存储以外,还可以用于:散列表对象、哨兵模式中的主从节点管理等在不同的应用中,字典的形态都可能不同,dictType是为了实现各种形态的字典而抽象出来的操作函数(多态)。
  2. 完整的Redis字典数据结构:
    Redis学习笔记---Redis的底层数据结构_第6张图片
  3. 字典扩容:字典达到存储上限(阈值 0.75),需要rehash(扩容)
  4. 扩容流程:
    1. 初次申请默认容量为4个dictEntry,非初次申请为当前hash表容量的一倍。
    2. rehashidx=0表示要进行rehash操作。
    3. 新增加的数据在新的hash表h[1]
    4. 修改、删除、查询在老hash表h[0]、新hash表h[1]中(rehash中)
    5. 将老的hash表h[0]的数据重新计算索引值后全部迁移到新的hash表h[1]中,这个过程称为rehash。
  5. 扩容流程图解:
    Redis学习笔记---Redis的底层数据结构_第7张图片
  6. 渐进式rehash:
  7. 当数据量巨大时rehash的过程是非常缓慢的,所以需要进行优化。
  8. 服务器忙,则只对一个节点进行rehash
  9. 服务器闲,可批量rehash(100节点)
  10. 应用场景:
    1. 主数据库的K-V数据存储
    2. 散列表对象(hash) 3、哨兵模式中的主从节点管理

7.RedisObject的7种Type—字典的底层实现hash表

  1. 散列表(hash表)组成:数组+链表
    1. 数组:有限,相同类型,有序集合,用来存储数据的容器,采用头指针+偏移量的方式能够以O(1)的时间复杂度定位到数据所在的内存地址。
    2. 链表:
    3. hash表的数组初始容量为4,随着k-v存储量的增加需要对hash表数组进行扩容,新扩容量为当前量的一倍,即4,8,16,32
    4. 索引值=Hash值&掩码值(Hash值与Hash表容量取余)
typedef struct dictht { 
	// 哈希表数组
	dictEntry **table;  
	
	// 哈希表数组的大小
	unsigned long size;  
	
	// 用于映射位置的掩码,值永远等于(size-1) 
	unsigned long sizemask; 
	
	// 哈希表已有节点的数量,包含next单链表数据 
	unsigned long used; 
	
} dictht;
  1. hash() 函数:
    1. Hash(散列),作用是把任意长度的输入通过散列算法转换成固定类型、固定长度的散列值。
    2. hash函数可以把Redis里的key:包括字符串、整数、浮点数统一转换成整数。例如:key=100.1 String “100.1” 5位长度的字符串
    3. 数组下标=hash(key)%数组容量(hash值%数组容量得到的余数)
  2. Hash冲突
    1. 不同的key经过计算后出现数组下标一致,称为Hash冲突。
    2. 采用单链表在相同的下标位置处存储原始key和value
    3. 当根据key找Value时,找到数组下标,遍历单链表可以找出key相同的value
  3. Hash表节点结构:
    1. key字段存储的是键值对中的键
    2. v字段是个联合体,存储的是键值对中的值。
    3. next指向下一个哈希表节点,用于解决hash冲突
	typedef struct dictEntry { 
		void *key; // 键 
		
		// 值v的类型可以是以下4种类型 
		union { 
			void *val; 
			uint64_t u64; 
			int64_t s64; 
			double d; 
		} v;  
		
	// 指向下一个哈希表节点,形成单向链表 解决hash冲突 
	struct dictEntry *next; 
	
	} dictEntry;
  1. Redis中字典的hash结构:dictEntry表示哈希表数组节点,dictEntry*[8],表示hhash表长为8
    Redis学习笔记---Redis的底层数据结构_第8张图片

7.RedisObject的7种Type—压缩列表

  1. 压缩列表(ziplist):是由一系列特殊编码的连续内存块组成的顺序型数据结构
  2. 是一个字节数组,可以包含多个节点(entry)。每个节点可以保存一个字节数组或一个整数。
  3. 压缩列表的数据结构如下:
    在这里插入图片描述
    1. zlbytes:压缩列表的字节长度
    2. zltail:压缩列表尾元素相对于压缩列表起始地址的偏移量
    3. zllen:压缩列表的元素个数
    4. entry1…entryX : 压缩列表的各个节点
    5. zlend:压缩列表的结尾,占一个字节,恒为0xFF(255)
  4. entryX元素的编码结构:
    在这里插入图片描述
    1. previous_entry_length:前一个元素的字节长度
    2. encoding:表示当前元素的编码
    3. content:数据内容
  5. 压缩了列表数据结构:
struct ziplist<T>{ 
	unsigned int zlbytes; // ziplist的长度字节数,包含头部、所有entry和zipend。 
	unsigned int zloffset; // 从ziplist的头指针到指向最后一个entry的偏移量,用于快速反向查询
	unsigned short int zllength; // entry元素个数 
	T[] entry; // 元素值 
	unsigned char zlend; // ziplist结束符,值固定为0xFF 
}
typedef struct zlentry { 
	unsigned int prevrawlensize; //previous_entry_length字段的长度 
	unsigned int prevrawlen; //previous_entry_length字段存储的内容 
	unsigned int lensize; //encoding字段的长度 
	unsigned int len; //数据内容长度 

	//当前元素的首部长度,即previous_entry_length字段长度与encoding字段长度之和。 
	unsigned int headersize; 
	
	unsigned char encoding; //数据类型 unsigned char 
	*p; //当前元素首地址 
	
} zlentry;

8.RedisObject的7种Type—快速列表

  1. 快速列表(quicklist)是Redis底层重要的数据结构。是列表的底层实现。(在Redis3.2之前,Redis采用双向链表(adlist)和压缩列表(ziplist)实现。)在Redis3.2以后结合adlist和ziplist的优势Redis设计出了quicklist。
  2. 快速列表底层实现是双向链表,所以列表可以lpush(左边插入)和rpush(右边插入)
  3. 双向列表(adlist):可以从两个方向进行遍历
    Redis学习笔记---Redis的底层数据结构_第9张图片
  4. 双向链表优势:
    1. 双向:链表具有前置节点和后置节点的引用,获取这两个节点时间复杂度都为O(1)。
    2. 普通链表(单链表):节点类保留下一节点的引用。链表类只保留头节点的引用,只能从头节点插入删除
    3. 无环:表头节点的 prev 指针和表尾节点的 next 指针都指向 NULL,对链表的访问都是以 NULL 结束。
      环状:头的前一个节点指向尾节点
    4. 带链表长度计数器:通过 len 属性获取链表长度的时间复杂度为 O(1)。
    5. 多态:链表节点使用 void* 指针来保存节点值,可以保存各种不同类型的值。
  5. 快速列表:quicklist是一个双向链表,链表中的每个节点时一个ziplist结构。quicklist中的每个节点ziplist都能够存储多个数据元素。
    Redis学习笔记---Redis的底层数据结构_第10张图片
  6. quicklist表头的结构定义如下:
typedef struct quicklist { 
	quicklistNode *head; // 指向quicklist的头部 
	quicklistNode *tail; // 指向quicklist的尾部 
	unsigned long count; // 列表中所有数据项的个数总和 
	unsigned int len; // quicklist节点的个数,即ziplist的个数 

	// ziplist大小限定,由list-max-ziplist-size给定 (Redis设定) 
	int fill : 16; 
	
	// 节点压缩深度设置,由list-compress-depth给定 (Redis设定) 
	unsigned int compress : 16; 
} quicklist;
  1. quicklist节点的结构定义如下:
typedef struct quicklistNode {
    struct quicklistNode *prev;     //前驱节点指针
    struct quicklistNode *next;     //后继节点指针

    //不设置压缩数据参数recompress时指向一个ziplist结构
    //设置压缩数据参数recompress指向quicklistLZF结构
    unsigned char *zl;

    //压缩列表ziplist的总长度
    unsigned int sz;                  /* ziplist size in bytes */

    //ziplist中包的节点数,占16 bits长度
    unsigned int count : 16;          /* count of items in ziplist */

    //表示是否采用了LZF压缩算法压缩quicklist节点,1表示压缩过,2表示没压缩,占2 bits长度
    unsigned int encoding : 2;        /* RAW==1 or LZF==2 */

    //表示一个quicklistNode节点是否采用ziplist结构保存数据,2表示压缩了,1表示没压缩,默认是2,占2bits长度
    unsigned int container : 2;       /* NONE==1 or ZIPLIST==2 */

    //标记quicklist节点的ziplist之前是否被解压缩过,占1bit长度
    //如果recompress为1,则等待被再次压缩
    unsigned int recompress : 1; /* was this node previous compressed? */

    //测试时使用
    unsigned int attempted_compress : 1; /* node can't compress; too small */

    //额外扩展位,占10bits长度
    unsigned int extra : 10; /* more bits to steal for future usage */
} quicklistNode;
  1. 数据压缩
    1. quicklist每个节点的实际数据存储结构为ziplist,这种结构的优势在于节省存储空间。
    2. 为了进一步降低ziplist的存储空间,还可以对ziplist进行压缩。
    3. Redis采用的压缩算法是LZF。其基本思想是:数据与前面重复的记录重复位置及长度,不重复的记录原始数据。
    4. 压缩过后的数据可以分成多个片段,每个片段有两个部分:解释字段和数据字段。quicklistLZF的结构体如下:
    typedef struct quicklistLZF { 
    	unsigned int sz; // LZF压缩后占用的字节数 
    	char compressed[]; // 柔性数组,指向数据部分 
    } quicklistLZF;
    
  2. 应用场景:列表(List)的底层实现、发布与订阅、慢查询、监视器等功能。

9.RedisObject的10种encoding

  1. encoding 表示对象的内部编码,占 4 位。

  2. Redis通过 encoding 属性为对象设置不同的编码

  3. String

    1. int:REDIS_ENCODING_INT(int类型的整数)
    2. embstr: REDIS_ENCODING_EMBSTR(编码的简单动态字符串),小字符串 长度小于44个字节(一个字节是8位)
    3. raw: REDIS_ENCODING_RAW (简单动态字符串)大字符串 长度大于44个字节
  4. list

    1. 列表的编码是quicklist:REDIS_ENCODING_QUICKLIST(快速列表)
  5. hash:散列的编码是字典和压缩列表

  6. dict:REDIS_ENCODING_HT(字典),当散列表元素的个数比较多或元素不是小整数或短字符串时。当Redis集合类型的元素是非整数或都处在64位有符号整数范围外(>18446744073709551616)

  7. ziplist:REDIS_ENCODING_ZIPLIST(压缩列表),当散列表元素的个数比较少,且元素都是小整数或短字符串时。

  8. set:集合的编码是整形集合和字典

  9. intset:REDIS_ENCODING_INTSET(整数集合),当Redis集合类型的元素都是整数并且都处在64位有符号整数范围内(<18446744073709551616)

  10. zset:有序集合的编码是压缩列表和跳跃表+字典

  11. ziplist:REDIS_ENCODING_ZIPLIST(压缩列表),当元素的个数比较少,且元素都是小整数或短字符串时。

  12. skiplist + dict:REDIS_ENCODING_SKIPLIST(跳跃表+字典),当元素的个数比较多或元素不是小整数或短字符串时

你可能感兴趣的:(Redis)