Redis 5.0 部分源码剖析

db2f6ed8c5cfd6a1a43e1894858ce5c6.gif

从前有句古话说得好,天将降大任于斯人也,必要先看Redis。

以前古人还说过,窗前明月光,低头Redis。

古人还说过,所有的答案都在源码里。

昨天还有人跟我说,用Redis比Tair申请要方便。

不识庐山真面目,只缘身在此山中

我们先给出一副大图,来看看Redis AOF Rewrite的总体流程是怎么样的。


13d50913b55efbe005102ea26365b259.png

先看看大图里的几大组成部分

  1. 主进程与子进程,大家都知道,Redis AOF Rewrite是通过创建一个子进程来完成的。父子进程有一个重要特性,那就是"读时共享,写时复制"。后面我们详细聊

  2. 父子进程间通信使用的三个通道。

  3. 客户端写入Redis主进程时,涉及到的两个数据结构,aof_buf,aof_rewrite_buf_blocks;子进程涉及到的aof_child_diff数据结构。

  4. 一份当前使用的AOF文件,这份文件是准备退休的"现役"文件,另一份是子进程正在重写的"预备役"文件。



大致涉及的内容就是如此了,接下来按照一个AOF Rewrite执行的时间顺序来看看到底发生了什么事

万般皆由长风起

先来看看,Redis AOF Rewrite机制是怎么触发的呢?

6996e1106dbcaf9f02e6b14a1c685a15.png

有两种方式大家应该都很清楚了,一种是配置文件中配置的若干时间内,发生了若干键值对的变化,达到阈值就需要触发一次重写。



(这里补充一句,这个检查是在Redis后台主线程中调度时检查的,这个时间并不会是很确定的)

601217759a2c03c2c3e1791de68aa334.png

所谓的serverCron就是这个方法,何时触发,如何触发,我们回头细说

515c2d900e3ae8e3cb48b4871aec3cde.png

另一种是客户端,要求执行bgrewriteaof。



这里写的第三种,是在开启AOF时,才会进行一次。一般是在启动时就完成了AOF的启动。

但这里有一种特殊情况,就是在Redis有主从时。从服务在跟主服务同步数据时,主服务会生成一个RDB文件给从,从使用假客户端读取数据恢复至内存。这个阶段,从服务是需要停止AOF(如果原来开启的话)的。等到完成了主从同步的数据恢复后,自动开启AOF,这里就会执行一次AOF的重写。

6d2c13e31ec6680d485f697635d412d5.png

山一程,水一程,fork子进程

不管是什么原因,当确定要进行AOF Rewrite之后,首先做的就是进行一系列检查,然后fork一个子进程。

8d00bb3d059fd81806123366051f5e8b.png

有红线的地方就是fork子进程的地方。

if语句中的是子进程需要执行的代码,else中是主进程的。



先不着急研究主子进程分别做了什么事,看下前面的校验。

首先,如果有AOF重写子进程或RDB重写进程存在的话,就不进行本次的重写。

其次,如果创建主子进程的管道失败的话,也不进行重写。



创建子进程成功之后,子进程就会立即开始AOF文件的重写,而主进程则是继续提供对外服务,只是为了确保重写期间AOF不丢失,会多做几步操作。



这里创建的管道十分重要,一共有三个:

4a4105371d3fcd6283ae4e4d04ce2f83.png

提示:由于fork子进程会让主子进程共享内存,那么子进程一定是要知道主进程原有的数据存储在哪里的。

这里就涉及到将原有主进程的页表复制一份过来的操作。这个操作是阻塞的,会导致fork操作卡住。

因此在流量大的时候,要注意AOF Rewrite将Redis卡住,而使RT增大。

话分两头

  子进程

f91b875887095c765206f449668d7682.png

我们看看子进程都做了啥:

  1. 创建一个临时文件,名字是temp-rewriteaof-{pid}的文件,然后初始化一下文件句柄之类的引用。

  2. 判断是否是RDB混合模式,还是纯AOF模式,进行重写。这两者的区别,这里就不赘述了。



