type
status
date
slug
summary
tags
category
titleIcon
password
icon
calloutIcon
调试Redis源码 +《Redis 5设计与源码分析》阅读笔记
预备工作
参考书籍

编译Redis源码
- 配置git代理与clone代码仓库
- 先修改src目录下的Makefile去掉编译优化,便于后续源码调试查看中间变量值,否则可能因编译优化而无法查看部分变量值(vscode显示为optimized out)
- 主要修改两点:1.OPTIMIZATION的O2改为O0 2.REDIS_LD后添加$(OPTIMIZATION)
- 如果一开始没有修改就进行了编译,可以修改文件后通过make clean清除现有内容重新make
- 如果移动过文件夹(比如从虚拟机本地文件夹移动到VMware的共享文件夹),也可以先make clean清除再重新make一次,不然调试找不到server.c的main函数


- optimized out的情况展示,就是中间结果被优化了,需要关掉O2优化,开O0

- 在项目目录下使用make

- 在src目录下make install

- 如果权限不够可以先修改对应目录的权限,非仅个人使用的机器慎用777


- 理想效果:
先查看是否有其他的redis-server,比如自行安装的,或者明确指定使用以下目录的内容

redis-server启动效果

redis-benchmark效果

VSCode配置Redis调试环境
调试文件
在项目目录下创建.vscode文件夹,往里面放入两个文件
launch.json
tasks.json
在调试界面使用gcc任务,可以观察到进入server.c的main函数

SDS示范效果 initlen为8,从call stack可以看出,本次创建的是key,test_sds总长为8

数据结构
SDS
hdr - header 表示可用于存储对应长度的字符串对应存储长度与容量的位数
跳表
创建
确定层高
每个节点创建时

删除known_hosts
开启一个redis-server redis-cli
需要重新make clean 再 make
如果不行就重新下仓库,改完Makefile再make
刚进入main就应该能看到j = 0,如果是optimized out就是优化没改


压缩列表
第4章 压缩列表
压缩列表ziplist本质上就是一个字节数组,是Redis为了节约内存而
设计的一种线性数据结构,可以包含多个元素,每个元素可以是一个字
节数组或一个整数。
Redis的有序集合、散列和列表都直接或者间接使用了压缩列表。
当有序集合或散列表的元素个数比较少,且元素都是短字符串时,
Redis便使用压缩列表作为其底层数据存储结构。列表使用快速链表
(quicklist)数据结构存储,而快速链表就是双向链表与压缩列表的组
合。
例如,使用如下命令创建一个散列键并查看其编码。
127.0.0.1:6379> hmset person name zhangsan gender 1 age 22
OK
127.0.0.1:6379> object encoding person
"ziplist"
HEADER:zlbytes 4字节 uint32 + zltail uint32 + zllen uint16 所以不能存储超过(2^16 - 1)元素个数 最多允许的字节数不超过(2^32 - 1)
END:zlend 1字节 uint8 表示结尾 固定为255 即为0xFF



《宏定义成员方法》
TAIL_OFFSET尾元素相对于起始地址偏移量


了解了压缩列表的基本结构,我们可以很容易地获得压缩列表的字
节长度、元素个数等,那么如何遍历压缩列表呢?对于任意一个元素,
我们如何判断其存储的是什么类型呢?我们又如何获取字节数组的长度
呢?
回答这些问题之前,需要先了解压缩列表元素的编码结构

previous_entry_length字段表示前一个元素的字节长度,占1个或者5
个字节,当前一个元素的长度小于254字节时,用1个字节表示;当前一
个元素的长度大于或等于254字节时,用5个字节来表示。而此时
previous_entry_length字段的第1个字节是固定的0xFE,后面4个字节才真
正表示前一个元素的长度。假设已知当前元素的首地址为p,那么pprevious_entry_length就是前一个元素的首地址,从而实现压缩列表从尾
到头的遍历。
encoding字段表示当前元素的编码,即content字段存储的数据类型
(整数或者字节数组),数据内容存储在content字段。为了节约内存,
encoding字段同样长度可变。压缩列表元素的编码如表4-1所示。


可以看出,根据encoding字段第1个字节的前2位,可以判断content
字段存储的是整数或者字节数组(及其最大长度)。当content存储的是
字节数组时,后续字节标识字节数组的实际长度;当content存储的是整
数时,可根据第3、第4位判断整数的具体类型。而当encoding字段标识
当前元素存储的是0~12的立即数时,数据直接存储在encoding字段的最
后4位,此时没有content字段。参照encoding字段的编码表格,Redis预
定义了以下常量对应encoding字段的各编码类型:
4.2 结构体
4.1节介绍了压缩列表的存储结构,我们发现对于压缩列表的任意
元素,获取前一个元素的长度、判断存储的数据类型、获取数据内容都
需要经过复杂的解码运算。解码后的结果应该被缓存起来,为此定义了
结构体zlentry,用于表示解码后的压缩列表元素。
结构体zlentry定义了7个字段,而4.1节显示每个元素只包含3个字
段。
回顾压缩列表元素的编码结构,可变因素实际上不止3个:
previous_entry_length字段的长度(prevrawlensize)、
previous_entry_length字段存储的内容(prevrawlen)、encoding字段的长
度(lensize)、encoding字段的内容(len表示元素数据内容的长度,
encoding表示数据类型)和当前元素首地址(p);而headersize则表示
当前元素的首部长度,即previous_entry_length字段长度与encoding字段
长度之和。
函数zipEntry用来解码压缩列表的元素,存储于zlentry结构体。

