Redis-数据结构

参考资料 极客时间Redis(亚风)

Redis数据结构

SDS

sds(Simple Dynamic String) 字符串接结构体:

struct --attribute_- ((-_packed__)) sdshdr8{
	uint8_t len;/* buf已保祥的字符串字节数,不包含结束标示*/
	uint8_t alloc;/* buf申请的总的字节数,不包含结束标示*/
	unsigned char flags;/*不同SDS的头类型,⽤来控制SDS的头⼤⼩*/
	char buf [];
}

Redis SDS 扩容机制
假如我们要给SDS追加⼀段字符串,Amy,这⾥⾸先会申请新内存空间:
• 如果新字符串⼩于1M,则新空间为扩展后字符串⻓度的两倍+1;
• 如果新字符串⼤于1M,则新空间为扩展后字符串⻓度+1M+1。称为内存预分
配。
特别注意: :所以我们在生产中如果用Redis直接存储字符串最好是不要超过1M,不然可能回导致空间浪费。

IntSet

IntSet是Redis中set集合的⼀种实现⽅式,基于整数数组来实现,并且具备⻓度可变、有序等特征。
IntSet结构体

typedef struct intset {
	uint32_t encoding; /*编码⽅式,⽀持存放16位、32位、64位整数*/
	uint32_t Length;/* 元素个数 */
	int8_t contents []/*整数数组,保存集合数据*/
}

为了⽅便查找,Redis将intset中所有整数按升序依次保存在contents数组中。比如我们存储了1,2,3,4这个时候encoding 为16足以存储,如果我们插入50000这个时候有符号16位不足以存下,这个时候就会倒序扩容然后存储也就是从最后一个数据开始向后复制,最后的结果是每个数据都扩容到了32位。如果小于0插入最前面,如果大于0插入到最后面,如果是中间的数呢是通过二分法进行插入。

DICT

我们知道Redis是⼀个键值数据库,我们可以根据键实现快速的增删改查。⽽
键与值的映射关系是通过Dict来实现的。
Dict由三部分组成,分别是:DictHashTable(HashMap 类比java)、DictEntry(Map.entry)、Dict

typedef struct dictht {
	// entry数组 数组中保存的是指向entry的指针
	dictEntry **table;
	//哈希表⼤⼩
	unsigned Long size;
	//哈希表⼤⼩的掩码,总等于size - 1  h % size == h & size - 1 的前提是size是2的倍数 引出面试题hashmap 为什么扩容是2倍扩容
	unsigned Long sizemask;
	// entry个数
	unsigned Long used;
}
typedef struct dictEntry {
	void *key;//键
	union {
	    // 值可能是4中类型之一
		void *val;
		uint64_t u64;
		int64_t s64;
		double d;
	} v;//值
	//下⼀个Entry的指针
	struct dictEntry *next;
}
// 此数据结构用于扩容
typedef struct dict {
	dictType *type;//dict类型,内置不同的hash函数
	void *privdata; // 私有数据,在做特殊hash运算时⽤
	dictht ht[2]//⼀个Dict包含两个哈希表,其中⼀个是当前数据,另⼀个⼀般是空,rehash时使⽤
	Long rehashidx;
	//rehash的进度,-1表示未进⾏
	int16_t pauserehash;// rehash是否暂停,1则暂停,0则继续
}

当我们向Dict添加键值对时,Redis⾸先根据key计算出hash值 (h),然后利⽤ h& sizemask来计算元素应该存储到数组中的哪个索引位置。
Dict中的HashTable就是数组结合单向链表的实现,当集合中元素较多时,必然导致哈希冲突增多,链表过⻓,则效率会⼤⼤降低。Dict在每次新增键值对时都会检查负载因⼦(LoadFactor =used/size),used表示entry的数量也就是数据节点的数量,size是hash表的大小,满⾜以下两种情况时会触发哈希表扩容:
• 哈希表的 LoadFactor >= 1,并且服务器没有执⾏ SAVE 或REWRITEAOF 等进程
• 哈希表的 LoadFactor>5;
扩容的逻辑

