当前位置: 首页 > news >正文

Redis数据结构SDS,IntSet,Dict

1.字符串:SDS

SDS的底层是C语言编写的构建的一种简单动态字符串 简称SDS,是redis比较常见的数据结构。

由于以下几种缺点,Redis并没有直接采用C语言的字符串。

1.获取长度需要计算

2.非二进制安全 :中间不能有 \0,否则就中断。

3.不可修改 :char * s = "hello"  

在这里Redis会在底层的创建两个SDS,一个是 “key” 的SDS ,一个是value。

由于不能直接采用C语言字符串,所以采用了一个结构体

注意:len的比特位数每一位就是单位是字节

举例:对于name字符串来说,redis底层的SDS结构体中,长度是4字节,向内存申请的字节数是4字节,flags则表示不同的编码格式

编码格式有什么作用?

核心目的是优化内存使用 和 操作效率 对于不同长度的字符串可以使用不同的头结构。

1.1.为什么叫做动态字符串

因为它具备动态扩容能力,好比一个 “hy”字符串的SDS

len:2alloc:2flags:1hy\0

如果要给它追加字符”Boy“,那么它首先会申请新的内存空间

如果新的字符串空间 < 1M,那么新空间是扩展后的2倍+1

如果新的字符串空间 > 1M,那么新空间是扩展后字符串长度 + 1M + 1

这个就是内存预分配。

len:5alloc:11flags:1hyBoy\0

优点:

1.获取字符串的长度时间复杂度为O(1)

2.支持动态扩容

3.二进制安全(中间可以有\0存在)

4.减少内存分配次数


2.IntSet

IntSet是Redis中集合的一种实现方式,基于整数数组来实现,并且具备长度可变,有序的特征。

encoding包含三种工作模式,表示储存的整数大小不同:

分别可储存2字节整数,4字节整数,8字节整数。

如果IntSet中储存三个数那么它的储存结构应该是这样的:采用默认的encoding,每个数字2字节

length大小是元素的个数,contents数组保存元素。

2.1.inset如何保存大于当前编码的最大数字?

当encoding采用int16_t 也就是2个字节大小来存放每个数字,所以每个数字不应该超过两个字节,最大是32767。当存下99999时会升级编码的方式去找到合适大小.

如图是升级编码前后的contents数组占用空间大小的情况

1.升级编码到INT_32,每个数字占4字节

2.依次将每个元素向后拷贝到正确的位置

3.将要添加的元素放入末尾

4.最后将encoding的属性改为INT_32,length的值更新。

源码:

intset *intsetAdd(intset *is, int64_t value, uint8_t *success) {uint8_t valenc = _intsetValueEncoding(value);// 获取当前值编码uint32_t pos; // 要插入的位置if (success) *success = 1;// 判断编码是不是超过了当前intset的编码if (valenc > intrev32ifbe(is->encoding)) {// 超出编码,需要升级return intsetUpgradeAndAdd(is,value);} else {// 在当前intset中查找值与value一样的元素的角标posif (intsetSearch(is,value,&pos)) {if (success) *success = 0; //如果找到了,则无需插入,直接结束并返回失败return is;}// 数组扩容is = intsetResize(is,intrev32ifbe(is->length)+1);// 移动数组中pos之后的元素到pos+1,给新元素腾出空间if (pos < intrev32ifbe(is->length)) intsetMoveTail(is,pos,pos+1);}// 插入新元素_intsetSet(is,pos,value);// 重置元素长度is->length = intrev32ifbe(intrev32ifbe(is->length)+1);return is;
}
static intset* intsetUpgradeAndAdd(intset* is, int64_t value) {// 获取当前intset编码uint8_t curenc = intrev32ifbe(is->encoding);// 获取新编码uint8_t newenc = _intsetValueEncoding(value);// 获取元素个数int length = intrev32ifbe(is->length);// 判断新元素是大于0还是小于0 ,小于0插入队首、大于0插入队尾int prepend = value < 0 ? 1 : 0;// 重置编码为新编码is->encoding = intrev32ifbe(newenc);// 重置数组大小is = intsetResize(is, intrev32ifbe(is->length) + 1);// 倒序遍历,逐个搬运元素到新的位置,_intsetGetEncoded按照旧编码方式查找旧元素while (length--) // _intsetSet按照新编码方式插入新元素_intsetSet(is, length + prepend, _intsetGetEncoded(is, length, curenc));/* 插入新元素,prepend决定是队首还是队尾*/if (prepend)_intsetSet(is, 0, value);else_intsetSet(is, intrev32ifbe(is->length), value);// 修改数组长度is->length = intrev32ifbe(intrev32ifbe(is->length) + 1);return is;
}

总结:

1.InSet确保元素唯一,有序

2.具备类型升级,可以节省空间

3.底层采用二分查找来查询元素

4.如果数组过长,扩容时移动元素效率不高


3.Dict

Redis是一种键-值型的数据库,那么键和值间关系的维护就要靠Dict维护。

Dict是由三部分组成,分别是哈希表(DictHashTable) ,哈希节点(DictEntry),字典(Dict)

当向Dict添加键值对时,Redis首先根据key计算哈希值(h),然后利用 h & sizemask(图上掩码)计算出元素应放索引的位置,假设哈希值 h = 1, 则 1 & 3 = 1, 因此储存到角标1位

一个Dict包含两个hash表,其中一个一般是空,rehash才使用,比如负载因子超过阈值,扩展,收缩,初始化时使用。