真正的重写其实很简单,就是挨个的读取db的内容,然后以对应的格式,写入文件中。

由于主子进程之间有"读时共享,写时复制"的限制,也就是如果是读取时两者公用一份内容,当有人要写的数据时,会将原有数据copy出来一份,在新的copy的数据上修改,旧保持不变。

Redis就利用了这一功能,保证了读到的数据是fork之前的最后版本的数据。



到这里为止,其实是AOF Rewrite的核心逻辑,其余的逻辑都是围绕在AOF Rewrite期间有数据发生变化来做的。



整个重写是非常耗费CPU的,趁着子进程加班加点干活时,我们来看一眼主进程在做什么。

  主进程

0f58dfa5d0ede5cc092e1d2d4d72dcfa.png

主进程在fork完子进程,把ORK交代给子进程之后,就对子进程不管不问了。偶尔检查一下子进程有没有把工作完成(通信管道有没有新数据/子进程有没有消失)。



假设在AOF重写过程中,有客户端发来了一个set a 1的请求,会将原来的a的值由0改为1。

由于有主子进程"读时共享,写时复制"的存在,不用担心子进程,它会读到老的数据。



在完成内存数据变化后,会走到下面这个方法中。我们仔细来看看。

99a219ee0e05cb0ad76adc96cd9cc4e1.png

这个方法在aof.c文件中,所有写aof的操作都走这个方法。

它做了一下几件事:

  1. Redis是有多个db的,如果命令操作的db不是当前的db,那么就会插入一条select db的命令。根据ditcid参数来确定

  2. 将带有过期时间的操作,转换为PEXPIREAT(EXPIRE/PEXPIRE/EXPIREAT/SETEX/PSETEX/SET [EX seconds][PX milliseconds])

  3. 将操作的命令,转换为RESP格式

  4. 写入AOF相关缓存

这里与AOF Rewrite相关的,是步骤4,我们重点来看下这个步骤做了什么:

f16cf0846e2745775c121cd3d876b615.png

首先,判断是否开启了AOF,那显然我们是开启了的啊,就需要将这条语句,写入旧的AOF文件中。这个很合理啊,万一重写失败了呢,数据不可以丢啊。

其次,如果有aof子进程pid存在,那么还要多做一步aofRewriteBufferAppend(),这个是做什么的呢?

它是将刚刚生成的语句,再次保存在一个aof_rewrite_buf_blocks的结构当中。

这个aof_rewrite_buf_blocks是一个list结构,它保存的都是10M大小一个的block。block中存储的就是刚刚生成的语句。

然后方法返回。本次aof写入操作结束。



aof_rewrite_buf_blocks的数据,会等待创建的管道1是否允许写入(写入时机由别的机制保证,这里略过),如果允许写入,就将数据写入这个管道中,然后将内存中写入部分的数据释放。

提示:这里需要注意,aof_buf与aof_rewrite_buf_blocks是两个数据结构,里面的数据也是两份,不是公用一份。

因此重写阶段,数据变更会让主进程将这些数据在内存中存储两份,这对主进程是额外的压力。

子进程拿到第一个KR了

我们把目光再次回到子进程身上。

此时它已经完成对原有数据的重写,拿到第一个KR,我们恭喜一下它~



现在我们知道了Rewrite期间变化的数据,是会通过管道通知的。那么子进程是如何处理的么?

  点点滴滴,聚水成河

其实在“子进程”章节中重写旧数据时,就开始处理了。子进程在重写旧数据时,会时不时的读取一下管道。

rdbSaveRio方法中

0ffc97c6924761ceb8ae6a25e8ba9980.png

rewriteAppendOnlyFileRio方法中

bef8ca99f60ce67ca41a8dbf7b2014d8.png

这些读取出来的数据都会保存在aof_child_diff的数据结构中。



