1 Redis 的简介

Redis 实际上是简称,全称为 Remote Dictionary Server (远程字典服务器),由 Salvatore Sanfilippo 写的高性能 key-value 存储系统,其完全开源免费,遵守 BSD 协议。Redis 与其他 key-value 缓存产品(如 memcache)有以下几个特点。

  • 数据持久化:可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。
  • 数据结构简单丰富:既有简单的 key-value 类型的数据,同时还提供 list,set,zset,hash 等数据结构的存储。
  • 高可用:支持主从、哨兵、集群等模式,可以有效提高可用性。

Redis 也是一种 分布式缓存 ,其代码是 c 语言写的,那我们该如何阅读呢?

2 环境搭建

环境依赖,先看看 gcc 、cc、g++ 有没有安装

whereis gcc
whereis cc
whereis g++

安装gcc

xcode-select --install
brew install gcc
brew install pkg-config

查看 gcc 的版本:

$ gcc --version
Apple clang version 14.0.0 (clang-1400.0.29.202)
Target: x86_64-apple-darwin22.1.0
Thread model: posix
InstalledDir: /Library/Developer/CommandLineTools/usr/bin

我使用 CLion 2022.3.1 ,这个版本可以支持 Makefile 的项目,我们可以检查一下环境是不是有问题, 如果有问题,这里会有错误信息,我的之前报错是因为 Clion 的版本版本太低了,升级之后就好了。

下载Redis源码:

git clone https://github.com/redis/redis.git

切换到指定的版本

git checkout 7.0

File => New CMake Project from Sources, 打开源码项目, 会自动生成根目录下的 CMakeList.txt 文件:

Clion 导入项目的时候选择已有的 MakeFile 文件,如果有是否 clean 项目,选择 clean 即可,之后可以点开 MakeFile 文件:

如果需要禁止编译器优化,可以使用下面命令:

make CFLAGS="-g -O0" MALLOC=jemalloc

运行完之后, Src 文件下就会出现可运行文件:

然后可以看到这些可运行的选项,继而配置Edit configuration 运行配置:

选择 debug 进行启动,启动成功,然后可以进行调试了:

可以使用 Redis Desktop Manager 来进行连接:

或者命令行连接(没有密码就可以不需要 -a 12345):

redis-cli -h 127.0.0.1 -p 6379 -a 12345

如果头文件引入报红色下划线,那就试试重新加载一下

3 Redis源码阅读技巧

3.1 Redis 的目录结构

Redis 的目录:

  • deps: Redis 所依赖的第三方代码库

    • hdr_histogram:用于生成命令的延迟追踪直方图
    • hiredis:官方c语言客户端
    • Jemalloc:内存分配器,默认情况下选择该内存分配器来代替 Linux 系统的 libc-malloc,libc-malloc 性能不高,且碎片化严重。
    • linenoise:一种读线替换。它由 Redis 的 同一作者开发,但作为一个单独的项目进行管理,并根据需要进行更新。
    • lua:lua 脚本相关的功能。
  • src:源代码
    • commons:都是 json 文件,放着每个指令的原信息。
    • modules:实现 Redis Module 的示例代码。
    • 其他文件均是源码
  • test:测试代码
    • cluster,Redis Cluster 功能测试。
    • sentinel,哨兵集群功能测试。
    • unit,单元测试。
    • integration,主从复制功能测试。
  • utils:工具类
  • Makefile:编译文件
  • redis.conf : redis 启动的配置文件
  • sentinel.conf:哨兵配置

3.2 Redis 源码阅读顺序

网上的源码阅读顺序(引自网上):

  • 自底向上:从耦合关系最小的模块开始读,然后逐渐过度到关系紧密的模块。就好像写程序的测试一样,先从单元测试开始,然后才到功能测试。
  • 从功能入手:通过文件名(模块名)和函数名,快速定位到一个功能的具体实现,然后追踪整个实现的运作流程,从而了解该功能的实现方式。
  • 自顶向下:从程序的 main() 函数,或者某个特别大的调用者函数为入口,以深度优先或者广度优先的方式阅读它的源码。

