winform 让他间隔一段时间 执行事件 且只执行一次_【Redis】从事件开始说起

Redis从事件开始说起

这是有关Redis缓存中间件系统的全局视图之一,我将在接下来的几篇博文中详细的从各个视角审视Redis系统。本篇作为系列的开端,着重于系统运行的主线过程,希望梳理出一个主线剧情,进而引出后面要说的复线剧情(模块以及数据结构)

Redis流程

Redis对linux的IO复用进行了封装,分别是ae_epoll, ae_select, ae_kqueue以及ae_evport,本文将重点介绍ae_epoll封装

目录:

  • main启动数据库
  • aeMain与aeProcessEvents
  • processTimeEvents
  • 小结

1. main启动数据库

// redis.c
int main(int argc, char **argv)

main是整个数据库启动的入口程序,也是我们阅读源码的起点。每个人阅读源码的习惯不同,着重的点也不同。我个人比较喜欢先摸索清楚一个大题的流程,抓住一个贯穿始终的主线。

main的工作繁杂且枯燥,从阅读角度来讲,就像是小说的背景介绍,平铺直叙,谈不上有什么精彩的地方。但是,它却可以给你一个全局视角——一个有关于Redis的整体轮廓——它能做什么,长什么样。比如,下面一段代码:你可以看到,Redis的服务器可以以哨兵模式运行,或者它也可以作为守护进程运行等。

if (server.sentinel_mode) {
        initSentinelConfig();
        initSentinel();
    }
...
if (server.daemonize) createPidFile();
...

