手撕Redis底层1-数据结构源码剖析

简介: Redis核心数据结构解析

 1.SDS动态字符串:

Redis中key保存的是字符串,value也往往是字符串或者字符串的集合,不过,Redis并未直接使用C语言中的字符串,因为C语言中的字符串存在一些问题,比如获取字符串长度需要通过运算且字符串不可修改,字符串是非二进制安全的,由此,Redis基于C语言构建了简单动态字符串SDS结构。

1.1 SDS结构实现

SDS是基于C语言中的结构体实现的,每个SDS有四个属性,buf[]为数据存储数组,len为buf中已保存的数据字节数(不包含结束标识),alloc表示buf申请的总字节数(包含结束标识),flags表示不同SDS类型,Redis中定义了多种类型的SDS,不同类型的SDS存储不同大小的头部和数据存储能力。

image.gif 编辑

image.gif 编辑

1.2 SDS动态扩容

SDS具备动态扩容的能力,例如,我们将一个内容为“hi”的SDS追加一段字符串",Amy",首先会申请新内存空间,进行内存预分配操作:

如果新字符串小于1M,则新空间为扩展后字符串长度的两倍+1

如果新字符串大于1M,则新空间为扩展后字符串长度+1M+1

image.gif 编辑

image.gif 编辑

基于SDS,我们实现了在O(1)时间复杂度获取字符串长度,实现了字符串的动态扩容,减少了内存分配次数,以及实现了二进制安全。

2.Inset结构

2.1 InSet结构实现

inset结构是Redis的Set集合的一种实现方式(当数据量较少并且只包含整数元素时Set结构采用Inset),也是基于结构体实现,具备长度可变,有序等特征,每个结构体有三个属性:content[]为整数数组,保存集合数据;length表示元素个数;encoding表示编码方式(即content数组中存储的具体数据类型),encoding有三种模式。

下图中的int8_t contents[] 是 C 语言里的柔性数组成员,它本身不占用结构体固定内存,只是一个 “占位符”。编译时,结构体实际大小不包含这个数组,运行时会动态分配内存。

image.gif 编辑

image.gif 编辑为了提高查找效率,Redis会将数据按升序依次放到content数组中,如图所示:encodeing占4字节,length占4字节,contents占2*3=6字节

image.gif 编辑

2.2 Inset升级流程 image.gif 编辑

此时,我们向inset中添加一个数字:50000,这个数字超出了2字节的存储范围,因此,inset会自动升级为合适的编码方式。

首先,encoding会升级为INTSET_ENC_INT32,每个整数占4字节,并按照新的编码方式扩容数组,扩容过程中倒序依次将数组中的元素拷贝到扩容后的位置:

image.gif 编辑

image.gif 编辑

image.gif 编辑

之后,将待添加的元素加入到数组末尾,最后,inset的encoding属性改为INTSET_ENC_INT32,length属性改为4

image.gif 编辑

升级过程源码:

image.gif 编辑

image.gif 编辑

Inset可以看作特殊类型的整数数组,会确保元素的唯一性和有序性,具备类型升级机制,可以节省内存空间,并且底层采用二分查找的方式来查找元素

3.Dict数据结构

3.1 Dict结构实现

Redis是一个键值型数据库,可以通过键对数据进行CRUD操作,而键与值的映射关系通过Dict实现,Dict由三部分构成:哈希表(DictHashTable),哈希节点(DictEntry),字典(Dict)

image.gif 编辑

当向Dict添加键值对时,Redis会首先根据Key计算出hash值,然后利用h & sizemask计算元素应存储到数组中哪个索引位置。

image.gif 编辑

image.gif 编辑

3.2 Dict扩容机制

Dict中的HashTable使用的是数组加链表的实现,当集合中元素较多时,必然导致哈希冲突增多,链表过长,查询效率大大降低。

Dict在每次新增键值对时会监测负载因子(Load=userd/size),即哈希表中使用的节点个数除以哈希表大小,如果负载因子满足以下两种情况,会触发哈希表扩容机制:

load>=1并且服务器没有执行 BGSAVE 或者 BGREWRITEAOF 等后台进程;

load>5;

同样,每次删除元素时,都会对负载因子做检查,当load<0.1时,会做收缩操作。

image.gif 编辑

3.3 Rehash操作

不管是扩容还是收缩,必然会创建新哈希表,导致哈希表的size和sizemask变化,而key查询又依赖于sizemask,因此,必须对哈希表中每一个key重新计算索引,插入到新的哈希表,此过程称为rehash,过程如下:

计算新哈希表的realSize,值取决于当前要做扩容还是收缩操作:

如果是扩容,则新size为第一个大于等于dict.ht[0].used + 1的2^n;

如果是收缩,则新size为第一个大于等于dict.ht[0].used的2^n (不得小于4);

根据新的realSize创建申请新的内存空间,创建dicthet,并赋值给dict.ht[1]

设置rehashidx=0,表示开始进行rehash

将dict.ht[0]中的每一个dictEntry都rehash到dict.ht[1]

将dict.ht[1]赋值给dict.ht[0],给dict.ht[1]初始化为空哈希表,释放原来dict.ht[0]的内存

image.gif 编辑

image.gif 编辑

image.gif 编辑

扩容操作:

image.gif 编辑

image.gif 编辑 image.gif 编辑

image.gif 编辑