从大方向来说,学习 Redis 会有两种路径:

  • 先从数据机构入手,直接手撕数据结构

    • 好处:学着踏实,知根知底
    • 坏处:容易从入门到放弃
  • 先从启动 Redis 开始,跟着启动顺序读源码,跟着具体的操作读源码
    • 好处:比较符合人的认知路线,知道 Redis 启动做了哪些操作,执行命令时做了哪些操作。
    • 坏处:容易迷路,前期看哪一句,都不知道在干嘛,毕竟 RDB,AOF,集群,哨兵这些源码,如果实操过才相对容易理解一点。

个人建议是先学习如何启动 Redis,抓大放小(大致知道哪个类启动,读那些配置文件,大概是做什么用的),学习 Redis 到底能干什么,大致知道 Redis 的一些用法之后,再去了解 Redis 的常用的数据结构,到底怎么实现的,这个时候对 Redis 的一些数据结构大致有印象,之后可以跟着 Redis 启动,执行命令去看具体功能执行的路径。

在 Debug 的过程中,可以加深影响,更加了解数据结构的设计,代码的调用关系。

4 C语言的知识

4.1 #define的基本用法

在C语言中,常量是使用频率很高的一个量。常量是指在程序运行过程中,其值不能被改变的量。常量常使用 #define来定义。

使用#define定义的常量也称为符号常量,可以提高程序的运行效率,Redis 的源代码中有比较多的地方都使用该方式。

一般有以下两种用法:

#define 宏名 宏值
#define 宏名(参数列表) 表达式

第一种就是定义常量,比如:

#define N 100

此后直到 #undef N之前, N的值都是100。当遇到#undef N,其后如果再出现 N,则 N 需要重新定义之后才可以使用。

第二种语法常用来定义符号函数。

例如:

#define AREA(x,y) (x)*(y)

表示用来求长和宽分别是x和y的矩形的面积。

需要注意的是,在表达式(x) * (y)中,x和y都要使用“()”括起来,这是因为符号函数在编译时时进行符号形式替换。如果不加()则可能会发生意想不到的错误,例如:

#define AREA(x,y)  x*y
...
A = AREA( 2+3, 1+2 );

此处预期的结果是15,但是实际的结果却是7,这是因为该段代码在编译进行了简单的符号替换而得到的实际表达式是:

A = 2+3 * 1+2;

根据运算符的优先级,先进行乘法运算,然后才是加法,这就导致了错误。

而如果使用

#define AREA(x,y)  (x)*(y)
...
A = AREA( 2+3, 1+2 );

则在编译时替换的结果是:

A = (2+3) * (1+2);
#include"stdio.h"
#define AREA(x,y) (x)*(y)
int main()
{
int a = AREA(2+3, 1+2);
printf( " %d\n", a);
return 0;
}

4.2 头文件

Redis 是使用 c 语言写的,里面有很多头文件:

#include "server.h"
#include "monotonic.h"
#include "cluster.h"
#include "slowlog.h"
#include "bio.h"
#include "latency.h"
#include "atomicvar.h"
#include "mt19937-64.h"
#include "functions.h"
#include "syscheck.h" #include <time.h>

< 开头的,比如 #include <time.h> 是标准库的头文件,会在系统指定路径下查找,对应到 Java里面可以理解为 官方的 jdk 里面的类,而类似 #include "server.h" 则是工程里面自定义的。

我没怎么写过 c 语言的代码, 一般 .c 文件是写实现的代码逻辑的,那如何在 a 文件里面写一个方法,让 b 文件也能用呢?

通过头文件的机制,类似 Java 里面的 接口, publicprivate 的概念,Java 中 一般希望对外暴露的方法,会设置为 public ,,如果不希望暴露,则设置为private。c 语言里面如果希望暴露,则可以在头文件里面定义,否则不用定义。(虽然c语言是面向过程的,但是Redis确实在里面实践一些面向对象的思想)。

比如计算两数之和 与 两数之差 的乘积 test.c

long long mul(int a,int b) {
return a*b;
} long long calculate(int a,int b) {
return mul(a+b,a-b);
}

暴露出去的头文件test.h

long long calculate(int a,int b);

运行的代码 main.c ,可以正常计算结果为 -3:

