您现在的位置是:网站首页> 编程资料编程资料

Redis底层数据结构详解_Redis_

2023-05-27 467人已围观

简介 Redis底层数据结构详解_Redis_

Redis作为Key-Value存储系统,数据结构如下:

在这里插入图片描述

Redis没有表的概念,Redis实例所对应的db以编号区分,db本身就是key的命名空间。

比如:user:1000作为key值,表示在user这个命名空间下id为1000的元素,类似于user表的id=1000的行。

RedisDB结构

Redis中存在“数据库”的概念,该结构由redis.h中的redisDb定义。

当redis 服务器初始化时,会预先分配 16 个数据库

所有数据库保存到结构 redisServer 的一个成员 redisServer.db 数组中

redisClient中存在一个名叫db的指针指向当前使用的数据库

RedisDB结构体源码:

 /* Redis database representation. There are multiple databases identified * by integers from 0 (the default database) up to the max configured * database. The database number is the 'id' field in the structure. */ typedef struct redisDb { dict *dict; /* 存储数据库所有的key-value */ dict *expires; /* 存储key的过期时间 */ dict *blocking_keys; /* blpop 存储阻塞key和客户端对象*/ dict *ready_keys; /* 阻塞后push 响应阻塞客户端 存储阻塞后push的key和客户端对象 */ dict *watched_keys; /* 存储watch监控的的key和客户端对象 */ int id; /* Database ID */ long long avg_ttl; /* 存储的数据库对象的平均ttl(time to live),用于统计 */ unsigned long expires_cursor; /* 循环过期检查的光标. */ list *defrag_later; /* 需要尝试去清理磁盘碎片的链表,会慢慢的清理 */ } redisDb;

id
id是数据库序号,为0-15(默认Redis有16个数据库)

dict
存储数据库所有的key-value,后面要详细讲解

expires
存储key的过期时间,后面要详细讲解

RedisObject结构

Value是一个对象
包含字符串对象,列表对象,哈希对象,集合对象和有序集合对象