3.1Dict的扩容

DIct中的HashTable就是数组结合单项链表实现,当集合中的元素过多必然导致hash冲突,同时链表过长hash的查询效率也会越来越低。

每次新增键值都会查询负载因子(LoadFactor = used / size)

1.如果LoadFactor >= 1 同时服务器没有执行BGSAVE/BGREWRITEAOF等后台进程。

2.哈希表的LoadFactor > 5,此时就算后台执行进程也会强制扩容。

扩容的大小为used + 1,以下是扩容操源码

static int _dictExpandIfNeeded(dict *d){// 如果正在rehash,则返回okif (dictIsRehashing(d)) return DICT_OK;    // 如果哈希表为空,则初始化哈希表为默认大小:4if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE);// 当负载因子(used/size)达到1以上,并且当前没有进行bgrewrite等子进程操作// 或者负载因子超过5,则进行 dictExpand ,也就是扩容if (d->ht[0].used >= d->ht[0].size &&(dict_can_resize || d->ht[0].used/d->ht[0].size > dict_force_resize_ratio){// 扩容大小为used + 1,底层会对扩容大小做判断,实际上找的是第一个大于等于 used+1 的 2^nreturn dictExpand(d, d->ht[0].used + 1);}return DICT_OK;
}

首先判断是否正在rehash 是就返回ok,否则向下执行。如果哈希表是空的就会初始化4个单位,

如果负载因子 >= 1 ,并且没有进行后台进程 或者 负载因子>= 5. 开始进行扩容。

3.2Dict的收缩

DIct除扩容之外还可以 收缩,每次执行删除后如果 负载因子 < 0.1 就会执行。

3.3.rehash

不管是扩容还是收缩,每次变化都会导致size 和 sizemask(掩码)变化,而key的查询与sizemask有关。所以必须对哈希表中每一个key重新计算索引,插入新的哈希表,这个过程就是rehash。

1.重新计算hash表的realeSize,值取决于当前要做的是扩容还是收缩

扩容:则新的size是第一个 >= dict.ht[0].used + 1 的 2^n

删除:则新的size是第一个 >= dict.ht[0].used 的 2^n (不小于 4)

2.按照新的realeSize申请内存空间,创建dictht,并赋值给dict.ht[1]

3.设置dict.rehashidx = 0,标识开始rehash

4.将dict.ht[0]的每个dictEntry都rehash到dict.h1

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

rehash不是一次能够完成的,如果数据百万entry则需要分批次完成,否则可能导致线程阻塞

所以叫渐进式rehash

1.重新计算hash表的realeSize,值取决于当前要做的是扩容还是收缩

扩容:则新的size是第一个 >= dict.ht[0].used + 1 的 2^n

删除:则新的size是第一个 >= dict.ht[0].used 的 2^n (不小于 4)

2.按照新的realeSize申请内存空间,创建dictht,并赋值给dict.ht[1]

3.设置dict.rehashidx = 0,标识开始rehash

4.将dict.ht[0]的每个dictEntry都rehash到dict.h1

5.每次增删查改都检查dict.rehashidx是否 > -1,如果是则将dict.ht[0].table[rehashidx]的entry链表rehash到dict.ht[1],并且将rehashidx++,直到所有数据都rehsh到dict.ht[1]

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

7.rehashidx赋值-1,代表rehash结束

8.在rehash过程中,新增操作,则直接写入ht[1],查询、修改和删除则会在dict.ht[0]和dict.ht[1]依次查找并执行。这样可以确保ht[0]的数据只减不增,随着rehash最终为空

http://www.xdnf.cn/news/164359.html

相关文章:

  • Java—— 五道算法水题
  • 强化学习基础
  • Python AI图像生成方案指南
  • Axure疑难杂症:全局变量典型应用及思考逻辑(玩转全局变量)
  • 剑指offer经典题目(六)
  • 做的一些题目的答案和自己的一些思考
  • LangChain 中的 Task(任务) 主要通过 生成器(Generator) 实现,而非传统的迭代器(Iterator)
  • Ardunio学习
  • 推论阶梯——AI与思维模型【81】
  • Redis 数据分片三大方案深度解析与 Java 实战
  • JavaScript原生实现简单虚拟列表(列表不定高)
  • 【Agent python实战】ReAct 与 Plan-and-Execute 的融合之道_基于DeepSeek api
  • 快速上手c语言
  • 栈与堆的演示
  • C++ 为什么建议类模板定义在头文件中,而不定义在源文件中
  • 对卡尔曼滤波的理解和简单示例实现
  • 数据库原理(1)
  • N字形上升形态选股代码如何编写?
  • 平面连杆机构(上)
  • 定制一款国密浏览器(11):SM2算法的椭圆曲线参数定义
  • 4月25日日记(补)
  • 6.Geometric Intersection (几何求交)- Preliminary
  • 用高德API提取广州地铁线路(shp、excel)
  • Docker Compose--在Ubuntu中安装Docker compose
  • Java 异常处理全解析:从基础到自定义异常的实战指南
  • Java社区门诊系统源码 SaaS医院门诊系统源码 可上线运营
  • 深入理解JavaScript异步编程:从回调地狱到Promise/Async优雅解决方案
  • Eigen核心矩阵/向量类 (Matrix, Vector, Array)
  • 循环神经网络RNN---LSTM
  • 函数递归之青蛙跳台阶+汉诺塔