手撕Redis源码1-数据结构实现

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存储不同大小的头部和数据存储能力。

1.2 SDS动态扩容

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

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

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

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

2.Inset结构

2.1 InSet结构实现

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

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

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

2.2 Inset升级流程

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

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

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


升级过程源码:

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

3.Dict数据结构

3.1 Dict结构实现

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

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

3.2 Dict扩容机制

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

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

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

load>5;

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

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]的内存

扩容操作:

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]。

相关推荐
雨中飘荡的记忆2 小时前
大流量下库存扣减的数据库瓶颈:Redis分片缓存解决方案
java·redis·后端
NineData2 小时前
数据库迁移总踩坑?用 NineData 迁移评估,提前识别所有兼容性风险
数据库·程序员·云计算
赵渝强老师5 小时前
【赵渝强老师】PostgreSQL中表的碎片
数据库·postgresql
全栈老石9 小时前
拆解低代码引擎核心:元数据驱动的"万能表"架构
数据库·低代码
曲幽10 小时前
FastAPI分布式系统实战:拆解分布式系统中常见问题及解决方案
redis·python·fastapi·web·httpx·lock·asyncio
倔强的石头_1 天前
kingbase备份与恢复实战(二)—— sys_dump库级逻辑备份与恢复(Windows详细步骤)
数据库
jiayou642 天前
KingbaseES 实战:深度解析数据库对象访问权限管理
数据库
李广坤3 天前
MySQL 大表字段变更实践(改名 + 改类型 + 改长度)
数据库
爱可生开源社区4 天前
2026 年,优秀的 DBA 需要具备哪些素质?
数据库·人工智能·dba
随逸1774 天前
《从零搭建NestJS项目》
数据库·typescript