当前位置: 首页 > 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最终为空

相关文章:

  • 【读论文】面向小目标的轻型变电设备缺陷检测算法
  • Python文件操作及数据库交互(Python File Manipulation and Database Interaction)
  • cron定时任务
  • element ui el-col的高度不一致导致换行
  • 论文阅读:2024 arxiv HybridFlow: A Flexible and Efficient RLHF Framework
  • x修改ssh版本号9.9可以躲过漏洞扫描器扫描
  • 精通线程池:业务场景中的实践、优化与监控
  • Java 面向对象编程:封装及其各种用法详解
  • 工业摄像头通过USB接口实现图像
  • A. Everybody Likes Good Arrays!
  • 视频转gif在线工具-免费快捷
  • 如何创建成员内部类数组
  • 硬件工程师面试常见问题(8)
  • SQL语法基础,进阶,高级sql语句学习
  • 山东大学软件学院项目实训-基于大模型的模拟面试系统-网页图片显示问题
  • Hutool TreeUtil快速构建树形数据结构
  • 简易版2D我的世界C++程序(有点BUG,但是可以玩!!!)
  • 教育领域的AIGC革命:构建多模态智能教学系统
  • Java 安全:如何保护敏感数据?
  • pytorch python常用指令
  • 中央纪委办公厅公开通报3起整治形式主义为基层减负典型问题
  • 重新认识中国女性|婚姻,古代传统家庭再生产的根本之道
  • 泰山景区管委会:未经审核同意不得擅自举办竞速类登山活动
  • 万能险新规落地:保险期限不得低于五年,明确万能险销售“负面清单”
  • “归雁经济”能带来什么?川大商学院调研团队深入乡村与返乡青年人才交流
  • 两部门:推动“青年驿站”为毕业生跨地区求职提供住宿优惠便利