【Redis技术进阶之路】「原理分析系列开篇」分析客户端和服务端网络诵信交互实现(服务端执行命令请求的过程 - 时间事件处理部分)

【专栏简介】

随着数据需求的迅猛增长,持久化和数据查询技术的重要性日益凸显。关系型数据库已不再是唯一选择,数据的处理方式正变得日益多样化。在众多新兴的解决方案与工具中,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的旅程,探索其无尽的可能性!


时间事件:serverCron函数

Redis服务器运行体系中,serverCron函数作为关键的周期性任务执行单元,以默认每100毫秒一次 的频率循环运作。该函数肩负着服务器资源调配维护 的重任,是保障Redis服务稳健运行的核心组件。

下文将系统阐述serverCron函数的具体执行流程,同时深入解析redisServer结构(即服务器状态)中与serverCron函数紧密关联的各项属性。

更新服务器时间缓存

Redis服务器中有不少功能需要获取系统的当前时间,而每次获取系统的当前时间都需要执行一次系统调用,为了减少系统调用的执行次数,服务器状态中的unixtime属性和mstime属性被用作当前时间的缓存:

c 复制代码
struct redisServer{
   //保存了秒级精度的系统当前Unix时间戳
	time_t unixtime;
	//保存了毫秒级精度的系统当前Unix时间戳
	long long mstime;
};

因为serverCron函数默认会以每l00 毫秒一次的频率更新unixtime属性和mstime属性,所以这两个属性记录的时间的精确度并不高:

  • 缓存主要用于场景服务器只会在打印日志、更新服务器LRU 时钟、**决定是否执行持久化任务、**计算服务器上线时间(uptime)这类对时间精确度要求不高的功能上。

对于为键设置过期时间、添加慢查询日志这种需要高精确度时间的功能来说,服务器还是会再次执行系统调用,从而获得最准确的系统当前时间

更新LRU时钟-lruclock

服务器状态中的lruclock属性保存了服务器的LRU时钟,这个属性和上面介绍的unixtime属性、mstime属性一样,都是服务器时间缓存的一种:

c 复制代码
struct redisServer
	//默认每10秒更新一次的时钟缓存,
	//用于计算键的空转(idle)时长。
	unsigned lruclock:22;
};

每个Redis对象都会有一个lru属性,这个lru属性保存了对象最后一次被命令访问的时间:

c 复制代码
typedef struct redisobject{
	unsigned lru:22;
} robj;

当服务器要计算一个数据库键的空转时间(也即是数据库键对应的值对象的空转时间 ),程序会用服务器的lruclock属性记录的时间减去对象的lru属性记录的时间,得出的计算结果就是这个对象的空转时间:

bash 复制代码
redis>SET msg "hello world"
OK

# 等待一小段时间
redis>OBJECT IDLETIME msg
(integer)20

# 等待一阵子
redis>OBJECT IDLETIME msg
(integer)180

# 访问msg键的值
redis>GET msg
"hello world

# 键处于活跃状态,空转时长为0
redis>OBJECT IDLETIME msg 
(integer)0

serverCron函数默认会以每10秒一次 的频率更新lruclock属性的值,因为这个时钟不是实时的,所以根据这个属性计算出来的LRU时间实际上只是一个模糊的估算值。

lruclock时钟的当前值可以通过INFO server命令的lruclock域查看:

bash 复制代码
redis>INFO server
# Server
lruclock:55923

更新服务器每秒执行命令次数-instantaneous_ops_per_sec

serverCron函数里,trackOperationsPerSecond函数会按照每100毫秒一次的频率来执行。

此函数借助抽样计算的手段,对服务器在最近一秒内所处理的命令请求数量进行估算,并将其记录下来。该数值能够通过INFO status命令的instantaneous_ops_per_sec域查看。

bash 复制代码
redis>INFO stats
# Stats
instantaneous_ops_per_sec:6

上面的命令结果显示,在最近的一秒钟内,服务器处理了大概6个命令。trackOperationsPerSecond函数和服务器状态中四个ops_sec开头的属性有关:

c 复制代码
struct redisServer{
//上一次进行抽样的时间
long long ops_sec_last_sample_time;
//上一次抽样时,服务器已执行命令的数量
long long ops_sec_last_sample_ops;
//REDIS_OPS_SEC_SAMPLES大小(默认值为16)的环形数组,
//数组中的每个项都记录了一次抽样结果。
long long ops_sec_samples [REDIS_OPS_SEC_SAMPLES];
//ops_sec_samp1es数组的素引值,
//每次抽样后将值自增一,
//在值等于16时重置为0,
//让ops_sec_samples数组构成一个环形数组。
int ops_sec_idx;
  1. 每次执行trackOperationsPerSecond函数时,会依据ops_sec_last_sample_time记录的上次抽样时刻与服务器当前时间 ,以及ops_sec_last_sample_ops记录的上次抽样已执行命令数当前已执行命令数。算出两次函数调用期间服务器每毫秒处理的命令请求平均数。
  2. 将该平均值乘以1000,得出服务器每秒处理命令请求的估计值,并将此估计值作为新元素存入ops_sec_samples环形数组中。
getoperationsPerSecond

当客户端执行INFO命令时,服务器将触发getoperationsPerSecond函数。该函数依据ops_sec_samples环形数组内的抽样数据,完成对instantaneous_ops_per_sec属性值的计算。以下为getoperationsPerSecond函数的具体实现代码:

c 复制代码
long long getoperationsPerSecond(void)(
	int j;
	long long sum 0
	// 计算所有取样值的总和
	for (j=0;j<REDIS_OPS_SEC_SAMPLES;j++)
		sum + = server.ops_sec_samples[j];
	//计算取样的平均值
	return sum / REDIS_OPS_SEC_SAMPLES;

getOperationsPerSecond函数的定义可知,instantaneous_ops_per_sec属性的值是通过对最近REDIS_OPS_SEC_SAMPLES次取样结果取平均值而得到的,这一数值属于估算值。

更新服务器内存峰值记录-stat_peak_memory

服务器状态中的stat_peak_memory属性记录了服务器的内存峰值大小:

c 复制代码
struct redisServer{
//已使用内存峰值
size_t stat_peak_memory
}

每次serverCron函数执行时,程序都会查看服务器当前使用的内存数量 ,并与stat_peak_memory保存的数值进行比较,如果当前使用的内存数量比stat_peak_memory属性记录的值要大,那么程序就将当前使用的内存数量记录到stat_peak_memory属性里面。

INFO memory

INFO memory命令的used_memory_peakused_memory_peak_human两个域分别以两种格式记录了服务器的内存峰值:

bash 复制代码
redis > INFO memory
# Memory
used_memory_peak:541824
used_memory_peak_human:492.06K

处理SIGTERM信号

在启动服务器时,Redis会为服务器进程的SIGTERM信号关联处理器sigtermHandler函数,这个信号处理器 负责在服务器接到SIGTERM信号时,打开服务器状态的shutdown_asap标识:

c 复制代码
//SIGTERM信号的处理器
static void sigtermHandler(int sig){
	//打印日志
	redisLogFromHandler(REDIS WARNING,"Received SIGTERM,scheduling shutdown...");
	//打开关闭标识
	server.shutdown_asap = 1;
}

每次serverCron函数运行时,程序都会对服务器状态的shutdown_asap属性进行检查,并根据属性的值决定是否关闭服务器:

c 复制代码
struct redisServer {
	//关闭服务器的标识:
	//值为1时,关闭服务器,
	//值为0时,不做动作。
	int shutdown_asap;
	... ...
}

以下展示了服务器在接到SIGTERM信号之后,关闭服务器并打印相关日志的:

bash 复制代码
[6794 I signal handler](1644435690)Received SIGTERM,scheduling shutdown...
[6794] 14 Nov 21:28:10.108 User requested shutdown...
[6794] 14 Nov 21:28:10.108 Saving the final RDB snapshot before exiting.
[6794] 14 Nov 21:28:10.161 DB saved on disk
[6794] 14 Nov 21:28:10.161 Redisis now ready to exit,bye bye...

从日志里面可以看到,服务器在关闭自身之前会进行RDB持久化操作 ,这也是服务器拦截SIGTERM信号的原因,如果服务器一接到SIGTERM信号就立即关闭,那么它就没办法执行持久化操作了。

管理客户端资源

serverCron函数每次执行都会调用clientsCron函数,clientsCron函数会对一定数量的客户端进行以下两个检查:

  • 若客户端与服务器的连接超时(即长时间无任何交互),程序将释放该客户端。
  • 若客户端自上次执行命令请求后,输入缓冲区大小超出规定长度,程序会释放当前输入缓冲区,并重新创建默认大小的输入缓冲区,以此避免输入缓冲区过度占用内存。

执行被延迟的BGREWRITEAOF

在服务器执行BGSAVE 命令的期间,如果客户端向服务器发来BGREWRITEAOF 命令,那么服务器会将BGREWRITEAOF 命令的执行时间延迟到BGSAVE命令执行完毕之后。

服务器的aof_rewrite_scheduled 标识记录了服务器是否延迟了BGREWRITEAOF命令:

c 复制代码
struct redisServer {
	// 如果值为1,那么表示有BGREWRITEAOF命令被延迟了
	int aof_rewrite_scheduled;
};

每次serverCron函数执行时,函数都会检查BGSAVE命令或者BGREWRITEAOF命令是否正在执行,如果这两个命令都没在执行,并且aof_rewrite_scheduled属性的值为1,那么服务器就会执行之前被推延的BGREWRITEAOF命令。

检查持久化操作的运行状态

服务器状态使用rdb_child_pid属性和aof_child_pid属性记录执行BGSAVE 命令和BGREWRITEAOF 命令的子进程的D,这两个属性也可以用于检查BGSAVE 命令或者BGREWRITEAOF命令是否正在执行:

c 复制代码
struct redisServer{
	//记录执行BGSAVE命令的子进程的ID:
	//如果服务器没有在执行BGSAVE,
	//那么这个属性的值为-1。
	pid t rdb_child_pid;
	/*PID of RDB saving child */
	//记录执行BGREWRITEAOF命令的子进程的ID:
	//如果限务器没有在执行BGREWRITEAOF,
	//那么这个属性的值为-1。
	pid_t aof_child_pid;
	/*PID if rewriting process */
};

每次serverCron函数执行时,程序都会检查rdb_child_pidaof_child_pid两个属性的值,只要其中一个属性的值不为-1,程序就会执行一次wait3函数,检查子进程是否有信号发来服务器进程:

  1. 如果有信号到达,那么表示新的RDB文件已经生成完毕(对于BGSAVE命令来说),或者AOF文件已经重写完毕(对于BGREWRITEAOF命令来说),服务器需要进行相应命令的后续操作,比如用新的RDB文件替换现有的RDB文件,或者用重写后的AOF文件替换现有的AOF文件。
  2. 如果没有信号到达,那么表示持久化操作未完成,程序不做动作。
相关推荐
源码云商1 小时前
Spring Boot + Vue 实现在线视频教育平台
vue.js·spring boot·后端
pqq的迷弟1 小时前
Redis的过期设置和策略
数据库·redis
不剪发的Tony老师2 小时前
Redis 8.0正式发布,再次开源为哪般?
数据库·redis
RunsenLIu3 小时前
基于Django实现的篮球论坛管理系统
后端·python·django
鱼儿也有烦恼4 小时前
Redis最新入门教程
数据库·redis·缓存
HelloZheQ4 小时前
Go:简洁高效,构建现代应用的利器
开发语言·后端·golang
caihuayuan55 小时前
[数据库之十四] 数据库索引之位图索引
java·大数据·spring boot·后端·课程设计
风象南5 小时前
Redis中6种缓存更新策略
redis·后端
码码哈哈0.05 小时前
2025最新:3分钟使用Docker快速部署Redis集群
redis·docker·容器
程序员Bears6 小时前
Django进阶:用户认证、REST API与Celery异步任务全解析
后端·python·django