Dict的rehash并不是一次性完成的,如果数据量非常大,要在一次rehash完成可能会导致主线程阻塞,因此rehash是渐进式完成的,每次执行增删改查操作时,都会检查dict.rehashidx是否大于-1,如果是,则将dict.ht[0].table[rehashidx]的entry链表rehash到dict.ht[1],并且将rehashidx++,直到dict.ht[0]所有数据都rehash到dict.ht[1],rehash完成,将rehashidx赋值为-1。相当于每次只rehash一个节点的链表,进行删改查操作时,两个哈希表都去找,找到数据后进行操作。进行新增操作时,直接写入ht[1]。

4. ZipList

ZipList是一种特殊的双端链表,由一系列特殊编码的内存块构成,可以在任意一端进行压入或弹出操作,并且操作的时间复杂度为O(1)。

image.gif 编辑

image.gif 编辑


相关文章
|
5月前
|
运维 负载均衡 Java
微服务基础1-微服务拆分与服务调用
文章摘要: 本文详细介绍了微服务架构的概念、优势及实现方式。对比单体架构的局限性,微服务通过拆分功能模块实现高内聚、低耦合,提升系统可用性和开发效率。重点讲解了微服务拆分策略(纵向按功能、横向抽通用)、服务注册与发现机制(基于Nacos),以及远程调用方案(RestTemplate和OpenFeign)。OpenFeign通过动态代理简化RPC调用,支持连接池和日志配置,使远程调用如同本地方法。文章还探讨了微服务拆分时机(初创期验证后或大型项目初期直接采用),为不同规模团队提供架构演进建议。
|
5月前
|
消息中间件 存储 Kafka
详解Kafka1-基础使用
Kafka消息队列技术指南 本文系统介绍了Kafka消息队列的核心概念与应用实践。主要内容包括: 消息队列基础 两种模型:生产者-消费者模型和发布-订阅模型 应用场景:异步处理、系统解耦、流量削峰等 Kafka核心架构 重要组件:Broker、Zookeeper、Topic、Partition等 消费者组机制实现负载均衡 消息存储与分区策略 实践操作 集群搭建与环境配置 基准测试方法与性能指标 Java API编程示例(生产者/消费者) 事务编程实现原子操作 高级特性 生产者幂等性原理与配置 事务API与隔
|
5月前
|
人工智能 监控 Linux
OpenClaw(小龙虾)进阶完全指南:17大高手技巧+阿里云/本地部署+大模型配置完整版
OpenClaw(小龙虾)作为轻量化开源AI Agent,已经成为本地部署、任务执行、多平台接入的主流框架。但绝大多数用户只停留在“安装启动、简单对话”的初级阶段,完全没有发挥其长期记忆、技能工程化、多Agent协作、稳定值守、人格管理等真正实力。
1555 0
|
并行计算 Linux
Linux内核中的线程和进程实现详解
了解进程和线程如何工作,可以帮助我们更好地编写程序,充分利用多核CPU,实现并行计算,提高系统的响应速度和计算效能。记住,适当平衡进程和线程的使用,既要拥有独立空间的'兄弟',也需要在'家庭'中分享和并行的成员。对于这个世界,现在,你应该有一个全新的认识。
501 67
|
10月前
|
人工智能 Unix API
50_选择模型:开源vs闭源
在大型语言模型(LLM)技术快速发展的今天,企业和开发者面临着一个关键决策:是选择开源LLM模型还是闭源LLM服务?这个选择直接影响到项目的成本结构、开发灵活性、数据安全性以及长期战略规划。随着2025年LLM技术的进一步成熟,开源与闭源模型之间的竞争格局也发生了显著变化。
1098 0
|
11月前
|
算法 数据安全/隐私保护 异构计算
基于FPGA的8PSK+帧同步系统verilog开发,包含testbench,高斯信道,误码统计,可设置SNR
本项目基于FPGA实现8PSK调制解调系统,包含仿真验证与Verilog核心代码。通过Vivado 2019.2进行功能仿真,验证在不同信噪比(SNR=10dB/30dB)下的通信性能。系统包括调制、信道传输、解调及帧同步等模块,适用于数字通信应用。
241 0
|
存储 Web App开发 Ubuntu
手机变PC:给安卓手机刷Ubuntu Touch系统
至此,魅族PRO 5刷入Ubuntu Touch即完成了。 魅族PRO 5的Ubuntu Touch的开机画面如下:
2581 0
|
存储 消息中间件 NoSQL
Redis从入门到精通之底层数据类型Stream详解和使用示例
Redis Stream的底层数据结构还涉及到基数树和listpacks,这些结构使得Redis Stream具有高效的空间和时间复杂度,同时允许通过ID进行随机访问。 基数树是一种高效的数据结构,用于存储有序数据集合。在Redis Stream中,消息ID有序集合就是基于基数树实现的。基数树可以快速定位一个元素在有序集合中的位置,以及在有序集合中查找一段范围内的元素。 listpack是一种紧凑的、可扩展的、有序的、二进制数据结构,用于存储多个元素。在Redis Stream中,消息哈希表就是基于listpack实现的。listpack可以高效地存储多个元素,同时支持快速的元素访问插入。
2405 120
Redis从入门到精通之底层数据类型Stream详解和使用示例
|
存储 NoSQL Redis
Redis常见面试题:ZSet底层数据结构,SDS、压缩列表ZipList、跳表SkipList
String类型底层数据结构,List类型全面解析,ZSet底层数据结构;简单动态字符串SDS、压缩列表ZipList、哈希表、跳表SkipList、整数数组IntSet
一文搞懂正则表达式之零宽断言
零宽断言: 用于查找在某些内容之前或之后的东西,也就是说它们像\b,^,$那样用于指定一个位置,这个位置应该满足一定的条件(即断言),因此它们也被称为零宽断言。
1025 0
一文搞懂正则表达式之零宽断言