static int _dictExpandI fNeeded (dict *d){
	//如果正在rehash,则返回ok
	if (dictisRehashing(d)) return DIcT.oK;
	//如果哈希表为空,则初始化哈希表为4
	if (d->ht[0].size == 0) return dictExpand (d, 4)// 当负载因⼦(used/size)达到1以上,并且当前没有进⾏rewriteaof等⼦进程操作
	//或者负载因⼦超过5,则进⾏ dictExpand,也就是扩容
	if (d->ht[0].used >= d->ht[0].size &&dict_can_resize Il d->ht [0] .used/d->ht 
	[0] .size > 5){
	//扩容⼤⼩为used +1,底层会对扩容⼤⼩做判断,实际上找的是第⼀个⼤于等于 
	used+12n
	return dictExpand(d, d->ht[0].used + 1)}
return DICT_OK;
}

Dict除了扩容以外,每次删除元素时,也会对负载因⼦做检查,当LoadFactor<0.1 时,会做哈希表收缩:

// 判断是否需要缩容 染出操作成功的时候检查
int htNeedsResize(dict *dict) {
	//哈希表⼤⼩
	size = dictSlots(dict)// entry数量
	used = dictSize(dict)// 负载因⼦低于0.1
	return (size > 4 &&(used*100/size < 10))}
// 收缩逻辑
int dictResize(dict *d){
	unsigned Long minimal;
	//如果正在做save或rewriteaof或rehash,则返回错误
	if (!dict_can_resize || dictIsRehashing (d))
	return DICT_ERR;
	// 获取used,也就是entry个数
	minimal = d->ht[0].used;
	//如果used⼩于4,则重置为4
	if (minimal < 4) minimal = 4;
	// 重置⼤⼩为minimal,其实是第⼀个⼤于等于minimal的2n
	return dictExpand(d, minimal)}

不管是扩容还是收缩,必定会创建新的哈希表,导致哈希表的size和sizemask变化,⽽key的查询与sizemask有关。因此必须对哈希表中的每⼀个key重新计算索引,插⼊新的哈希表,这个过程称为rehash。过程是这样的:
1 计算新hash表的realsize,即第⼀个⼤于等于dict.ht[0].used的2的n次方。
2 按照新的realsize申请内存空间,创建dictht,并赋值给dict.ht[1]。
3 设置rehashidx= 0,标示开始rehash。
4 将dict.ht[0]中的每⼀个dictEntry都rehash到dict.ht[1]每次执⾏新增、查询、修改、删除操作时,都检查⼀下dict.rehashidx是否⼤于-1,如果是则将ht[0].table[rehashidx]的entry链表rehash到dict.ht[1](这里的逻辑是每次将这个槽里面的数据全部搬过去,不是将整个数据(所有槽)搬过去),井且将rehashidx++。直⾄dict.ht[0]的所有数据都rehash到dict.ht[1]。搬运过程中,这条链表上的数据,有可能会搬到别的位置。满足一个原则:元素要不原位置不动,要不搬运到oldsize + index的位置去。
5 将dict.ht[1]赋值给dict.ht[0],给dict.ht[1]初始化为空哈希表。
6 将rehashidx赋值为-1,代表rehash结束。
7 在rehash过程中,新增操作,则直接写⼊ht[1],查询、修改和删除则会在dict.ht[0]和dict.ht[1]依次查找并执⾏。这样可以确保h[0]的数据只减不增,随着rehash最终为空。

ZipList

生成环境中最多存储几千的数据,数据多性能会下降
ZipList 是⼀种特殊的双端链表,由⼀系列特殊编码的连续内存块组成。可以在任意⼀端进⾏压⼊/弹出操作,并且该操作的时间复杂度为 0(1)。
在这里插入图片描述
在这里插入图片描述
ZipList 中的Entry并不像普通链表那样记录前后节点的指针,因为记录两个指针要占⽤16个字节,浪费内存。⽽是采⽤了下⾯的结构:
在这里插入图片描述
previous_entry_length:前⼀节点的字节⼤⼩,占1个或5个字节。如果前⼀节点的⻓度⼩于254字节,则采⽤1个字节来保存这个⻓度值。如果前⼀节点的⻓度⼤于254字节,则采⽤5个字节来保存这个⻓度值,第⼀个字节为0xfe,后4个字节才是真实⻓度数据。
encoding:编码属性,记录content的数据类型(字符串还是整数)以及content的字节数,占⽤1个、2个或5个字节。如果encoding是以“00”、“01〞或者“10”开头,则证明content是字符串,如果encoding是以“11〞开始,则证明content是整数,encoding占⽤1个字节。
Content:负责保存节点的数据,可以是字符串或整数