#include "stdio.h"
#include "test.h"
int main(){
printf("结果:%lld",calculate(1,2));
return 0;
}

但是如果直接引用 sum() 方法,则会报错,无法使用:

如果我们多次引用头文件会怎么样?结果是正常运行:

4.3 ifndef

Redis 里面有挺多的地方定义头文件的时候总是来一句 #isdef 或者 ifndef

#ifdef __linux__
#include <sys/mman.h>
#endif
#ifndef __ADLIST_H__
#define __ADLIST_H__
...
#endif /* __ADLIST_H__ */

如果加了 #ifndef ,则会判断只有没有定义这个宏的时候,才会定义它,第二次再次遇到 include 的时候,发现这个宏已经被定义过了,就会直接跳过,这样可以保证多次 include 也不会被解析多次,有且只有一次。

解析多次的坏处是什么?

  1. 如果在.h 文件里面定义了全局变量,会导致变量重复定义。这个基本不太会,公司编码规范一般都会禁止,这样写是不人道的。
  2. 浪费编译时间。

既然禁止了在 .h 文件里面定义全局变量,那全局变量在哪里定义呢?当然是 .c 文件,比如 Redis 里面的全局变量:

那其他的文件怎么使用?这个 sever 可是全局唯一的,维护了 redis 的全部状态数据,那当然是暴露出去,在哪里暴露出去,在 .h 文件,使用关键字 extern

5 小结一下

阅读源码,是一件长期的事情,但是我们每次跟读代码的时候,一定要带着问题去阅读,否则效率会下降挺多。前期了解数据结构模型的时候,可以在网上找一些简单易懂的博客,最好是有图片的,书籍比较推荐《Redis 设计与实现》。有一定了解之后,会有些疑问,不用担心,此时再通过读源代码去验证我们的想法,可能不少小伙伴没学过 c 语言,也不必担心,语言之间都是相通的,其次即使有关键字不会,可以通过搜索也可以快速了解其作用。

希望我们都能从全局看功能 --> 实践 --> 抓大放小 --> 带疑问看源码 --> 重构知识图谱 --> 关联知识 --> 跳出细节俯瞰全局,最终完成 Redis 相关的知识学习,并形成一套自己的方法论。

作者:秦怀