《没有输入类型与返回类型的函数》do { } while(0);
既使用高亮又避开注释格式限制

只有系统为big endian时候生效




每次指针操作先获取要读取的字段的字节/比特长度,然后决定1.按照涉及的范围读取内容2.跳到下一个字段的开头
哈希
1)可以存储海量数据,键值对是映射关系,可以根据键以O(1)的
时间复杂度取出或插入关联值。
2)键值对中键的类型可以是字符串、整型、浮点型等,且键是唯
一的。例如:执行set test"hello world"命令,此时的键test类型为字符
串,如test这个键存在数据库中,则为修改操作,否则为插入操作。
3)键值对中值的类型可为String、Hash、List、Set、SortedSet。
作用是把任意长度的输入通过散列算法转换成固定类型、固定长度的散列值,换句话
说,Hash函数可以把不同键转换成唯一的整型数据。散列函数一般拥有
如下特征:
1)相同的输入经Hash计算后得出相同输出;
2)不同的输入经Hash计算后一般得出不同输出值,但也可能会出现相同输出值。
所以,好的Hash算法是经过Hash计算后其输出值具有强随机分布性。例如Daniel J.Bernstein在comp.lang.c上发布的“times 33”散列函数,
其使用的核心算法是:“hash(i)=hash(i-1)*33+str[i]”,这是针对字符串已
知的最好的散列函数之一,因为其计算速度快,而且输出值分布得很好。
在应用上,通常使用现成的开源Hash算法,例如Redis自带客户端
就是使用“times 33”散列函数来计算字符串的Hash值,Redis服务端的
Hash函数使用的是siphash算法,主要功能与客户端Hash函数类似,其优
点是针对有规律的键计算出来的Hash值也具有强随机分布性,但算法较
为复杂
dictGenHashFunction函数主要作用是,入参是任意长度的字符串,
通过Hash计算后返回无符号整型数据。因此,我们可以通过Hash函数,
将任意输入的键转换成整型数据,使其可以当作数组的下标使用。
读到这里,想必有读者会有疑问,前文中字典的第2个特征是“键的类型可以为字符串、整型、浮点型等”,而Hash函数只把字符串转换成
整型数据,当遇到键的类型为非字符串时该如何处理?答案很简单,第
2个特征中键的类型是客户端感知的,而Redis服务端收到客户端发送过
来的键实际都为字符串。
当客户端执行“set 100.86 hello”命令时,此时的键在客户端看来是
浮点型数据,但Redis服务端收到的键的值其实就是字符串——100.86,
字符串长度为6,经过Hash函数转换后返回值为
11361771491584941503。
字典数据结构在这就需要添加第2及第3个字段,
分别为:①总容量——size字段;②已存入数据量——used字段
为了解决Hash冲突,所以数组中的元素除了应把键值对中的“值”存
储外,还应该存储“键”信息和一个next指针,next指针可以把冲突的键
值对串成单链表,“键”信息用于判断是否为当前要查找的键。此时数组
中元素的字段也明确了

人为设定Hash表的数组容量初始值为4,随着键值对存储
量的增加,就需对Hash表扩容,新扩容的容量大小设定为当前容量大小
的一倍,也就是说,Hash表的容量大小只能为4,8,16,32...。而sizemask
掩码的值就只能为3,7,15,31...,对应的二进制为11,111,1111,11111...,
因此掩码值的二进制肯定是每一位都为1。
索引值=Hash值&掩码值,对应Redis源码为:idx=hash&d>ht[table].sizemask,其计算结果等同Hash值与Hash表容量取余,而计算
机的位运算要比取余运算快很多。


dictht dictEntry dict


dictType结构体,则是为了实现各种形态的字典而抽象出来的一组操作函数。
·privdata字段,私有数据,配合type字段指向的函数一起使用。
·ht字段,是个大小为2的数组,该数组存储的元素类型为dictht,虽然有两个元素,但一般情况下只会使用ht[0],只有当该字典扩容、缩容需要进行rehash时,才会用到ht[1],rehash介绍详见5.3.2节。
·rehashidx字段,用来标记该字典是否在进行rehash,没进行rehash时,值为-1,否则,该值用来表示Hash表ht[0]执行rehash到了哪个元素,并记录该元素的数组下标值。
·iterators字段,用来记录当前运行的安全迭代器数,当有安全迭代器绑定到该字典时,会暂停rehash操作。Redis很多场景下都会用到迭代器,例如:执行keys命令会创建一个安全迭代器,此时iterators会加1,命令执行完毕则减1,而执行sort命令时会创建普通迭代器,该字段不会改变,关于迭代器的介绍详见5.4.1节。

在redis-server启动中,整个数据库会先初始化一个空的字典用于存储整个数据库的键值对。初始化一个空字典,调用的是dict.h文件中的dictCreate函数,对应源码为
原文给定的dictCreate定义错误
hiredis

唯一声明

不包括hires1个,有2个dictType变量声明,不使用DictType后缀抽出的常量



- 作者:CamelliaV
- 链接:https://camelliav.netlify.app/article/redis-source?target=comment
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
相关文章