ZipList这种特殊情况下产⽣的连续多次空间扩展操作称之为连锁更新(Cascade Update)。新增、删除都可能导致连锁更新的发⽣。这种情况的发生就是因为254字节这个临界点,当前节点以后的数据都要被更新,因为大于254长度会用5个字节来存,所以不推荐存大量的数据存储

QuickList

问题1:ZipList虽然节省内存,但申请内存必须是连续空间,如果内存占⽤较多,申请内存效率很低。怎么办?
答:限制ZipList的⻓度和entry⼤⼩。
问题2:但是我们要存储⼤量数据,超出了ZipList最佳的上限该怎么办?
答:可以创建多个ZipList来分⽚存储数据。
问题3:数据拆分后⽐较分散,不⽅便管理和查找,这多个ZipList如何建⽴联系?
答:使⽤QuickList,它是⼀个双端链表,只不过链表中的每个节点都是⼀个ZipList。
QuickList结构

typedef struct quicklist{
	quicklistNode *head;
	quicklistNode *tail;
	// 所有ziplist的entry的数量
	unsigned Long count;
	// ziplists总数量
	unsigned Long Len;
	// ziplist的entry上限,默认值 -2
	int fill: -2// ⾸尾不压缩的节点数量
	unsigned int compress: 0 
}
typedef struct quicklistNode 
{
	struct quicklistNode *prev;
	struct quicklistNode *next;
	//当前节点的ZipList指针
	unsigned char *zl;
	// 当前节点的ZipList的字节⼤⼩
	unsigned int sz;
	//当前节点的ZipList的entry个数
	unsigned int count: 16//编码⽅式:1,ZipList;2,压缩模式
	unsigned int encoding: 2// 是否被解压缩。1:则说明被解压了,将来要重新压缩
	unsigned int recompress : 1 
}

为了避免QuickList中的每个ZipList中entry过多,Redis提供了⼀个配置项:listmax-ziplist-size来限制。

  • 如果值为正,则代表ZipList的允许的entry个数的最⼤值
  • 如果值为负,则代表ZipList的最⼤内存⼤⼩,分5种情况:
    ① -1:每个ZipList的内存占⽤不能超过4kb
    ② -2: 每个ZipList的内存占⽤不能超过8kb
    ③ -3:每个ZipList的内存占⽤不能超过16kb
    ④ -4:每个ZipList的内存占⽤不能超过32kb
    ⑤ -5:每个ZipList的内存占⽤不能超过64kb

除了控制zipList的⼤⼩,QuickList还可以对节点的ZipList做压缩。通过配置项list-compress-depth来控制。因为链表⼀般都是从⾸尾访问较多,所以⾸尾是不压缩的。这个参数是控制⾸尾不压缩的节点个数:
0:特殊值,代表不压缩
1:标示QuickList的⾸尾各有1个节点不压缩,中间节点压缩
2:标示QuickList的⾸尾各有2个节点不压缩,中间节点压缩
在这里插入图片描述
中间…表示被压缩的节点。

SkipList

SkipList(跳表)⾸先是链表,但与传统链表相⽐有⼏点差异:
• 元素按照升序排列存储
• 节点可能包含多个指针,指针跨度不同

typedef struct zskiplist {
	//头尾节点指针
	struct zskiplistNode *header, *tail;
	//节点数量
	unsigned Long Length;
	// 最⼤的索引层级,默认是1
	int Level;
} zskiplist;

