Redis设计与实现读书笔记

Redis设计与实现读书笔记

  • Redis设计与实现[^1]
    • 简单动态字符串
      • SDS的基础定义
      • 与C字符串的差别
        • 常数获取长度
        • 杜绝缓冲区溢出
        • 减少修改字符串时带来的内存重分配次数
        • 二进制安全
        • 函数兼容
    • 链表
      • 链表和链表节点的实现
    • 字典
      • 字典的实现
        • 哈希表定义
        • 哈希表节点定义
        • 字典定义
      • 哈希算法
      • 解决键冲突

Redis设计与实现1

简单动态字符串

SDS的基础定义

代码结构示例:

struct sdshdr {//记录buf数组中已使用字节的数量//等于SDS所保存字符串的长度int len;//记录buf数组中未使用字节的数量int free;//字节数组,用于保存字符串char buf[];
};

内存描述

  • free属性的值为0,表示这个SDS没有分配任何未使用空间。
  • len属性的值为5,表示这个SDS保存了一个五字节长的字符串。
  • buf属性是一个char类型的数组,数组的前五个字节分别保存了’R’、‘e’、‘d’、‘i’、‘s’五个字符,而最后一个字节则保存了空字符’\0’。

SDS遵循C字符串以空字符结尾的惯例,保存空字符的1字节空间不计算在SDS的len属性里面,并且为空字符分配额外的1字节空间,以及添加空字符到字符串末尾等操作,都是由SDS函数自动完成的,所以这个空字符对于SDS的使用者来说是完全透明的。遵循空字符结尾这一惯例的好处是,SDS可以直接重用一部分C字符串函数库里面的函数。

与C字符串的差别

C语言使用的这种简单的字符串表示方式,并不能满足Redis对字符串在安全性、效率以及功能方面的要求,所以Redis的作者开发了SDS这种字符串数据结构

常数获取长度

因为C字符串并不记录自身的长度信息,所以为了获取一个C字符串的长度,程序必须遍历整个字符串,对遇到的每个字符进行计数,直到遇到代表字符串结尾的空字符为止,这个操作的复杂度为O(N)​。
和C字符串不同,因为SDS在len属性中记录了SDS本身的长度,所以获取一个SDS长度的复杂度仅为O(1)​。

这是属于功能和效率上的考虑,使用更加简洁高效

杜绝缓冲区溢出

因为C字符串不记录自身的长度,内存和长度担保需要手动控制,否则不小心就会导致溢出覆盖掉其他内存中存储的字符串值,安全性上是有隐患的。
与C字符串不同,SDS的空间分配策略完全杜绝了发生缓冲区溢出的可能性:当SDS API需要对SDS进行修改时,API会先检查SDS的空间是否满足修改所需的要求,如果不满足的话,API会自动将SDS的空间扩展至执行修改所需的大小,然后才执行实际的修改操作,所以使用SDS既不需要手动修改SDS的空间大小,也不会出现前面所说的缓冲区溢出问题。

这是属于安全性上的考虑

减少修改字符串时带来的内存重分配次数

为了避免c字符串的内存重新分配动态变化带来的性能损耗而设计,这应该也是SDS设计的主要原因
SDS通过未使用空间解除了字符串长度和底层数组长度之间的关联:在SDS中,buf数组的长度不一定就是字符数量加一,数组里面可以包含未使用的字节,而这些字节的数量就由SDS的free属性记录。通过未使用空间,SDS实现了空间预分配和惰性空间释放两种优化策略。

  • 空间预分配用于优化SDS的字符串增长操作:当SDS的API对一个SDS进行修改,并且需要对SDS进行空间扩展的时候,程序不仅会为SDS分配修改所必须要的空间,还会为SDS分配额外的未使用空间。
    • 如果对SDS进行修改之后,SDS的长度(也即是len属性的值)将小于1MB,那么程序分配和len属性同样大小的未使用空间,这时SDS len属性的值将和free属性的值相同。举个例子,如果进行修改之后,SDS的len将变成13字节,那么程序也会分配13字节的未使用空间,SDS的buf数组的实际长度将变成13+13+1=27字节(额外的一字节用于保存空字符)​。
    • 如果对SDS进行修改之后,SDS的长度将大于等于1MB,那么程序会分配1MB的未使用空间。举个例子,如果进行修改之后,SDS的len将变成30MB,那么程序会分配1MB的未使用空间,SDS的buf数组的实际长度将为30MB+1MB+1byte。
  • 惰性空间释放用于优化SDS的字符串缩短操作:当SDS的API需要缩短SDS保存的字符串时,程序并不立即使用内存重分配来回收缩短后多出来的字节,而是使用free属性将这些字节的数量记录起来,并等待将来使用。