这样旧数据会直接写入到aof重写文件中,期间变化的数据会保存在内存aof_child_diff中,数据顺序就不会混乱了。最后把这部分变化的数据统一写入待aof重写文件中即可。

  最重要的是向上管理

子进程在完成第一个KR,重写了旧数据之后,就马不停蹄的开展了下一项工作。从管道1中读取变化的数据。

1851fd2ef6547942eef29528ab947522.png

这里有两个点需要注意,一个是主进程可能一直在接受新的数据,这就导致通道1中永远有数据,子进程就无限制的读取数据,这肯定是不能接受到,因此,限制了最多只会读取1s。

另一个,如果一直没有数据,也没必要一直等啊,大家时间都很宝贵的,如果20毫秒没有数据,那么子进程也就不会读取了。



下面就到了关键一步,要通知主进程自己的工作已经完成了80%,进行向上管理了。

f7b3c2b51b4181d62c78f83c148024af.png

子进程向通道2写入一个!号,通知主进程,请停止向管道1当中写入数据

677883e4f7076146baf511049e97f22f.png

  主进程接受会邀

9f1a9b87214f1a3e8607e16d88a399ae.png

主进程在接受到通道2的通知之后,将aof_stop_sending_diff设置为1,变化的数据仍旧进入aof_rewrite_buf_blocks中,但是不会在写入通道1中了,全部停留在自己的内存中。

主进程接着向通道3中写入一个!,表明自己停止写入数据。

  临门一脚

8f24274ccb4efe371501d641f0568323.png

子进程收到通知后,最后将通道1中的数据全部读取出来,然后刷入磁盘,并将aof重写文件重写命名为temp-rewriteaof-bg-{pid}.aof,然后自己退出进程。



这里的写入,就是指将内存中的aof_child_buf中的数据一股脑的写入文件当中,然后随着进程退出,内存一并释放。

主进程:我来兜底!

f342c25e8d7927d7fc0b66bf446ffe49.png

此时子进程已经完成了它的工作,退出了进程,主进程在serverCron中发现子进程已经不存在了之后,就调用backgroundRewriteDoneHandler方法,处理善后的工作。

  1. 将之前aof_rewrite_buf_blocks还存在的数据,写入aof重写文件之中。(temp-rewriteaof-bg-{pid}.aof)

  2. 将aof文件设置为重写文件,重写aof正式转正,旧文件退休。



道由白云尽,春与青溪长

到此为止,一次完整的AOF Rewrite重写就结束了。



纵观整个过程,我们可以看到,核心的重点是如何处理AOF Rewrite期间变化的数据。

为了保证这部分的数据正确,Redis 5.0 版本总共使用了两个内存结构存储(主进程的aof_rewrite_buf_blocks,子进程的aof_child_diff),两个磁盘IO(主进程写入旧AOF文件,子进程写入新AOF重写文件),三个通信管道,双倍的CPU开销来完成。

有兴趣的同学可以了解下还未正式发布的Redis 7.0,Multi Part AOF的实现。这个版本的实现完美解决了上述的冗余问题。



好的,今天的分享就到此为止啦,感谢大家。

团队介绍

我们是大聚划算技术团队。

使命:让货品和心智运营变得高效且有确定性!

愿景:与运营、产品合力,打造最具价格优惠心智的购物入口,最具爆发性的营销矩阵。

职责:负责支持聚划算、百亿补贴、天天特卖等业务。我们聚焦优惠和选购体验,通过数智化驱动形成更有效率和确定性的货品运营方法论,为消费者提供精选和极致性价比的商品,为商家提供更具爆发确定性的营销方案。

这是一支极具业务 sense,又具备复杂业务系统架构和设计经验的团队。

✿  拓展阅读

de5a37c23d7eb3db621b365f8e47d613.png

058933042c070ba0ca00062093f411b3.png

作者|时昼

编辑|橙子君

d55b8a9e90813f9388decb29b795e10f.png

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值