typedef struct zskiplistNode {
	sds ele; //节点存储的值
	double score;// 节点分数,排序、查找⽤
	struct zskiplistNode *backward;// 前⼀个节点指针
	struct zskiplistLevel {
	struct zskiplistNode * forward;//下⼀个节点指针
	unsigned Long span;//索引跨度
	} level[]//多级索引数组
} zskiplistNode;

在这里插入图片描述

Redis对象

Redis 对底层的数据结构进行了进一步封装,但是其底层仍然是上面的几种数据结构实现:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
参考资料 极客时间Redis(亚风)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/250574.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

正式开通运营!“一应黔行”服务网点进驻贵阳地铁3号线

今天下午14:00,贵阳地铁3号线正式进入初期运营。为进一步实现一卡通在贵阳市全区域覆盖,不断提升“一应黔行一卡通”业务办理效率,贵阳市信捷科技有限公司在贵阳地铁3号线明珠大道站、顺海站、皂角井站3个站点设立“一应黔行”服务网点&#…

[Spring ~松耦合的设计神器]`SPI`

Java SPI(Service Provider Interface)是一种Java的服务提供者接口机制。它允许在运行时动态加载实现服务接口的类。 文章目录 基本概念最简单的实例使用 jar 包通过 spi动态实现接口功能 基本概念 SPI 机制的基本思想是,定义一个服务接口&a…

基于ssm线上学习网站论文

线上学习网站 摘要 随着信息技术在管理上越来越深入而广泛的应用,作为学校以及一些培训机构,都在用信息化战术来部署线上学习以及线上考试,可以与线下的考试有机的结合在一起,实现线上学习网站在技术上已成熟。本文介绍了线上学…

【Android开发-25】Android中多线程编程用法介绍