结构信息概览

 typedef struct redisObject { unsigned type:4; //类型 对象类型 unsigned encoding:4;//编码 // LRU_BITS为24bit 记录最后一次被命令程序访问的时间 unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or * LFU data (least significant 8 bits frequency * and most significant 16 bits access time). */ int refcount; //引用计数 void *ptr;//指向底层实现数据结构的指针 } robj;

4位type

type 字段表示对象的类型,占 4 位;

REDIS_STRING(字符串)、REDIS_LIST (列表)、REDIS_HASH(哈希)、REDIS_SET(集合)、REDIS_ZSET(有序集合)。

当我们执行 type 命令时,便是通过读取 RedisObject 的 type 字段获得对象的类型

 127.0.0.1:6379> set a1 111 OK 127.0.0.1:6379> type a1 string

4位encoding

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

每个对象有不同的实现编码

Redis 可以根据不同的使用场景来为对象设置不同的编码,大大提高了 Redis 的灵活性和效率。

通过 object encoding 命令,可以查看对象采用的编码方式

 127.0.0.1:6379> OBJECT encoding a1 "int"

24位LRU
lru 记录的是对象最后一次被命令程序访问的时间,( 4.0 版本占 24 位,2.6 版本占 22 位)。

高16位存储一个分钟数级别的时间戳,低8位存储访问计数(lfu : 最近访问次数)

   lru----> 高16位: 最后被访问的时间

   lfu----->低8位:最近访问次数

refcount
refcount 记录的是该对象被引用的次数,类型为整型。

refcount 的作用,主要在于对象的引用计数和内存回收。

当对象的refcount>1时,称为共享对象

Redis 为了节省内存,当有一些对象重复出现时,新的程序不会创建新的对象,而是仍然使用原来的对象。

ptr
ptr 指针指向具体的数据,比如:set hello world,ptr 指向包含字符串 world 的 SDS。

7种type 字符串对象

C语言: 字符数组 “\0”

Redis 使用了 SDS(Simple Dynamic String)。用于存储字符串和整型数据。

在这里插入图片描述

 /* Note: sdshdr5 is never used, we just access the flags byte directly. * However is here to document the layout of type 5 SDS strings. */ struct __attribute__ ((__packed__)) sdshdr5 { unsigned char flags; /* 3 lsb of type, and 5 msb of string length */ char buf[]; }; struct __attribute__ ((__packed__)) sdshdr8 { uint8_t len; /* used */ uint8_t alloc; /* excluding the header and null terminator */ unsigned char flags; /* 3 lsb of type, 5 unused bits */ char buf[]; }; struct __attribute__ ((__packed__)) sdshdr16 { uint16_t len; /* used */ uint16_t alloc; /* excluding the header and null terminator */ unsigned char flags; /* 3 lsb of type, 5 unused bits */ char buf[]; }; struct __attribute__ ((__packed__)) sdshdr32 { uint32_t len; /* used */ uint32_t alloc; /* excluding the header and null terminator */ unsigned char flags; /* 3 lsb of type, 5 unused bits */ char buf[]; }; struct __attribute__ ((__packed__)) sdshdr64 { uint64_t len; /* used */ uint64_t alloc; /* excluding the header and null terminator */ unsigned char flags; /* 3 lsb of type, 5 unused bits */ char buf[]; };

buf[] 的长度=len+free+1

SDS的优势:

1.SDS 在 C 字符串的基础上加入了 free 和 len 字段,获取字符串长度:SDS 是 O(1),C 字符串是
O(n)。
buf数组的长度=free+len+1

2.SDS 由于记录了长度,在可能造成缓冲区溢出时会自动重新分配内存,杜绝了缓冲区溢出。

3.可以存取二进制数据,以字符串长度len来作为结束标识

  • C:

\0 空字符串 二进制数据包括空字符串,所以没有办法存取二进制数据

  • SDS :

非二进制 \0
二进制: 字符串长度 可以存二进制数据

使用场景:
SDS的主要应用在:存储字符串和整型数据、存储key、AOF缓冲区和用户输入缓冲。

跳跃表(重要)

跳跃表是有序集合(sorted-set)的底层实现,效率高,实现简单。

跳跃表的基本思想:

将有序链表中的部分节点分层,每一层都是一个有序链表。

查找

在查找时优先从最高层开始向后查找,当到达某个节点时,如果next节点值大于要查找的值或next指针指向null,则从当前节点下降一层继续向后查找。

举例:
在这里插入图片描述
查找元素9,按道理我们需要从头结点开始遍历,一共遍历8个结点才能找到元素9。

第一次分层:

遍历5次找到元素9(红色的线为查找路径)
在这里插入图片描述
第二次分层:

遍历4次找到元素9

在这里插入图片描述
第三层分层:

遍历4次找到元素9
在这里插入图片描述
这种数据结构,就是跳跃表,它具有二分查找的功能。

插入与删除

上面例子中,9个结点,一共4层,是理想的跳跃表。

通过抛硬币(概率1/2)的方式来决定新插入结点跨越的层数,每层都需要判断:

正面:插入上层

背面:不插入

达到1/2概率(计算次数)

删除

找到指定元素并删除每层的该元素即可

跳跃表特点:

每层都是一个有序链表

查找次数近似于层数(1/2)

底层包含所有元素

空间复杂度 O(n) 扩充了一倍

Redis跳跃表的实现

 /* ZSETs use a specialized version of Skiplists */ typedef struct zskiplistNode { /* 存储字符串类型数据 redis3.0版本中使用robj类型表示,但是在redis4.0.1中直接使用sds类型表示 */ sds ele; /*存储排序的分值*/ double score; /*后退指针,指向当前节点最底层的前一个节点*/ struct zskiplistNode *backward; /*层,柔性数组,随机生成1-64的值*/ struct zskidictEntryplistLevel { struct zskiplistNode *forward; //指向本层下一个节点 unsigned long span; //本层下个节点到本节点的元素个数 } level[]; } zskiplistNode; typedef struct zskiplist { //表头节点和表尾节点 struct zskiplistNode *header, *tail; //表中节点的数量 unsigned long length; //表中层数最大的节点的层数 int level; } zskiplist;

完整的跳跃表结构体:

在这里插入图片描述

跳跃表的优势:
1、可以快速查找到需要的节点 O(logn)
2、可以在O(1)的时间复杂度下,快速获得跳跃表的头节点、尾结点、长度和高度。
应用场景:有序集合的实现

字典(重要)

字典dict又称散列表(hash),是用来存储键值对的一种数据结构。
Redis整个数据库是用字典来存储的。(K-V结构)
对Redis进行CURD操作其实就是对字典中的数据进行CURD操作。

数组

数组:用来存储数据的容器,采用头指针+偏移量的方式能够以O(1)的时间复杂度定位到数据所在的内存地址。

Redis 海量存储 快

Hash函数

Hash(散列),作用是把任意长度的输入通过散列算法转换成固定类型、固定长度的散列值。

hash函数可以把Redis里的key:包括字符串、整数、浮点数统一转换成整数。

key=100.1 String “100.1” 5位长度的字符串

Redis-cli :times 33

Redis-Server : MurmurHash

数组下标=hash(key)%数组容量(hash值%数组容量得到的余数)

Hash冲突

不同的key经过计算后出现数组下标一致,称为Hash冲突。

采用单链表在相同的下标位置处存储原始key和value

当根据key找Value时,找到数组下标,遍历单链表可以找出key相同的value
在这里插入图片描述

Redis字典的实现

Redis字典实现包括:字典(dict)、Hash表(dictht)、Hash表节点(dictEntry)。
在这里插入图片描述

Hash表

 typedef struct dictht { dictEntry **table; // 哈希表数组 unsigned long size; // 哈希表数组的大小 unsigned long sizemask; // 用于映射位置的掩码,值永远等于(size-1) unsigned long used; // 哈希表已有节点的数量,包含next单链表数据 } dictht;

1、hash表的数组初始容量为4,随着k-v存储量的增加需要对hash表数组进行扩容,新扩容量为当前量的一倍,即4,8,16,32

2、索引值=Hash值&掩码值(Hash值与Hash表容量取余)

Hash表节点

 typedef struct dictEntry { void *key; // 键 union { // 值v的类型可以是以下4种类型 void *val; uint64_t u64; int64_t s64; double d; } v; struct dictEntry *next; // 指向下一个哈希表节点,形成单向链表 解决hash冲突, 单链表中会存储key和value } dictEntry;

key字段存储的是键值对中的键

v字段是个联合体,存储的是键值对中的值。

next指向下一个哈希表节点,用于解决hash冲突

在这里插入图片描述

dict字典

 typedef struct dict { dictType *type; //该字典对应的特定操作函数 void *privdata; //上述类型函数对应的可选参数 dictht ht[2];/* 两张哈希表,存储键值对数据,ht[0]为原生哈希表,ht[1]为 rehash 哈希表 */ long rehashidx; /* rehash标识 当等于-1时表示没有在rehash,否则表示正在进行rehash操作, 存储的值表示hash表 ht[0]的rehash进行到哪个索引值(数组下标)*/ unsigned long iterators; /* 当前运行的迭代器数量 */ } dict;

type字段,指向dictType结构体,里边包括了对该字典操作的函数指针

 typedef struct dictType { // 计算哈希值的函数 uint64_t (*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;

  Redis字典除了主数据库的K-V数据存储以外,还可以用于:散列表对象、哨兵模式中的主从节点管理等在不同的应用中,字典的形态都可能不同,dictType是为了实现各种形态的字典而抽象出来的操作函数(多态)。

完整的Redis字典数据结构:

在这里插入图片描述

字典扩容

字典达到存储上限(阈值 0.75),需要rehash(扩容)

在这里插入图片描述

说明:

初次申请默认容量为4个dictEntry,非初次申请为当前hash表容量的一倍。rehashidx=0表示要进行rehash操作。新增加的数据在新的hash表h[1]修改、删除、查询在老hash表h[0]、新hash表h[1]中(rehash中)将老的hash表h[0]的数据重新计算索引值后全部迁移到新的hash表h[1]中,这个过程称为rehash。 渐进式rehash

当数据量巨大时rehash的过程是非常缓慢的,所以

-六神源码网