本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!
【专栏简介】
随着数据需求的迅猛增长,持久化和数据查询技术的重要性日益凸显。关系型数据库已不再是唯一选择,数据的处理方式正变得日益多样化。在众多新兴的解决方案与工具中,Redis凭借其独特的优势脱颖而出。
【技术大纲】
为何Redis备受瞩目?原因在于其学习曲线平缓,短时间内便能对Redis有初步了解。同时,Redis在处理特定问题时展现出卓越的通用性,专注于其擅长的领域。深入了解Redis后,您将能够明确哪些任务适合由Redis承担,哪些则不适宜。这一经验对开发人员来说是一笔宝贵的财富。
在这个专栏中,我们将专注于Redis的6.2版本进行深入分析和介绍。Redis 6.2不仅是我个人特别偏爱的一个版本,而且在实际应用中也被广泛认为是稳定性和性能表现都相当出色的版本。
【专栏目标】
本专栏深入浅出地传授Redis的基础知识,旨在助力读者掌握其核心概念与技能。深入剖析了Redis的大多数功能以及全部多机功能的实现原理,详细展示了这些功能的核心数据结构和关键算法思想。读者将能够快速且有效地理解Redis的内部构造和运作机制,这些知识将助力读者更好地运用Redis,提升其使用效率。
将聚焦于Redis的五大数据结构,深入剖析各种数据建模方法,并分享关键的管理细节与调试技巧。
【目标人群】
Redis技术进阶之路专栏:目标人群与受众对象,对于希望深入了解Redis实现原理底层细节的人群。
1. Redis爱好者与社区成员
Redis技术有浓厚兴趣,经常参与社区讨论,希望深入研究Redis内部机制、性能优化和扩展性的读者。
2. 后端开发和系统架构师
在日常工作中经常使用Redis作为数据存储和缓存工具,他们在项目中需要利用Redis进行数据存储、缓存、消息队列等操作时,此专栏将为他们提供有力的技术支撑。
3. 计算机专业的本科生及研究生
对于学习计算机科学、软件工程、数据分析等相关专业的在校学生,以及对Redis技术感兴趣的教育工作者,此专栏可以作为他们的学习资料和教学参考。
无论是初学者还是资深专家,无论是从业者还是学生,只要对Redis技术感兴趣并希望深入了解其原理和实践,都是此专栏的目标人群和受众对象。
让我们携手踏上学习Redis的旅程,探索其无尽的可能性!
Redis服务端
Redis作为高性能的键值存储系统,其核心职责在于高效地与众多客户端建立并维护网络连接,处理来自这些客户端的多样化命令请求。
在执行这些命令时,Redis不仅负责在其内部数据库中存储和更新由客户端操作产生的数据,还通过精细的资源管理机制确保服务器自身的稳定运行与性能优化。
服务启动流程
自服务器程序启动的刹那起,直至其全面就绪,能够无缝接纳并高效处理来自客户端的多样化指令与请求,这一流程蕴含了数个至关重要的阶段与精细化的准备过程。
- 预设配置基准值:为所有配置项赋予一套预设的基准值,这些值将作为系统运行的初始参考点,确保在没有用户自定义配置的情况下,系统也能以一套合理的参数启动。
// 初始化所有配置,为它们设置默认值
initServerConfig();
// 如果是sentinel模式,额外做相关初始化
// sentinel_mode的值由命令行参数中是否包含参数“--sentinel”决定
if (server.sentinel_mode) {
initSentinelConfig();
initSentinel();
}
- 配置文件融合策略:随后,系统加载
redis.conf
配置文件,该步骤旨在根据用户的配置偏好和特定需求,对之前预设的基准值进行灵活调整,实现配置参数的个性化与动态覆盖。
// redis-server和redis-check-rdb、redis-check-aof共享了同一个main函数
// 进入redis_check_rdb_main或redis_check_aof_main后不会再返回,
// 也就是这两个函数后的代码均不会被执行。
if (strstr(argv[0],"redis-check-rdb") != NULL)
redis_check_rdb_main(argc,argv,NULL);
else if (strstr(argv[0],"redis-check-aof") != NULL)
redis_check_aof_main(argc,argv);
// 加载redis.conf,
// 包括初始化module列表loadmodule_queue,
// 注意会将redis-server的命令行参数一并传递给module
loadServerConfig(configfile,options);
- 信号管理机制部署:为了提升系统的响应性和健売性,系统安装了一套高效的信号处理器。这套机制能够监听并响应外部或系统内部的各类信号,如中断请求、终止信号等,确保系统在接收到相应信号时能够做出恰当的响应。
initServer();
- 全局状态初始化:紧接着,系统对全局状态进行初始化操作,包括内存分配、数据结构构建等关键步骤。这一过程为系统的后续运行奠定了坚实的基础,确保系统内部状态的一致性和准确性。
// 如果是daemon方式,则创建pid文件
if (background || server.pidfile) createPidFile();
// 修改进程的title,加入状态和端口号,这样对ps等更为友好
redisSetProcTitle(argv[0]);
// 进程启动时显示LOGO,
// 可通过redis.conf中的always-show-logo控制是否显示
redisAsciiArt();
- 启动TCP监听与epoll事件循环:系统通过创建epoll实例并启动TCP监听,实现了对网络请求的高效管理和响应。epoll作为一种高效的I/O事件通知机制,能够显著提升系统在处理大量并发连接时的性能和稳定性。
// 检查TCP的“/proc/sys/net/core/somaxconn”
checkTcpBacklogSettings()
-
BIO线程池构建:为了进一步优化系统的I/O处理能力,系统创建了BIO(Blocking I/O)线程池。这些线程专门负责处理阻塞式的I/O操作,如文件读写、网络数据传输等,从而有效减轻主线程的负担,提高系统的整体性能。
-
模块加载机制:系统支持动态加载模块的功能,通过加载用户指定的模块,可以扩展系统的功能集,满足多样化的应用需求。在启动过程中,系统会按照配置或默认策略加载相应的模块。
// 如果不是在哨兵模式下,且运行在Linux系统上,执行特定的内存配置检查
// 检查"/proc/sys/vm/overcommit_memory"和"/sys/kernel/mm/transparent_hugepage/enabled"的值
linuxMemoryWarnings();
// 加载所有通过redis.conf中的loadmodule指令指定的模块,如果有任何模块加载失败,则Redis启动失败
moduleLoadFromQueue();
- 数据持久化加载:系统支持AOF(Append Only File)和RDB(Redis Database)两种数据持久化方式。在启动过程中,系统会根据配置选择并加载相应的持久化数据文件,确保系统能够恢复到之前的状态或继续之前的操作。
// 从磁盘加载RDB或AOF文件到内存,根据配置(是否启用了AOF),决定加载AOF文件还是RDB文件,加载失败会导致Redis进程无法启动
loadDataFromDisk();
- 回调机制设置:为了提高系统的灵活性和可扩展性,系统设置了
beforeSleep
和afterSleep
两个回调点。这些回调点允许用户在特定时刻插入自定义代码,以实现诸如日志记录、性能监控等功能。
// 注册回调beforeSleep,对于eversec模式的appendfsync将在这个回调中进行
aeSetBeforeSleepProc(server.el,**beforeSleep**);
aeSetAfterSleepProc(server.el,afterSleep);
// 进入epoll主循环:BSD为kqueue死循环,Solaris为evport死循环,其它为select死循环
- 进入主事件循环:最后,系统进入epoll事件循环阶段,持续监听并处理网络事件、I/O操作等。这一阶段是系统持续运行、提供服务的关键所在,确保了系统能够长时间稳定运行并满足用户的各种需求。
// 如果Redis配置了监听TCP端口(ipfd_count > 0)
if (server.ipfd_count > 0)
serverLog(LL_NOTICE,"Ready to accept connections");
// 如果Redis配置了Unix套接字监听(sofd > 0)
if (server.sofd > 0)
serverLog(LL_NOTICE,
"The server is now ready to accept connections at %s",
server.unixsocket);
// 网络数据的收和发,以及所有COMMAND的操作均在这个循环中完成
aeMain(server.el);
// 关闭epoll,释放相关资源
aeDeleteEventLoop(server.el);
redis-server核心的初始化
接下来,我们将采用源码逐一揭示这些步骤,包括初始化配置参数、创建网络连接监听、启动后台线程(如serverCron)、加载持久化数据等,包括但不限于:
尽管其底层实现采用了高效且接近硬件的C语言,却巧妙地融入了面向对象的设计理念,通过一系列精心设计的核心对象体系,展现了其强大的数据操作能力和灵活性。
redisServer对象
redisServer对象本质上构成了Redis服务器的灵魂——redis-server
的Context对象。它不仅是一个容器,更是一个全能的管家,全面掌管着Redis服务器的各种全局状态与配置。
// 定义Redis服务器的结构体
struct redisServer {
/* General settings and information */
pid_t pid; // 主进程的进程ID
char *configfile; // 配置文件的绝对路径,如果未指定则为NULL
/* Networking configuration */
int port; // TCP监听端口
/* Append Only File (AOF) persistence */
int aof_state; // AOF的状态(ON开启|OFF关闭|WAIT_REWRITE等待重写)
int aof_fsync; // 同步文件的策略
/* Redis Database (RDB) persistence */
long long dirty; // 自上次保存以来,数据库的变化量
/* Logging */
char *logfile; // 日志文件的路径
/* Replication as master */
char replid[CONFIG_RUN_ID_SIZE+1]; // 当前复制ID,用于复制过程中标识服务器
/* Replication as slave */
char *masterhost; // 主服务器的主机名
/* Cluster configuration */
int cluster_enabled; // 是否启用集群模式
/* Scripting support */
lua_State *lua; // Lua解释器实例,所有客户端共享一个实例
/* Latency monitoring */
dict *latency_events; // 延迟事件的字典,用于监控Redis操作的延迟
//保存了秒级精度的系统当前NWIX时间鞭
time_t unixtime;
//保存了毫秒级精度的系统当前WIX时间段
long long mstime;
};
命令执行流程
针对于分析服务器内部各组件(如命令解析器、执行引擎、数据存储模块等)如何紧密协作,共同完成命令的接收、解析、执行与响应反馈。
serverCron
Redis服务器内置的serverCron
函数,作为其核心的心跳机制,默认以每100毫秒的精确间隔自动触发执行。包括但不限于内存管理(如过期键清理)、持久化任务(如AOF和RDB文件的写入)、客户端连接管理(如超时检测与断开)、统计信息收集等。
更新服务器时间缓存
serverCron
函数以每100毫秒的频率自动更新unixtime
和mstime
属性,这两个时间标记的精度实际上受限于其更新频率,因此在需要高精度时间的场景中并不直接适用。
struct redisServer{
//保存了秒级精度的系统当前NWIX时间鞭
time_t unixtime;
//保存了毫秒级精度的系统当前WIX时间段
long long mstime;
};
具体而言,这些时间属性主要用于一些对时间精度要求不高的操作,比如日志记录的时间戳、服务器LRU(最近最少使用)缓存策略的时钟更新、评估是否执行数据持久化任务(如AOF或RDB文件写入)的时机,以及计算服务器的连续运行时间(uptime)。
更新LRU时钟
在Redis服务器的状态管理中,lruclock
属性担任着记录服务器最近最少使用(LRU)时钟的重要角色。与unixtime
和mstime
属性相似,lruclock
同样属于服务器时间缓存机制的一部分,但其专门服务于优化内存管理策略。
struct redisServer{
//默认每10秒更新一次的时钟缓存,用于计算键的空转(idle)时长。
unsigned lruclock:22;
}
虽然lruclock
的更新频率可能不如unixtime
或mstime
那样频繁(因为它们各自服务于不同的时间精度需求),但它确保了在内存管理任务中,键的LRU状态能够得到准确且高效的维护。
每个Redis对象都会有一个lru属性,这个lru属性保存了对象最后一次被命令访问的时间,程序会执行一个高效的计算过程:从服务器当前的lruclock
值中减去该对象上次被访问时记录的lru
时间戳值,所得结果即为该对象自上次访问以来的闲置时间,即空转时间。
typedef struct redisobject(
unsigned lru:22;
} robj;
当需要评估一个数据库键(即键值对中键所对应的值对象)的闲置时间,即所谓的“空转时间”时,系统巧妙地运用了lruclock
属性与对象自身的lru
(注意,这里假设lru
为对象记录的时间戳属性,实际属性名可能因Redis版本或配置而异,但逻辑相似)属性之间的差值来实现。
命令请求的执行过程
探讨一个命令请求从发起至获取响应的全过程时,客户端与服务器之间需紧密配合,历经一系列精心设计的交互步骤。以客户端执行特定命令为例,这一过程不仅仅是简单的信息发送与接收,而是涉及了多个复杂而精细的操作环节。
redis>SET KEY VALUE
OK
客户端发送SET KEY VALUE命令至接收到OK回复的过程中,客户端与服务器之间协同工作的细节变得尤为关键。这一周期性的交互涵盖了多个精心编排的步骤,确保了命令的准确传达、高效处理及成功响应。
-
客户端发起行动,将
SET KEY VALUE
的指令请求精准地传递给服务器,启动了数据更新流程的起点。 -
服务器接收到来自客户端的
SET KEY VALUE
指令后,立即启动处理机制。它解析指令内容,随后在内部数据库中执行相应的设置操作,确保数据的一致性与准确性。操作成功后,服务器生成了OK
的确认回复,标志着命令的成功执行。 -
服务器将这份
OK
的确认回复封装成网络可识别的数据包,并通过高效的网络通信协议将其安全地传回给客户端,实现了信息的即时反馈。 -
客户端在接收到服务器的
OK
回复后,迅速进行解析与验证,确认操作已成功完成。随后,它以一种直观且友好的方式,将这个成功的反馈呈现给用户,让用户能够即时了解到操作的结果。
发送命令请求
当输入一个命令请求时,客户端立即启动其内置的转换机制,将这一人类可读的命令请求转换为Redis协议所规定的格式。这一过程不仅确保了命令的标准化,还使其能够在网络中高效、安全地传输。
客户端利用已经与Redis服务器建立的稳定连接一个基于套接字的通信通道,将转换后的协议格式命令请求发送给服务器。通过一个实例来深化这一过程的理解。在Redis客户端中,轻轻敲下了这样一个命令:
SET KEY VALUE
这个看似简单的指令,实则蕴含了数据存储与检索的复杂逻辑。客户端便迅速响应,将这一人类语言编写的命令,巧妙地转化为Redis服务器能够理解的协议格式。转换后的协议内容,以一种结构化的方式展现,如下所示:
*3\r\n$3\r\nSET\r\n$3\r\nKEY\r\n$5\r\nVALUE\r\n
读取命令请求
当客户端与服务器之间建立的连接套接字,因客户端的写入操作而变得具备读取条件时,服务器会触发一个机制,该机制随即调用命令请求处理器以执行一系列精心设计的操作。
-
解析与存储协议命令:服务端读取套接字中按照既定协议格式编排的命令请求,随后将这些精确的数据安全地保存到专为客户端维护的输入缓冲区中。
-
命令参数的抽取与分类:系统对输入缓冲区内的命令请求进行深入分析,精确地提取出命令请求所蕴含的命令参数及其数量。
系统将这些宝贵的参数及其数量信息,分别精心地存储到客户端状态中的
argv
(参数值数组)和argc
(参数数量)属性里,以便后续命令执行时能够高效访问。
- 执行指定命令:完成了命令参数的准备工作后,系统无缝地调用命令执行器,依据客户端明确指定的命令,启动执行流程。
延续前一小节关于SET命令的探讨,程序在成功将命令请求存储至客户端状态的输入缓冲区后,该客户端状态所呈现出的具体形态。
我们针对于对输入缓冲区中的协议进行分析:*3\r\ns3\r\nSET\r\ns3\r\nKEY\r\ns5\r\nVALUE\r\n
得出的分析结果保存到客户端状态的argv属性和argc属性里面。
命令执行器 — 查找命令实现
当命令执行器启动其处理流程时,首要任务是依据客户端请求中的argv[0]参数,该参数作为标识符,用于在预定义的命令表(command table)中精确检索目标命令。这一查找过程是基于键值对映射的逻辑,其中命令表被巧妙地设计为一个字典结构,其键(key)直接对应于Redis支持的各类命令名称,诸如"set"、"get"、"del"等常用操作;而每个键所对应的值(value),则是一个精心构造的redisCommand结构体实例。
redisCommand结构的主要属性
redisCommand结构体,作为Redis命令实现信息的载体,详尽地记录了每个命令的核心属性与功能特性。这些属性包括但不限于命令的处理函数指针、命令的参数数量、命令的读写类型、以及命令执行时的权限检查要求等,它们共同定义了一个Redis命令的完整行为轮廓。
属性名 | 类型 | 作用 |
---|---|---|
name | char | 命令的名字,比如"set" |
proc | redisCommandProc | 函数指针,指向命令的实现函数,比如setCommand。redisCommandProc定义为typedef void redisCommandProc(redisClient *c); |
arity | int | 命令参数的个数,用于检查命令请求的格式是否正确。如果这个值为负数-N,那么表示参数的数量大于等于N。注意命令的名字本身也是一个参数,比如SET msg "hello world"命令的参数是"SET"、"msg"、"hello world",而不仅仅是"msg"和"hello world" |
sflags | char* | 字符串形式的标识值,这个值记录了命令的属性,比如这个命令是写命令还是读命令,这个命令是否允许在载入数据时使用,这个命令是否允许在Lua脚本中使用等等 |
flags | int | 对sflags标识进行分析得出的二进制标识,由程序自动生成。服务器对命令标识进行检查时使用的都是flags属性而不是sflags属性,因为对二进制标识的检查可以方便地通过位运算(如&、|、~)来完成 |
calls | long long | 服务器总共执行了多少次这个命令 |
milliseconds | long long | 服务器执行这个命令所耗费的总时长(以毫秒为单位) |
sflags属性的标识
标识 | 意义 | 带有这个标识的命令 |
---|---|---|
w | 这是一个写入命令,可能会修改数据库 | SET、RPUSH、DEL等 |
r | 这是一个只读命令,不会修改数据库 | GET、STRLEN、EXISTS等 |
m | 这个命令可能会占用大量内存,执行前要先检查服务器的内存使用情况,如果内存紧缺就禁止执行这个命令 | SET、APPEND、RPUSH、LPUSH、SADD、SINTERSTORE等 |
a | 这是一个管理命令 | SAVE、BGSAVE、SHUTDOWN等 |
p | 这是一个发布与订阅功能方面的命令 | PUBLISH、SUBSCRIBE、PUBSUB等 |
s | 这个命令不可以在Lua脚本中使用 | BRPOP、BLPOP、BRPOPLPUSH、SPOP等 |
R | 这是一个随机命令,对于相同的数据集和相同的参数,命令返回的结果可能不同 | SPOP、SRANDMEMBER、SSCAN、RANDOMKEY等 |
S | 当在Lua脚本中使用这个命令时,对这个命令的输出结果进行一次排序,使得命令的结果有序 | SINTER、SUNION、SDIFF、SMEMBERS、KEYS等 |
l | 这个命令可以在服务器载入数据的过程中使用 | INFO、SHUTDOWN、PUBLISH等 |
A | 这是一个允许从服务器在带有过期数据时使用的命令 | SLAVEOF、PING、INFO等(注意:这里我假设了PNG 可能是PING 的误写,WFO 可能不是标准Redis命令,因此我替换了它) |
M | 这个命令在监视器(monitor)模式下不会自动被传播(propagate) | EXEC(注意:通常EXEC命令与事务相关,不直接与监视器模式传播相关,但这里按您的要求列出) |
redisCommand
SET命令,其标识性名称为"set",通过setCommand
函数实现具体功能。该命令的参数个数被设定为-3,这一独特设计巧妙地表示SET命令能够接收至少三个参数,以灵活应对多样化的数据设置需求。同时,SET命令被赋予了"wm"标识,这双重标记不仅揭示了其作为写入命令的本质,即能够修改数据库状态,还强调了执行前需对服务器内存状况进行评估的重要性,以防因内存占用过多而导致服务不稳定。
GET命令则以"get"为名,通过getCommand
函数简洁明了地实现了数据检索功能。其参数个数严格设定为2,明确指出了GET命令操作所需的精确参数数量,体现了Redis命令设计的严谨性。而"r"标识的赋予,则直观表明了GET命令的只读特性,即该命令不会对数据库状态产生任何修改。
最后,服务器内部各组件之间的紧密协作是一个高度复杂而精细的过程,它依赖于各组件之间的高效沟通与协同工作。通过不断优化这一流程,我们可以进一步提升服务器的性能与稳定性,为用户提供更加优质、高效的服务体验。