reids设计与实现(一)——数据结构

1. 前言

reids作为最常用的缓存数据库,深入了解,对于业务开发大有裨益,那么从这里开始,我们从《redis设计与实现》这本书,我们同最常用的字符串入手,了解redis的设计与思路。

2. redis 动态字符串

字符串作为redis最为核心,最为常用的数据类型,后面我们称sds,我们深入了解一下。

2.1. 字符串的数据结构:

在这里插入图片描述我们从数据结构入手,猜测字符串实现的功能和特性,我们可以发现,这里相比于c字符串多了两个属性。freelen

2.2. 剖析,length;

  1. 快速获取length :首先,leng最简单的效果便是可以直接获取redis字符串的长短,由于是直接获取属性,时间复杂度为O(1)。

  2. 二进制安全: 除了获取长度外,为了实现redis的sds可以存储任意数据的功能,sds通过length判断字符串是否到结尾,这和c字符串不同(‘/0’),因此可以存储任意二进制数据。

2.3. 剖析,free;

其实简单length之后发现基本功能都差不多了,那么这个free有什么作用呢?

  1. 预留free空间: 减少重新分配,在sds除了记录length之外,还会分配一倍length(1mb大小以内)的未使用空间,如果length在再次增加的情况下,不过增加的长度小于free,则不需要重新分配内存。

2.3. 使用c字符串函数;

redis虽然自行实现了字符串数据机构,但是还是在字符串末尾增加一个’/0’空字符,目的是为了使用c字符串的函数。

3. redis 链表

redis链表并没有非常奇特的地方,在redis链表中主要有两个数据结构。

  1. list 统计
    在这里插入图片描述
    这里包括list的一些概要信息和一些函数,目的是为了后面使用链表节点方便一点。
  2. listnode 节点。
    在这里插入图片描述

一般来说这个在学习数据结构中用的很多,一般情况下,只需要记录一个pre node便可以遍历整个链表。

redis链表为双向链表。并没有过于多的特殊。

4. 字典

字典又称为符号表,map,key-value。

  1. dictht(hash)表,属性包括,hash表数组,表大小,hash表大小掩码,已有节点数量。
    在这里插入图片描述

  2. dictEntry表数据,是key-value结构
    在这里插入图片描述

redis对于hash冲突的解决方案是链地址法,即如果冲突,在原来dictEntrt下面通过next链接冲突节点。

  1. 字典,hash的上层结构,和java中的hashMap功能类似。
    在这里插入图片描述
    dictType 可以指定不同低操作函数。
    ht 为两个hash表,另外一个用于备份。(在再hash时使用)

  2. hash算法: hash算法和普通的hash表别无二致,通话hash算法,hash(key)& mask把数组放到表里。

  3. hash冲突
    redis使用链地址发把键值对存储在链表之前解决冲突,这样的时间复杂度为O(1)。

  4. rehash
    当hash表空间不够的时候,一般需要再次hash,
    渐进式再hash,在渐进式 rehash 过程中,Redis 会同时保持旧的哈希表和新的哈希表。然后,在每次执行命令时,Redis 会从旧哈希表中移动一小批键值对到新哈希表,这个过程分散在多个操作中逐步完成。

具体条件为

  1. 当负载因子大于1且没在持久化(BGSAVE,BGREWRITEAOF)会进行再hash。
  2. 当负载因子大于3,目前在执行BGSAVE,BGREWRITEAOF)时,会进行再hash。
  3. 在负载因子小于0.1时,会进行收缩。
  1. BGSAVE 命令用于在后台创建 Redis 数据库的快照。当执行此命令时,Redis 会 fork 出一个子进程,子进程则将内存中的数据写入到磁盘上的一个 RDB 文件中。这个过程不会阻塞主 Redis 进程,所以 Redis 可以继续处理客户端请求。RDB(Redis DataBase)文件是一个压缩的二进制文件,表示某一时刻 Redis 数据库的完整快照
  2. BGREWRITEAOF 命令用于优化 AOF(Append Only File)文件的大小。Redis 的 AOF 持久化通过记录数据库的所有写操作到一个文件中来工作,这个文件随着操作的积累会不断增长。BGREWRITEAOF 命令会在后台创建一个当前数据的最小操作集,以此来重写 AOF 文件,这个过程同样不会阻塞主 Redis 进程。

5. 跳跃表

跳跃表几乎只用于有序集合。
在这里插入图片描述

  1. zskiplistNode: 这是跳跃表的节点结构定义。每个节点代表有序集合中的一个元素。
  2. zskiplistLevel: 这个结构体定义了跳跃表节点在不同层级的信息,每个节点可以有一个或多个层级(level)。
  • forward: 是一个指向同一层级的下一个节点的指针。在查找操作中,这个指针允许我们“跳过”一些节点,从而更快地在跳跃表中进行搜索。

  • span: 这是一个无符号整数,它记录了当前节点与通过 forward 指针所指向的下一个节点之间的跨度。在进行范围查询或者计算排名时,这个值非常有用,因为它可以快速计算出两个节点之间的间隔。
    backward: 这是一个指向当前节点前一个节点的指针,在双向遍历时使用。

  1. score: 这是一个双精度浮点数,用来保存节点的分数值。在有序集合中,元素是根据这个分数进行排序的,分数相同时则按照存储的对象(obj)来进行字典序比较。

  2. obj: 这是一个指向实际存储数据的指针,通常是一个字符串类型。在 Redis 中,这是指向 robj(Redis 对象)的指针,它可以存储字符串、列表、哈希表等不同类型的数据结构。

  3. level[]: 这是一个大小可变的数组,它的具体长度取决于节点所在的层数。这个数组存储每一层的 zskiplistLevel 结构体,允许节点在跳跃表的多个层级上存在。