二进制安全

SDS的API都是二进制安全的(binary-safe)​,所有SDS API都会以处理二进制的方式来处理SDS存放在buf数组里的数据,程序不会对其中的数据做任何限制、过滤、或者假设,数据在写入时是什么样的,它被读取时就是什么样。这也是我们将SDS的buf属性称为字节数组的原因——Redis不是用这个数组来保存字符,而是用它来保存一系列二进制数据。

通过使用二进制安全的SDS,而不是C字符串,使得Redis不仅可以保存文本数据,还可以保存任意格式的二进制数据。这是安全性和功能性上的胜利。

函数兼容

通过遵循C字符串以空字符(即/0)结尾的惯例,SDS可以在有需要时重用<string.h>函数库,从而避免了不必要的代码重复。

链表

链表和链表节点的实现

每个链表节点使用一个adlist.h/listNode结构来表示:

typedef struct listNode {// 前置节点struct listNode * prev;// 后置节点struct listNode * next;//节点的值void * value;
}listNode;

虽然仅仅使用多个listNode结构就可以组成链表,但使用adlist.h/list来持有链表的话,操作起来会更方便:

typedef struct list {//表头节点listNode * head;//表尾节点listNode * tail;//链表所包含的节点数量unsigned long len;//节点值复制函数void *(*dup)(void *ptr);//节点值释放函数void (*free)(void *ptr);//节点值对比函数int (*match)(void *ptr,void *key);
} list;

如图所示:
list图示
Redis的链表实现的特性可以总结如下:

  • 双端:链表节点带有prev和next指针,获取某个节点的前置节点和后置节点的复杂度都是O(1)​。
  • 无环:表头节点的prev指针和表尾节点的next指针都指向NULL,对链表的访问以NULL为终点。
  • 带表头指针和表尾指针:通过list结构的head指针和tail指针,程序获取链表的表头节点和表尾节点的复杂度为O(1)​。
  • 带链表长度计数器:程序使用list结构的len属性来对list持有的链表节点进行计数,程序获取链表中节点数量的复杂度为O(1)​。
  • 多态:链表节点使用void*指针来保存节点值,并且可以通过list结构的dup、free、match三个属性为节点值设置类型特定函数,所以链表可以用于保存各种不同类型的值。

链表被广泛用于实现Redis的各种功能,比如列表键、发布与订阅、慢查询、监视器等。

字典

字典的实现

哈希表定义

Redis字典所使用的哈希表由dict.h/dictht结构定义:

typedef struct dictht {//哈希表数组dictEntry **table;//哈希表大小unsigned long size;//哈希表大小掩码,用于计算索引值//总是等于size-1unsigned long sizemask;//该哈希表已有节点的数量unsigned long used;
} dictht;

table属性是一个数组,数组中的每个元素都是一个指向dict.h/dictEntry结构的指针,每个dictEntry结构保存着一个键值对。size属性记录了哈希表的大小,也即是table数组的大小,而used属性则记录了哈希表目前已有节点(键值对)的数量。sizemask属性的值总是等于size-1,这个属性和哈希值一起决定一个键应该被放到table数组的哪个索引上面。

哈希表节点定义

哈希表节点使用dictEntry结构表示,每个dictEntry结构都保存着一个键值对:

typedef struct dictEntry {//键void *key;//值union{void *val;uint64_tu64;int64_ts64;} v;//指向下个哈希表节点,形成链表struct dictEntry *next;
} dictEntry;

key属性保存着键值对中的键,而v属性则保存着键值对中的值,其中键值对的值可以是一个指针,或者是一个uint64_t整数,又或者是一个int64_t整数。next属性是指向另一个哈希表节点的指针,这个指针可以将多个哈希值相同的键值对连接在一次,以此来解决键冲突(collision)的问题。

字典定义

Redis中的字典由dict.h/dict结构表示:

typedef struct dict {//类型特定函数dictType *type;//私有数据void *privdata;//哈希表dictht ht[2];// rehash索引//当rehash不在进行时,值为-1in trehashidx; /* rehashing not in progress if rehashidx == -1 */
} dict;

type属性和privdata属性是针对不同类型的键值对,为创建多态字典而设置的:

  • type属性是一个指向dictType结构的指针,每个dictType结构保存了一簇用于操作特定类型键值对的函数,Redis会为用途不同的字典设置不同的类型特定函数。
  • 而privdata属性则保存了需要传给那些类型特定函数的可选参数。