1,线程基本用法 在Android中,线程的使用主要有两种方法:一种是扩展java.lang.Thread类,另一种是实现Runnable接口。 1.1以下是一个简单的Android线程继承Thread的用法示例: public class MyThread extends Thread {…

maven+spock

pom配置 话说JunitMockito的组合用起来是真难用&#xff0c;还是Spock的简单&#xff0c;尤其是参数化的测试。junit的Parameter是鸡肋&#xff0c;杂恶心&#xff1b;Theories用来也不爽。 <?xml version"1.0" encoding"UTF-8"?><project xm…

SoloLinker第一次使用记录,解决新手拿到板子的无所适从

本文目录 一、简介二、进群获取资料2.1 需要下载资料2.2 SDK 包解压 三、SDK 编译3.1 依赖安装3.2 编译配置3.3 启动编译3.4 编译后的固件目录 四、固件烧录4.1 RV1106 驱动安装4.2 打开烧录工具4.3 进入boot 模式&#xff08;烧录模式&#xff09;4.4 烧录启动固件4.5 烧录升级…

Spring入门

学习的最大理由是想摆脱平庸&#xff0c;早一天就多一份人生的精彩&#xff1b;迟一天就多一天平庸的困扰。各位小伙伴&#xff0c;如果您&#xff1a; 想系统/深入学习某技术知识点… 一个人摸索学习很难坚持&#xff0c;想组团高效学习… 想写博客但无从下手&#xff0c;急需…

设计模式——组合模式(结构型)

引言 组合模式是一种结构型设计模式&#xff0c; 你可以使用它将对象组合成树状结构&#xff0c; 并且能像使用独立对象一样使用它们。 问题 如果应用的核心模型能用树状结构表示&#xff0c; 在应用中使用组合模式才有价值。 例如&#xff0c; 你有两类对象&#xff1a; ​…

Graphics Profiler 使用教程

GraphicsProfiler 使用教程 1.工具简介&#xff1a;2.Navigation介绍2.1.打开安装好的Graphics Profiler。2.2.将手机连接到计算机&#xff0c;软件会在手机中安装一个GraphicsProfiler应用(该应用是无界面的&#xff09;。2.3.Show files list2.4.Record new trace2.4.1.Appli…

java --- 集合进阶

目录 一、单列集合顶层接口 Collection 1.1 基本方法 1.2 Collection 的遍历方式 二、list集合 1.2 ArrayList Vector 底层结构 1.3 LinkedList ArrayList 和 LinkedList 比较 三、set接口 3.1、Set 接口和常用方法 3.2 HashSet HashSet 底层机制&#xff08;HashMap…

Python 直观理解基尼系数

基尼系数最开始就是衡量人群财富收入是否均衡&#xff0c;大家收入平平&#xff0c;那就是很平均&#xff0c;如果大家收入不平等&#xff0c;那基尼系数就很高。 还是给老干部们讲的言简意赅。 什么是基尼系数 我们接下来直接直观地看吧&#xff0c;程序说话 # -*- coding:…

万兆网络之屏蔽线序接法(上)

可以经常听到用RJ45指代网线&#xff0c;用RJ11指代电话线的&#xff0c;RJ&#xff08;Registered Jack&#xff09;即已注册插口&#xff0c;可以简单理解为一种约定就行&#xff08;参见参考链接&#xff09; 前一篇已经讲到&#xff0c;网线线对互相缠绕是为了电流方向相反…

CSRF(跨站脚本请求)

一、漏洞原理 CSRF&#xff08;Cross-Site Request Forgery&#xff09;是一种网络安全攻击&#xff0c;攻击者通过欺骗用户在不知情的情况下发送请求&#xff0c;从而实现对目标网站的操作。 网站管理员(已经登录网站后台)——黑客构造的恶意服务器(是网站的创建用户请求)——…

(第6天)RHEL 8 安装单机 Oracle 19C CDB 数据库

RHEL 8 安装单机 Oracle 19C 数据库(第6天) 随着 Oracle 版本的升级,硬件也在不断更新迭代,为了迎合这种趋势,Linux 系统也在不断升级,目前已经更新至 8 代版本。相信不久的将来,Linux 8 和 Oracle 19C 将成为主流版本,因此不得不讲 Linux 8 如何安装 Oracle 19C 数据…

K8s投射数据卷

目录 一.Secret 1.secret介绍 2.secret的类型 3.创建secret 4.使用secret 环境变量的形式 volume数据卷挂载 二ConfigMap 1.创建ConfigMap的方式 2.使用ConfigMap 2.1作为volume挂载使用 2.2.作为环境变量 三.Downward API 1.以环境变量的方式实现 2.Volume挂载 一.S…

C++相关闲碎记录(16)

1、正则表达式 &#xff08;1&#xff09;regex的匹配和查找接口 #include <regex> #include <iostream> using namespace std;void out (bool b) {cout << ( b ? "found" : "not found") << endl; }int main() {// find XML/H…

0/1背包问题

实验要求 随机生成500个0/1背包问题&#xff08;问题规模可以相对较小&#xff09;&#xff0c;分别使用贪心算法和动态规划进行求解&#xff0c; 要求&#xff1a;1&#xff09;统计贪心算法求得最优值的概率&#xff0c; 2&#xff09;计算比值 3&#xff09;应用贪心算法…

Postman中参数填写方式

Postman中参数填写和请求方法有关&#xff0c;一般接口用例请求方法GET与POST常用&#xff0c;所以主要是这两种请求方法请求参数填写 一、GET请求方法参数填写 1、直接在URL中填写请求参数,如直接在URL中填写&#xff1a; http://www.example.com:8089/userapi?unamelisi&…

直播原理,直播CDN及相关协议

一、直播原理 直播是一对多的完整的视频解编码原理&#xff1a; 那么直播的原理无疑也是要基于视频的解编码原理的 参考视频 二、直播CDN 什么是CDN就不多说了&#xff0c;可参考亚马逊的文章 三、相关协议 RTMP及HTTP-FLV&#xff08;都是在FLV封装格式基础上的&#xf…

串口通信(6)-C#串口通信Modbus协议完整实例

本文讲解C#基于ModbusRTU协议串口通信完整实例。 前言 关于modbus的协议从上一篇中有介绍,本篇不在阐述。 串口通信(5)-C#串口通信数据接收不完整解决方案 创建实例 添加控件和事件等 参考界面文件 namespace ModbusRTUDemo {partial class MainForm{/// <summary>…