main的主要任务,可以总结位以下几点:

  1. 初始化服务器及其配置(可以是配置文件,也可以是传入的参数
  2. 确定并配置服务器的运行模式(哨兵/守护/集群等配置
  3. 启动事件循环(马上讲到
题外话:程序该怎么看?尤其是成系统的应用,我们该如何阅读代码呢?我给出的建议是:先摸清楚主要的流程,然后进行分割。如何分割是一个仁者见仁智者见智的过程,各取所需。比如说,我们知道Redis不仅是一个优秀的缓存,但除此之外,它对一些基本和高级的数据结构的实现也非常精彩,如果你希望获得一个优秀的c的容器库,Redis绝对可以给你一些参考。

2.aeMain与aeProcessEvents

// ae.c
void aeMain(aeEventLoop *eventLoop) {...}

在服务器初始化完毕后会进入事件循环,该函数就整个系统的IO复用入口,在这里进行对事件的操作,主要的函数是aeProcessEvents,它有以下目的:

  1. 设置下次服务器进行阻塞等待的时间(特定的算法计算这个时间,比如这次循环没有事件被触发,那就阻塞的久一些,否则就阻塞的少一些)
  2. 触发所有就绪事件的响应函数(基于反应堆模型,后面会说到基本的几个反应函数)
  3. 如有必要,处理时间事件(TimeEvents)

32886b891947b86460f3a5b6861117de.png
aeProcessEvents的活动图

这个函数较为简单,比较拗口就是计算下次阻塞时间的部分,有兴趣的读者可以进一步阅读源码。

3. processTimeEvents

如上所述,当处理完就绪的读写事件之后,Redis进行对事件时间的处理历程中,这些任务主要在该函数中完成。

static int processTimeEvents(aeEventLoop *eventLoop)

之所以要将处理时间的部分抽出来,我认为主要原因有:

  1. 防止时间穿插异常(time skew)
  2. 对时间事件的处理不同于读写事件,事件时间存在发生顺序因果关系。函数中总是取出最近(相对于time(NULL))发生的事件进行处理

d67abf7bfd55094b5c58dd4d0a47338d.png
processTimeEvents活动图

过程较为简单,比较拗口的是防御时间穿插异常,有兴趣的读者可以进一步阅读源码。

4. 小结

经过以上三步,Redis就正式启动并开始监听事件了。接下来,我们需要继续顺着主线往下看:事件的响应函数。

Redis主流程的事件响应函数

接下来,我们主要介绍l两个事件响应函数,分别是serverCron, replicationCron。然后,我们会简单介绍一下AOF过程以及AOF和RDB结束后的工作。

目录:

  • serverCron
  • replicationCron
  • AOF/RDB及其后续

  1. serverCron

根据Redis源码注解,该函数的主要任务可以概括位以下几点

  • 统计数据(占比相当大)
  • 主从同步以及数据库持久化(AOF/RDB以及M-S同步)
  • 处理增量式哈希表的扩张(dict的处理,数据结构部分会讲到)
  • 时间事件的处理和连接管理

进而我们可以把serverCron函数视为一个“数据库的管理者”:它调用客户端程序的接口,来控制客户端的连接清空;它调用数据库接口,来管理和查看数据库的状态;它根据服务器当前的状态,有选择性地执行AOF和RDB;它根据收集到的从服务器地消息,来决定是否进行主从同步。

说这个函数是最重要的响应函数也不为过,它每秒执行一个固定的次数。从它出发,Redis才真正的“开始做事情”。

在一次serverCron的调用中,它可能会执行以下服务器函数

  1. clientsCron:客户端连接管理历程
  2. databasesCron:数据库再哈希历程
  3. rewriteAppendOnlyFileBackground(被挂起的一次):在进行一次AOF过程时,其他客户端的AOF请求将会被挂起,此时如果时机成熟,需要响应。
  4. backgroundSaveDoneHandler/backgroundRewriteDoneHandler:主从同步的管理历程
  5. rdbSaveBackground:rdb持久化管理历程
  6. rewriteAppendOnlyFileBackground(主动执行的一次):当修改-时间间隔条件满足时进行AOF持久化管理
  7. replicationCron:分布式复制管理历程,该历程非常重要
  8. clusterCron:集群管理历程
  9. 按照特定的频率进行服务器运行状态的收集

此外,我们都知道Redis是多进程模型,RDB和AOF历程都是fork出进程在处理。fork发生在以下服务历程中:

  1. rewriteAppendOnlyFileBackground:后台进行AOF存储,fork的子进程会执行rewriteAppendOnlyFile函数,进行实际的存储工作;
  2. rdbSaveBackground:后台进行RDB存储,fork的子进程会执行rdbSave函数来进行全表保存操作;

因此,对于发起fork的父进程,有义务将僵尸进程回收。所以,在serverCron中,我们需要对fork的进程进行回收。我们来看一下serverCron的处理方式

/* Check if a background saving or AOF rewrite in progress terminated. */
if (server.rdb_child_pid != -1 || server.aof_child_pid != -1) {

这是判断是否有子进程正在运行的逻辑,相当简单:只要对应进程的pid不是-1即可。然后,回收子进程时需要检查子进程发来的信号:

// WNOHANG non-blocking
if ((pid = wait3(&statloc,WNOHANG,NULL)) != 0) {

wait3是一个比较老的API,此处的WNOHANG是非阻塞式的回收方式。

2. replicationCron

replicationCron主要负责redis主从复制的过程管理,过程非常复杂。但从下图的活动顺序上大致可以总结归纳出以下几个点

  1. 在基于安全的通信的基础上,实现一个简单的PING-PONG心跳检测的协议
  2. 对于从服务器来说,replicationCron需要调用接口来接受主服务器的数据
  3. 对于主服务器来说,replicationCron需要调用接口来尽可能少而快地向从服务器发送数据

对于第三点,Redis专门做了优化,从命令上来说,Redis向开发人员提供了PSYNSYNC两个命令。其中PSYN命令用于执行增量同步复制,而SYNC则执行一次全量同步复制。后面我们会详细介绍。

85db4f56ef03651f93a32a5a3d8fc019.png
replicationCron活动图

上图中有两个区域,第一个区域是从服务器的视角,下面第二个区域则是主服务器的视角。

首先,我们先看Redis的PING-PONG协议。它是一个轻量的协议,只是用来主从用来确认彼此存在。下面是通信过程中的状态机图:

06b0edde0dc3ca285271950247737905.png
主从同步复制的状态机图

我们看到当一台服务器处于REDIS_REPL_CONNECT时,它通过调用connectWithMaster函数进行通信前的准备,当发出PING消息后,便进入等待PONG消息的状态,当PONG到达之后,服务器状态转换为REDIS_REPL_TRANSFER,即传输状态,在该状态下服务器期望开始一次BGSAVE或者SAVE命令(这条命令应该由主服务器执行)。

我们注意到,在服务器尝试发送PING之前,会进行一个检测,代码如下:

if (server.repl_backlog == NULL && listLength(slaves) == 0) return;

要看懂这一段,我们需要先介绍一下,Redis的增量复制的过程。

首先,如果你还不知道redisServer结构中的一个循环队列结构backlog。建议你先阅读一下这篇博文,它详细解释了该结构,以及主服务器进行一次PSYN的过程。

摇头哥:Redis主从复制的同步设计

当结构结构不存在时(NULL),也就是说服务器并没有希望向从服务器发送任何数据的意愿,所以函数就直接返回,不再发送PING消息。当发送确认可以发送PING后,服务器会启动一次MULTI事务,完成发送。

bee698aff1346301e740b75c482db1e2.png
主服务器发送PING

我们知晓了PING-PONG协议之后,主从服务器就开始传送数据。

在整个传输过程中,从服务器只可能发送PSYN或者SYNC命令:当PSYN被主服务器拒绝之后,从服务器再发送SYNC请求全复制。从此之后,从服务器不再发送消息给主服务器。

5b9feecec12b680df8c0c77d1771d77f.png
syncWithMaster回调函数,当收到PONG消息之后

PSYN

我们先来说一下PSYN的一些实现细节

从服务器尝试进行PSYN的函数是slaveTryPartialResynchronization,当从服务器重新连接到主服务器时(PING-PONG之后),从服务器需要与主服务器进行同步。这里需要重点说一下过程。

服务器结构体中,有一个缓存着的主服务器

redisClient *cached_master; /* Cached master to be reused for PSYNC. */

试想,当一个重新连接上的从服务器,如果保存着上次同步时的主服务器,那么就可以再次利用它的runid和offset(乐观锁,版本号),进行一次尝试。否则的话,则需要在集群中找到当前的主服务器,然后再尝试。他们对应的PSYN命令的如下:

  1. 有缓存的主服务器:PSYN cachedRunId cachedOffset
  2. 无缓存的主服务器:PSYN ? -1 表示请求当前主服务器,并且当前从服务器的版本为最初版本(为了请求到最近的版本号)

d449ffe558ff0bd55f8d386a19b6b69a.png
slaveTryPartialResynchronization活动图

当主服务器接收到来自从服务器的PSYN命令之后,便会执行syncCommand函数,在这个函数中,服务器就会最终决定是否可以进行一次部分增量备份(masterTryPartialResynchronization,可参考,摇头哥:Redis主从复制的同步设计),

SYN

在syncWithMaster函数中可以看到,从服务器如果只能进行FULL SYNC,则会注册一个readSyncBulkPayload函数,在该函数中,从服务器从规定的套接字中读取主服务器发送来的消息,并保存到本地的临时文件中,当传送完毕后,将临时文件名修改为配置的文件名,从而完成一次同步复制过程。

e1a06de975a218ab5573601a27db976d.png
readSyncBulkPayload活动图

3. AOF/RDB及其后续

我们把目光放回到serverCron历程中去,看一看它如何管理AOF和RDB的保存工作。

AOF过程及其后续操作

在serverCron函数中,主要通过rewriteAppendOnlyFileBackground函数接口来实现后台的AOF操作。AOF操作由客户端显式地执行BGREWRITEAOF命令来调用。由于是在后台开启新的进程对当前时间节点的所有"Append"操作进行记录,所以主进程需要在子进程结束后,将这一段时间内新增地操作添加进AOF文件中(这一时间段内新增的数据暂存在服务器的aof_rewrite_buf_blocks字段中)。这一过程是通过backgroundRewriteDoneHandler接口实现的。

2f556e2276755abf4122d5410309c0bb.png
AOF顺序图

由于AOF文件不向从服务器传送,所以无需考虑复制的情况。

RDB及其后续

rdb的保存于aof的保存过程大同小异,但是由于rdb文件需要在存储节点之间进行同步复制,所以多了一些处理步骤。

首先在serverCron中,程序需要检测服务器执行的次数和间隔是否超过了配置的值,然后进行保存

7a6b35a817d9d206dbeabd778d0d1517.png
rdbSaveBackground顺序图

rdb完成返回之后,子进程会通告父进程其执行的结果码,backgroundSaveDoneHandler接口将会检验这个结果的值,会有以下几种情况

  1. rdb执行成功:更新服务器的dirty位,减去同步完毕的数据量,设置lastbgsave_status位成功
  2. rdb执行出错:设置lastbgsave_status位错误
  3. rdb被中断:不一定失败

因为在后台执行的一次BGSAVE中,主进程正在进行rdbSave,这时,一些希望进行同步的从服务器就未得到主服务器的复制。故而,需要一个对尚未满足需求的从服务器们进行一次同步的过程。

updateSlavesWaitingBgsave函数实现了上述功能需求,其核心逻辑如下:

当从服务器的状态是等待bgsave开始时,则启动一次新的bgsave(这时从服务器可能在请求一次最新的rdb复制)。如果是等待结束,那么次此保存已经结束,可以开始把数据送给从服务器,所以为从服务器的套接字注册一个写事件(sendBulkToSlave)

if (slave->replstate == REDIS_REPL_WAIT_BGSAVE_START) 
{
        startbgsave = 1;
        slave->replstate = REDIS_REPL_WAIT_BGSAVE_END;
} else if (slave->replstate == REDIS_REPL_WAIT_BGSAVE_END) 
{
        struct redis_stat buf;
        if (bgsaveerr != REDIS_OK)
{
            freeClient(slave);
            redisLog(REDIS_WARNING,"SYNC failed. BGSAVE child returned an error");
           continue;
}
            if ((slave->repldbfd = open(server.rdb_filename,O_RDONLY)) == -1 ||
                redis_fstat(slave->repldbfd,&buf) == -1) {
                freeClient(slave);
                continue;
}

            slave->repldboff = 0;
            slave->repldbsize = buf.st_size;
            slave->replstate = REDIS_REPL_SEND_BULK;
            slave->replpreamble = sdscatprintf(sdsempty(),"$%lldrn",
                (unsigned long long) slave->repldbsize);


            aeDeleteFileEvent(server.el,slave->fd,AE_WRITABLE);
            if (aeCreateFileEvent(server.el, slave->fd, AE_WRITABLE, 
                          sendBulkToSlave, slave) == AE_ERR) {
                freeClient(slave);
                continue;
            }
        }

sendBulkToSlave执行完毕(指把数据传送完毕),就会再注册sendReplyToClient回调函数,将成功的消息发送给主服务器,完成一轮事件循环。

全文小结

本文主要顺着程序运行的主线,梳理了Redis的事件循环/主从复制等内容

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值