typedef struct dictType {//计算哈希值的函数unsigned int (*hashFunction)(const void *key);//复制键的函数void *(*keyDup)(void *privdata, const void *key);//复制值的函数void *(*valDup)(void *privdata, const void *obj);//对比键的函数int (*keyCompare)(void *privdata, const void *key1, const void *key2);//销毁键的函数void (*keyDestructor)(void *privdata, void *key);//销毁值的函数void (*valDestructor)(void *privdata, void *obj);
} dictType;

ht属性是一个包含两个项的数组,数组中的每个项都是一个dictht哈希表,一般情况下,字典只使用ht[0]哈希表,ht[1]哈希表只会在对ht[0]哈希表进行rehash时使用。除了ht[1]之外,另一个和rehash有关的属性就是rehashidx,它记录了rehash目前的进度,如果目前没有在进行rehash,那么它的值为-1。

哈希算法

Redis计算哈希值和索引值的方法如下:

#使用字典设置的哈希函数,计算键key的哈希值
hash = dict-type->hashFunction(key);
#使用哈希表的sizemask属性和哈希值,计算出索引值
#根据情况不同,ht[x]可以是ht[0]或者ht[1]
index = hash & dict->ht[x].sizemask;

当字典被用作数据库的底层实现,或者哈希键的底层实现时,Redis使用MurmurHash2算法来计算键的哈希值。2

解决键冲突

Redis的哈希表使用链地址法(separate chaining)来解决键冲突,每个哈希表节点都有一个next指针,多个哈希表节点可以用next指针构成一个单向链表,被分配到同一个索引上的多个节点可以用这个单向链表连接起来,这就解决了键冲突的问题。

这块没什么好说的,jdk最早期的实现,包括现在jdk初始的实现也都是用的这个办法,缺点在于键冲突过多时链表寻找也会比较慢,导致效率下降


  1. 作者是黄健宏,2014-06出版,redis的版本比较老,基于Redis 3.0来写的,不过基本的思路和大概的设计实现是可以参考借鉴的。 ↩︎

  2. 现在用的什么算法没有考证,留一个代办项吧 ↩︎

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/483882.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【笔记】离散数学 1-3 章

1. 数理逻辑 1.1 命题逻辑的基本概念 1.1.1 命题的概念 命题(Proposition):是一个陈述句,它要么是真的(true),要么是假的(false),但不能同时为真和假。例如…

SQL SERVER 2016 AlwaysOn 无域集群+负载均衡搭建与简测

之前和很多群友聊天发现对2016的无域和负载均衡满心期待,毕竟可以简单搭建而且可以不适用第三方负载均衡器,SQL自己可以负载了。windows2016已经可以下载使用了,那么这回终于可以揭开令人憧憬向往的AlwaysOn2016 负载均衡集群的神秘面纱了。 …

浅谈——Linux命令入门之前奏

目录 一、备份操作系统 1、快照 2、克隆 二、操作系统的使用注意 1、Linux严格区分大小写 2、Linux 文件“扩展名” 3、Linux 中所有的内容以文件的形式进行保存 4、Linux 中所有的存储设备都必须挂载之后才能使用 5、Linux 系统文件目录的结构 6、Linux 系统文件的目…

matlab中disp,fprintf,sprintf,display,dlmwrite输出函数之间的区别

下面是他们之间的区别: disp函数与fprintf函数的区别 输出格式的灵活性 disp函数:输出格式相对固定。它会自动将变量以一种比较直接的方式显示出来。对于数组,会按照行列形式展示;对于字符串,直接原样输出并换行。例如…

计算机视觉——相机标定(Camera Calibration)

