【专栏简介】
随着数据需求的迅猛增长,持久化和数据查询技术的重要性日益凸显。关系型数据库已不再是唯一选择,数据的处理方式正变得日益多样化。在众多新兴的解决方案与工具中,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服务器是事件驱动程序,处理的事件分时间事件和文件事件。
上一篇文章主要介绍了文件事件处理器机制,它基于Reactor模式实现网络通信。文件事件是对套接字操作的抽象,套接字可应答、可写或可读时,对应文件事件产生。文件事件分 AE_READABLE(读事件)和 AE_WRITABLE(写事件)。
文件事件和时间事件相互协作,服务器轮流处理,不发生抢占,时间事件实际处理时间通常会比设定的到达时间稍晚。
文件事件API实现(补充一下)
创建文件事件
ae.c/aeCreateFileEvent
函数接受一个套接字描述符 、一个事件类型 ,以及一个事件处理器 作为参数。 将给定套接字的给定事件加人到/O多路复用程序 的监听范围之内,并对事件 和事件处理器进行关联。
删除文件事件
ae.c/aeDeleteFileEvent
函数接受一个套接字描述符和一个监听事件类型作为参数。 让I/O多路复用程序取消对给定套接字的给定事件的监听,并取消事件和事件处理器之间的关联。
获取文件事件
ae.c/aeGetFileEvents
函数接受一个套接字描述符,返回该套接字正在被监听的事件类型:
- 如果套接字没有任何事件被监听,那么函数返回:AE_NONE。
- 如果套接字的读事件正在被监听,那么函数返回:AE_READABLE。
- 如果套接字的写事件正在被监听,那么函数返回:AE_WRITABLE。
- 如果套接字的读事件和写事件正在被监听,那么函数返回:AE_READABLE I AE_WRITABLE。
阻塞等待套接字事件
ae.c/aeWait
函数接受一个套接字描述符 、一个事件类型 和一个毫秒数为参数。
在给定的时间内阻塞并等待套接字的给定类型事件产生,当事件成功产生,或者等待超时之后,函数返回。
获取多路复用器实现
ae.c/aeGetApiName
函数返回I/O多路复用程序底层所使用的IO多路复用函数库的名称:返回"epoll"表示底层为epoll函数库,返回"select"表示底层为select函数库,诸如此类。
事件循环监听机制
-
ae.c/aeApiPoll
函数接受一个sys/time.h/struct timeva1
结构为参数,并在指定的时间内,阻塞并等待所有被aeCreateFileEven
t函数设置为监听状态的套接字产生文件事件,当有至少一个事件产生,或者等待超时后,函数返回。 -
ae.c/aeProcessEvents
函数是文件事件分派器 ,它先调用aeApiPoll
函数来等待事件产生,然后遍历所有已产生的事件,并调用相应的事件处理器来处理这些事件。
时间事件
Redis的时间事件主要分为一下两类:
- 定时事件:只在指定时间触发一次。例如,让一段程序在指定的时间之后执行一次。比如说,让程序X在当前时间的30毫秒之后执行一次。
- 周期性事件 :定期触发,一般情况下,服务器仅执行周期性的 serverCron 时间事件。比如说,让程序Y每隔30毫秒就执行一次。
时间事件的组成部分
- id:服务器为时间事件创建的全局唯一标识号
- 标识号在整个系统中是独一无二的,并且按照从小到大的顺序递增,新产生的事件的标识号会比旧事件的标识号更大
- when :毫秒精度的UNIX时间戳
- 记录了时间事件的到达时间,能够精确到毫秒级别,用于确定事件发生的时刻
- timeProc :时间事件处理器即一个函数 。
- 当时间事件到达时,服务器会调用相应的这个函数来处理该事件
如何判定时间事件的类型
时间事件是定时事件还是周期性事件取决于时间事件处理器的返回值
- 如果事件处理器返回
ae.h/AE_NOMORE
,意味着这个事件是定时事件 。- 定时事件在到达一次后就会被删除,并且之后不会再次到达。也就是说,一旦出现这种返回值,就可以确定该事件只会发生一次,之后不会再出现。
- 如果事件处理器返回一个非
ae.h/AE_NOMORE
的整数值,那么这个事件为周期性时间 :- 当一个时间事件到达之后,服务器会根据事件处理器返回的值,对时间事件的when属性进行更新,让这个事件在一段时间之后再次到达,并以这种方式一直更新并运行下去。
比如说,如果一个时间事件的处理器返回整数值30,那么服务器应该对这个时间事件进行更新,让这个事件在30毫秒之后再次到达。
定时事件的实现原理
服务器将所有时间事件都放在一个无序链表中,每当时间事件执行器运行时,它就遍历整个链表,查找所有已到达的时间事件,并调用相应的事件处理器。
下图展示了一个保存时间事件的链表的例子,链表中包含了三个不同的时间事件: 新的时间事件会被插入到链表的开头位置。由于这个特性,三个时间事件按照 ID 进行逆序排列。在这个排列中,处于表头位置的事件其 ID 为 3;处于中间位置的事件 ID 为 2;处于表尾位置的事件 ID 为 1。
无序链表
保存时间事件的链表为无序链表,明确指出这种无序不是指不按 ID 排序,而是不按照when属性的大小排序。
由于链表没有按when属性排序,所以在时间事件执行器运行时,必须遍历链表中的所有时间事件,以保证服务器中所有已到达的时间事件都能被处理。
无序链表并不影响时间事件处理器的性能
正常模式时的Redis服务器仅仅运用 serverCron 这一个时间事件。而当处于 benchmark 模式时,服务器也仅仅使用两个时间事件。
在这样的状况下,服务器差不多把无序链表简化为一个指针来加以使用。正因如此,采用无序链表去保存时间事件,并不会对事件执行的性能产生影响。
时间事件函数
aeCreateTimeEvent函数
ae.c/aeCreateTimeEvent
函数有两个参数,一个是毫秒数milliseconds,另一个是时间事件处理器Proc,主要目的是将一个新的时间事件添加到服务器中。
新的时间事件会在当前时间的milliseconds毫秒之后到达,并且这个事件的处理器是 proc。也就是说,通过这个函数可以设定一个在未来特定时间点触发的事件,并指定了触发时要执行的处理函数。
aeDeleteFileEvent函数
ae.c/aeDeleteFileEvent
函数接受一个时间事件ID作为参数,然后从服务器中删除该D所对应的时间事件。
aeSearchNearestTimer函数
ae.c/aeSearchNearestTimer
函数返回到达时间距离当前时间最接近的那个时间事件。
processTimeEvents函数
ae.c/processTimeEvents
函数是时间事件的执行器,这个函数会遍历所有已到达的时间事件,并调用这些事件的处理器。已到达指的是,时间事件的when属性记录的UNIX时间截等于或小于当前时间的UNX时间戳。
大致的伪代码逻辑如下所示:
python
def processTimeEvents ()
# 遍历服务器中的所有时间事件
for time_event in time_event_linked_list ()
# 检查事件是否已经到达
if time_event.when <unix_ts_now ()
# 事件已到达
# 执行喜件处理器,并获取返回值
retval time_event.timeProc()
# 如果这是一个定时事件
if retval AE NOMORE:
# 那么将该喜件从限务器中则除
delete_time_event from_server (time_event)
# 如果这是一个周期性事件
else:
# 那么按限事件处理器的返回值更新时何事件的when属性
# 让这个事件在指定的时间之后再次到达
update_when(time event,retval)
时间事件之serverCron函数
持续运行的Redis服务器需要定期对自身的资源和状态进行检查和调整,从而确保服务器可以长期、稳定地运行,这些定期操作由redis.c/serverCron
函数负责执行,它的主要工作包括: Redis服务器以周期性事件的方式来运行serverCron函数,在服务器运行期间,每隔一段时间,serverCron就会执行一次,直到服务器关闭为止。
- 在Redis2.6版本及之前,服务器默认规定serverCron每秒运行10次,平均每间隔100毫秒运行一次。
- 在Redis2.8开始之后,用户可以通过修改hz 选项来调整serverCron的每秒执行次数,具体信息请参考示例配置文件redis.conf关于hz选项的说明。
文件事件和时间事件调度执行
因为服务器中同时存在文件事件和时间事件两种事件类型,所以服务器必须对这两种事件进行调度,决定何时应该处理文件事件,何时又应该处理时间事件,以及花多少时间来处理它们等等。
aeProcessEvents
事件的调度和执行由ae,c/aeProcessEvents
函数负责,以下是该函数的伪代码表示:
python
def aeProcessEvents ()
# 获取到达时问高当前时间最接近的时间事件
time_event = aesearchNearestTimer()
# 计算最接近的时问事件距高到达还有多少毫秒
remaind_ms = time_event.when - unix_ts_now()
# 如果事件巳到达,那么remaind ms的值可能为负数,将它设定为0
if remaind_ms < 0:
remaind_ms = 0
# 根据remaind_ms的值,创建timeval结构
timeval = createtimeval_with_ms (remaind_ms)
# 阻塞并等待文件事件产生,最大阻塞时间由传入的timeval结构决定,如果remaind_ms的值为0,那么aeApiPol1调用之后马上返回,不阻塞
aeApiPoll(timeval)
# 处理所有已产生的文件事件
processFileEvents()
# 处理所有已到达的时间事件
processTimeEvents()
将aeProcessEvents函数置于一个循环里面,加上初始化和清理函数,这就构成了Redis服务器的主函数,以下是该函数的伪代码表示:
python
def main():
# 初始化服务器
init_server()
# 一直处理事件,直到服务器关闭为止
while server_is_not_shutdown ()
aeProcessEvents()
# 服务器关闭,执行清理操作
clean_server()
- 文件事件是随机出现的,如果等待并处理完一次文件事件之后,仍未有任何时间事件到达,那么服务器将再次等待并处理文件事件。
- 文件事件的不断执行,时间会逐渐向时间事件所设置的到达时间逼近,并最终来到到达时间,这时服务器就可以开始处理到达的时间事件了。
aeApiPoll
aeApiPoll函数的最大阻塞时间由到达时间最接近当前时间的时间事件决定,这个方法既可以避免服务器对时间事件进行频繁的轮询(忙等待),也可以确保aeApiPoll函数不会阻塞过长时间。
文件事件和时间事件的执行逻辑
文件事件和时间事件的处理是以同步、有序且原子的方式进行的,服务器既不会在中途中断事件处理,也不会对事件进行抢占。所以,无论是文件事件处理器,还是时间事件处理器,都会尽可能缩短程序的阻塞时长,并且在必要时主动放弃执行权,以此降低事件饥饿发生的概率。
例如,当命令回复处理器把一条命令回复写入客户端套接字时,若写入的字节数超出了预设常量,该处理器会主动使用 "break" 跳出写入循环,把剩余的数据留到下次再写。此外,时间事件会把极为耗时的持久化操作安排到子线程或子进程中去执行。
因为时间事件在文件事件之后执行,并且事件之间不会出现抢占,所以时间事件的实际处理时间,通常会比时间事件设定的到达时间稍晚一些。