可以通过zskiplist持有这些节点。
在这里插入图片描述
为什么要用跳跃表?

简单性:跳表的算法和代码实现相比平衡树要简单得多。对于平衡树,如 AVL
树或红黑树,它们的旋转操作逻辑复杂,难以编写且容易出错。跳表提供了一种容易理解和实现的高效有序数据结构。

效率:跳表的查找、插入和删除操作的平均时间复杂度都是 O(log n),与平衡树相当。

灵活性:跳表支持快速的顺序访问和有效的范围查询,这对于数据库索引来说是非常重要的。

并发性:跳表的数据结构更容易实现锁定机制,这使得在并发环境下的性能表现更好。由于节点的层次结构,跳表可以更容易地实现细粒度锁或无锁并发算法。

动态:跳表无需预先知道数据规模,它可以根据实际需要动态地进行扩展,这在不可预知数据量的实时系统中非常有用。

空间效率:虽然跳表的多层结构需要额外的空间来存储指针,但它的空间复杂度仍然是线性的(O(n)),而且在实践中这个额外空间的使用通常是可控的。

实践:在实际应用中,跳表往往能够提供与平衡树相似或有时候更优的性能表现,特别是在插入和删除操作频繁的场景中。

6. 整数set(intset)

整数集合是集合键的底层实现之一,当一个集合包含整数值元素,并且这个集合的元素数量不多时,redis就会使用整数集合作为集合键的底层实现。

在这里插入图片描述

  1. encoding: 整数类型位宽
    决定着整数是几位的。

  2. length: 集合中元素数量

  3. contents 元素集合
    元素集合仅仅手encoding影响。
    intset 主要支持三种整数编码类型:16 位、32 位和 64 位整数

6.1. 升级(upgarde)

整数集合一般默认会给于较小的整数类型,如16位。

不过当加入一个新的集合元素,数据类型大于整数位宽的话,会进行升级操作,以便存入新的较长的整数。

下面简述下升级步骤

  1. 根据新元素的类型,拓展数组空间的大小。
  2. 将原来元素全部转换成新的元素。
  3. 将新元素添加到数组中。

有序集合不支持降级。

思考一下为什么?

  1. 判断麻烦
    升级操作只需大于当前数据类型即可,降级操作需要所有元素小于更小一档数据类型的长度,难以判断。

7. 压缩列表(ziplist)

Redis 的压缩列表(ziplist)是一种为了节省内存而设计的紧凑数据结构,用于存储整数值、小字符串或者一些较小的聚合数据结构,如小哈希表、小列表等。压缩列表是 Redis 内部使用的一种序列化方式,它通过在一个连续的内存区域中紧密排列数据元素来减少内存占用。

7.1. 列表结构

在这里插入图片描述

  • zibytes:unit32_t 4字节:
    记录压缩列表占用的字节数,可用于内存重合重分配和计算zlend
  • zltail :unit32_t 4字节:
    记录压缩列表起始地址到压缩列表表尾节点
  • zllen : unit16_t 2字节:
    记录压缩列表中包含节点的数量
  • entry 列表节点
    列表中的节点
  • alend unit8_t 1字节 特殊值
    标记压缩列表末端

7.2. 压缩列表节点的构成

在这里插入图片描述

  1. previous_entry_length:
    这个属性用于快速遍历压缩列表时向前移动。它记录了前一个节点的长度,如果前一个节点的长度小于254字节,则该长度直接存储在这个属性中;如果长度大于或等于254字节,则这个属性会占用5个字节,第一个字节设置为0xFE(254),接下来的4个字节存储前一个节点的实际长度。

  2. encoding: 该属性定义了节点值的类型和存储格式。它既可以表示值是一个整数还是一个字符串,又可以指示具体的存储方式,比如整数值可以根据大小使用不同的编码来节省空间,字符串则可以根据长度使用不同的编码方式。

  3. content: 这是节点实际存储的数据,其格式和长度由编码属性确定。如果节点存储的是整数,则内容直接是这个整数的二进制表示。如果节点存储的是字符串,则内容就是字符串的实际字节序列。

7.3. 连锁更新

连锁更新是压缩列表对于节点内容发生增加操作的时候会造成连续更新的操作,如果造成连环式更新会造成非常巨大的性能消耗,虽然这种概率很低?

这种情况是如何发生的的呢?

  1. 增加内容
    当节点数据增加的时候,如果恰好增加后大于254字节,那么后面的previous_entry_length势必会增加。
  2. 最坏情况
    如果恰好,previous_entry_length增加加上4个字节的时候,也大254个字节,后面的节点也会进行调成,最坏的情况,所有的节点都要调整。
  3. 调整过程
    由于压缩列表中每个节点都仅仅压缩在一起,每个节点调整,会造成所有后置节点内存地址重分配。所以最坏的复杂度为O(N^2)

节点减少会造成连锁更新吗
不会!

  • 连锁更新是个费事费力的操作,若非增加必要则不一定会做。
  • 在减少更新的过程中,这种压缩的作用手机节省内存,这个操作往往不会立即发生。
  • 20
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

只会写bug的靓仔

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值