文章目录 1. 简介2. 原理3. 相机模型3.1 四大坐标系3.2 坐标系间的转换关系3.2.1 世界坐标系到相机坐标系3.2.2 相机坐标系到图像坐标系3.2.3 像素坐标系转换为图像坐标系3.2.4 世界坐标转换为像素坐标 3.3 畸变3.3.1 畸变类型3.3.1.1 径向畸变(Radial Distortion&a…

纯粹直播 1.7.7 |手机版和TV版,聚合六大直播平台,原画播放

纯粹直播是一款开源的应用程序,支持兴趣化主题的游戏直播、户外直播和才艺直播节目。目前可以观看斗鱼、B站、虎牙和抖音等六大直播平台的内容。该应用适配了安卓手机和电视盒子平台使用,并且软件无广告,提供原画质播放体验。 大小&#xff…

【论文复现】隐式神经网络实现低光照图像增强

📝个人主页🌹:Eternity._ 🌹🌹期待您的关注 🌹🌹 ❀ 隐式神经网络实现低光照图像增强 引言那么目前低光照图像增强还面临哪些挑战呢? 挑战1. 不可预测的亮度降低和噪声挑战2.度量友好…

算法第一弹-----双指针

目录 1.移动零 2.复写零 3.快乐数 4.盛水最多的容器 5.有效三角形的个数 6.查找总价值为目标值的两个商品 7.三数之和 8.四数之和 双指针通常是指在解决问题时,同时使用两个指针(变量,常用来指向数组、链表等数据结构中的元素位置&am…

JAVA-平台模块系统原理

菜鸟为了巩固所写 目录 菜鸟为了巩固所写 代码之间的依赖性 绘制类型依赖图 扩展到包之间的依赖关系 进一步延伸到jar包之间的依赖性 组件依赖图 JAVA技术领域中的两个著名的“擦除” Java类型的“大泥球” JAVA模块解析 模块解析的过程 模块路径明确模块的搜索与…

Keil5配色方案修改为类似VSCode配色

1. 为什么修改Keil5配色方案 视觉习惯:如果你已经习惯了VSCode的配色方案,尤其是在使用ESP-IDF开发ESP32时,Keil5的默认配色可能会让你感到不习惯。减少视觉疲劳:Keil5的默认背景可能过于明亮,长时间使用可能会导致视…

微服务监控prometheus+Grafana

目录 Prometheus 概述 核心组件 特点 使用场景 Grafana 概述 功能特点 使用场景 PrometheusGrafana组合 部署和配置 一、准备工作 二、部署Prometheus 三、部署Grafana 四、创建监控仪表盘 五、验证和调优 总结 微服务监控是确保微服务架构稳定运行的关键环节…

⭐Java---反射--获取类信息⭐

目录 三种获取类信息的方式: 一个输入类名字获取类信息的类: 一个测试类: 测试结果 三种获取类信息的方式: 对象.getClass()类.classClass.forname("类的路径") People p; Class c1p.getClass();//将对象&#xff…

等差数列末项计算

等差数列末项计算 C语言代码C 代码Java代码Python代码 💐The Begin💐点点关注,收藏不迷路💐 给出一个等差数列的前两项a1,a2,求第n项是多少。 输入 一行,包含三个整数a1,a2&#x…

PETRv2: A Unified Framework for 3D Perception from Multi-Camera Images

全文摘要 本文介绍了一种名为PETRv2的统一框架,用于从多视图图像中进行三维感知。该框架基于先前提出的PETR框架,并探索了时间建模的有效性,利用前一帧的时间信息来提高三维物体检测效果。作者在PETR的基础上扩展了三维位置嵌入(…

【最新免费PPT制作并下载】Kimi PPT助手:智能化演示文稿生成,职场效率的革命性提升

最新免费PPT制作方法在这里!下面我想向大家介绍一款能够极大提升我们工作效率的工具——Kimi PPT助手。 Kimi PPT助手:智能化演示文稿生成 Kimi PPT助手是由Moonshot AI推出的一款革命性产品,它通过人工智能技术,实现了PPT的一键…

蓝桥杯准备训练(lesson2 ,c++)

3.1 字符型 char //character的缩写在键盘上可以敲出各种字符,如: a , q , , # 等,这些符号都被称为字符,字符是⽤单引号括 起来的,如: ‘a’ , ‘b’ &…

C# 动态类型 Dynamic

文章目录 前言1. 什么是 Dynamic?2. 声明 Dynamic 变量3. Dynamic 的运行时类型检查4. 动态类型与反射的对比5. 使用 Dynamic 进行动态方法调用6. Dynamic 与 原生类型的兼容性7. 动态与 LINQ 的结合8. 结合 DLR 特性9. 动态类型的性能考虑10. 何时使用 Dynamic&…

前端开发 之 15个页面加载特效中【附完整源码】

前端开发 之 15个页面加载特效中【附完整源码】 文章目录 前端开发 之 15个页面加载特效中【附完整源码】八:圆环百分比加载特效1.效果展示2.HTML完整代码 九:毒药罐加载特效1.效果展示2.HTML完整代码 十:无限圆环加载特效1.效果展示2.HTML完…

C语言练习作业1204

编写程序实现:strlen;strcpy;strcat;strcmp 的功能。 一、strlen()函数 1.1 分析 size_t strlen(const char *s);【功能】:计算字符串的长度,\0之前的字符串数量;【参数】:s&#…

查询品牌涉及两张表(brand、brand_admin_mapping)

文章目录 1、BrandController2、AdminCommonService3、BrandApiService3、BrandCommonService4、BrandSqlService涉及的表SQL 查询逻辑参数处理执行查询完整 SQL 逻辑参数映射总结 查询指定管理员下的品牌所涉及的表有哪些? http://127.0.0.1:8087/brand/admin/list…