Redis【1】- 如何阅读 Redis源码的更多相关文章

  1. Redis集群生产环境源码安装

    安装redis集群  根据各人单位生产环境用户搭建一.安装环境    操作系统:centos7.6 关闭防火墙.关闭selinux redis1:192.168.26.128 redis2:192.1 ...

  2. Redis 专栏(使用介绍、源码分析、常见问题...)

    一.介绍相关 说Redis : 介绍Redis特性,使用场景,使用Jedis操作Redis等. 二.源码分析 1. 数据结构 Redis源码分析(sds):Redis自己封装的C语言字符串类型. Re ...

  3. 如何阅读Java源码 阅读java的真实体会

    刚才在论坛不经意间,看到有关源码阅读的帖子.回想自己前几年,阅读源码那种兴奋和成就感(1),不禁又有一种激动. 源码阅读,我觉得最核心有三点:技术基础+强烈的求知欲+耐心.   说到技术基础,我打个比 ...

  4. newsstand杂志阅读应用源码ipad版

    一款newsstand iPad杂志阅读应用源码(newsstand在线下载/动态显示等)可以支持在线下载/动态显示等  ,也是一款newsstand iPad杂志阅读应用源码.运行之后,会在iPad ...

  5. 如何阅读Java源码

    刚才在论坛不经意间,看到有关源码阅读的帖子.回想自己前几年,阅读源码那种兴奋和成就感(1),不禁又有一种激动.源码阅读,我觉得最核心有三点:技术基础+强烈的求知欲+耐心. 说到技术基础,我打个比方吧, ...

  6. 如何阅读mysql源码

    在微博上问mysql高手,如何阅读mysql 源码大致给了下面的一些建议: step 1,知道代码的组织结构(官方文档http://t.cn/z8LoLgh: Step2: 尝试大致了解一条sql涉及 ...

  7. 如何阅读jdk源码?

    简介 这篇文章主要讲述jdk本身的源码该如何阅读,关于各种框架的源码阅读我们后面再一起探讨. 笔者认为阅读源码主要包括下面几个步骤. 设定目标 凡事皆有目的,阅读源码也是一样. 从大的方面来说,我们阅 ...

  8. 如何阅读Java源码?

    阅读本文大概需要 3.6 分钟. 阅读Java源码的前提条件: 1.技术基础 在阅读源码之前,我们要有一定程度的技术基础的支持. 假如你从来都没有学过Java,也没有其它编程语言的基础,上来就啃< ...

  9. 早前阅读live555源码做的笔记

    早前阅读live555源码的时候做了一些简单的笔记.现在看来那个时候对C++的理解还是不够,还有很多不足.当时对很多名词也不是很熟悉,对一些类的描述也很生硬,所以笔记中有一些不通畅之处. 阅读live ...

  10. 阅读 Android源码的一些姿势

    日常开发中怎么阅读源码 找到正确的源码 IDE 是日常经常用的东西,Eclipse 就不说了,直接从 Android Studio(基于 IntelliJ Community 版本改造)开始. 我们平 ...

随机推荐

  1. Argo CD初体验

    什么是 Argo CD? Argo CD 是一个声明式的 GitOps 持续交付工具,用于 Kubernetes 集群.它通过持续监控 Git 仓库中的 Kubernetes 资源配置文件,将这些配置 ...

  2. 【论文解读】RLAIF基于人工智能反馈的强化学习

    [论文解读]RLAIF基于人工智能反馈的强化学习 一.简要介绍 人类反馈强化学习(RLHF)可以有效地将大型语言模型(LLM)与人类偏好对齐,但收集高质量的人类偏好标签是一个关键瓶颈.论文进行了一场R ...

  3. ASP.NET Core – Razor Syntax

    前言 Full stack 的问题就是经常需要一阵子离开一个环境. 比如我跑去写 Angular 几个月. 回来写 Razor, 肯定是卡卡的. 尤其是一些比较不常用到的语法. 所以有一个很好的笔记就 ...

  4. Naming Conversion & Case Style 命名规范

    前言 写代码有 2 个点很重要 第一是表达 (不要词不达意) 要达到这点, 就要多参考其它人如何表达. 第二是一致性 (一样的东西就用一样的写法) 要达到这点就要建立规范 以前的笔记 命名规范 nam ...

  5. JAVAEE——navicat安装

    Navicat Premium 15激活步骤 准备 Navicat Premium 15.Navicat Premium 15注册机 用到的都放在这里了: 链接:https://pan.baidu.c ...

  6. 配置windows update失败还原更改

    配置windows update失败还原更改_解决方案 解决方法: 方法1:     重启,按F8,选择最后一次正常启动.     如果还是需要等待.可采用方法2: 方法2:     重启,按F8,选 ...

  7. 《Vue.js 设计与实现》读书笔记 - 第9章、简单 Diff 算法

    第9章.简单 Diff 算法 9.1 减少 DOM 操作的性能开销 在之前的章节,如果新旧子节点的类型都是数组,我们会先卸载所有旧节点,再挂载所有新的子节点.但是如果存在相同类型的节点,我们完全可以复 ...

  8. Flutter 实现骨架屏

    什么是骨架屏 在客户端开发中,我们总是需要等待拿到服务端的响应后,再将内容呈现到页面上,那么在用户发起请求到客户端成功拿到响应的这段时间内,应该在屏幕上呈现点什么好呢? 答案是:骨架屏 那么什么是骨架 ...

  9. torch.max()使用讲解

    output = torch.max(input, dim) input输入的是一个tensor dim是max函数索引的维度0/1,0是每列的最大值,1是每行的最大值 实例: import torc ...

  10. 2023年6月中国数据库排行榜:OceanBase 连续七月踞榜首,华为阿里谋定快动占先机

    群雄逐鹿,酣战墨坛. 2023年6月的 墨天轮中国数据库流行度排行 火热出炉,本月共有273个数据库参与排名.本月排行榜前十变动不大,可以用一句话概括为:OTO 组合连续两月开局,传统厂商GBase南 ...