总述
本文章主要记录redis的两种持久化方式:RDB和AOF,知识来源于编程迷思大佬的博客和钱文品大佬的《Redis深度历险》,为两位巨人的知识创作点赞,如果本博客有表述不清楚的地方,可以前去这两个地方详细阅读;
持久化的功能:redis是内存型数据库,数据都存储在内存中,为了避免进程退出导致数据的永久丢失,需要定期将redis中的数据以某种形式(数据或者命令,分别对应两种持久化方式)从内存中保存到硬盘中去,当下次redis重启或者发生进程异常恢复的时候,利用持久化的文件就可以实现数据的恢复,除此之外,为了进行灾难备份,可以将持久化文件拷贝到一个远程位置;下面分别介绍RDB持久化和AOF持久化方式;
RDB持久化
RDB持久化是将当前内存中的数据生成快照保存到硬盘中(也称之为快照持久化),生成的持久化文件的后缀是.rdb,当redis重启的时候,启动进程可以读取文件进行数据恢复;
1.触发条件
RDB持久化触发分为手动触发和自动触发两种,下面分别来详细了解一下rdb备份的两种触发方式
(1)手动触发
手动触发rdb的命令有:save/bgsave两种,因为save命令在执行rdb备份的同时会阻塞redis的主进程,直到rdb文件创建完毕为止,因此已经逐渐被废弃,在redis服务器的阻塞期间,不能够处理任何请求;
而bgsave其实会fork出一个子进程,由子进程来负责创建RDB文件,父进程则继续处理请求;需要注意的是父进程在fork子进程的阶段是阻塞的,也就是说fork子进程的过程中redis是不能继续响应客户端的请求的;
(2)自动触发
save m n
自动触发最常见的情况是在配置文件中配置 save m n, m和n这两个参数的意思是当m秒发生n次的redis数据库变更的时候,会触发RDB备份;
我们可以看一下redis.conf的配置文件里关于这个的配置:
可以看到这个配置是900秒内有1次变动或者300秒内10次变动或者60秒内10000次变动都会触发bgsave进而发生RDB数据备份,而且配置文件中的三个配置只要任意一个满足,就会发生bgsave的备份;
save m n的实现原理解析:
实现save m n 需要三个前置能力: serverCron + dirty计数器 + lastsave时间戳
可能我写到这里聪明的读者应该已经猜到了,serverCron可以理解是redis服务器的周期性操作函数,默认是100ms执行一次,该函数对服务器的状态进行维护,其中的一项工作就是检查save m n条件是否满足,如果满足就执行bgsave
dirty计数器顾名思义,其实是用来对redis数据库修改次数进行计数的,它记录了上次bgsave/save之后,服务器执行了多少次的增删改查,因为是自上次bgsave之后的,所以dirty再每次bgsave之后都会重置为0;
需要注意的是,服务器记录的是数据发生了多少数据的修改,而不是客户端提交了多少次修改,mset key1 value1 key2 value 2 也算两次的;
最后是lastsave的时间戳,记录的是上次成功执行save/bgsave的时间戳;
将这个三个能力串起来我们就可以得到save m n 的工作流程:
每间隔100ms,服务器的定时任务函数serverCron()就会执行,然后去检查save m n条件是否满足,具体检查的条件是: dirty >= n && currentTime - lastsave>= m 这样子;
ps:个人猜测 serverCron()函数是不是也用来处理键的过期时间? expire?
(3)其他触发机制
还有一些其他的case会触发bgsave:
- 主从复制场景下,如果从节点要求执行全量复制或者主节点发现只能进行全量复制,这时候主节点的redis主进程会fork子进程执行rdb的全量备份,然后将rdb文件发送给从节点
- redis执行shotdown的时候,会自动执行rdb备份进行持久化;
RDB的完整执行流程
看主进程的链路,fork出子进程之后就会继续去处理其他命令,fork的时候子进程会执行内存的copy,获得主进程fork时内存的全部的状态,然后子进程利用这部分内存的镜像来产生rdb的文件,copy完成之后会去通知主进程,整个流程执行完毕
但是这里有个问题,那就是在执行fork之后子进程产生rdb文件之前的客户端指令是没有办法被子进程感知到的,那这部分数据变更怎么处理呢?
RDB文件的文件格式
RDB文件是经过压缩的二进制文件,下面梳理一些rdb文件的相关细节;
存储路径
rdb文件的存储路径既可以在启动前配置,也可以通过命令来动态指定;
dir用来配置指定目录,dbfilename用来指定文件名;
RDB的文件格式
rdb文件格式如下图所示:
开头是REDIS五个字符常量,类似于.class文件中的cafebabe哈哈
然后是db_version,RDB文件的版本号,注意并不是redis的版本号
之后是SELECTDB 0 pairs;表示一个完整的数据库,redis数据库即使是单机的情况下,,仍然是分成了好多分的,类似于kafka中的分区的概念吧,如果一个指定号的数据库没有数据的话,是不会出现在rdb文件中的,所以我们可以看到只有0号数据库和3号数据库中有数据,也只有他们出现在redis的rdb文件中;
pairs中就是存储的具体的redis的键值对数据,EOF表示RDB文件的结束描述符,而check_sum是文件校验和;
启动时加载
RDB文件的载入工作是在服务器启动的时候自动执行的,并没有专门的命令,但是因为AOF的优先级更高,所以当AOF开启的时候,redis会优先载入AOF来恢复数据,只有当AOF关闭的时候,才会在Redis服务器启动时检测RDB文件并自动载入,服务器在载入rdb文件期间处于阻塞状态,直到载入完成为止;
RDB配置总结
本来这里我不打算记录了,但是考虑到后续工程中可能会着手去配置一些redis的相关的配置项,记录一下混个眼熟吧
- save m n:bgsave自动触发的条件;如果没有save m n配置,相当于自动的RDB持久化关闭,不过此时仍可以通过其他方式触发
- stop-writes-on-bgsave-error yes:当bgsave出现错误时,Redis是否停止执行写命令;设置为yes,则当硬盘出现问题时,可以及时发现,避免数据的大量丢失;设置为no,则Redis无视bgsave的错误继续执行写命令,当对Redis服务器的系统(尤其是硬盘)使用了监控时,该选项考虑设置为no
- rdbcompression yes:是否开启RDB文件压缩
- rdbchecksum yes:是否开启RDB文件的校验,在写入文件和读取文件时都起作用;关闭checksum在写入文件和启动文件时大约能带来10%的性能提升,但是数据损坏时无法发现
- dbfilename dump.rdb:RDB文件名
- dir ./:RDB文件和AOF文件所在目录
AOF持久化
aof是区别于rdb的完全不同的持久化方式,rdb的话是将内存中的数据进行快照化的存储,aof的话就是将客户端执行的每条指令进行持久化存储,两者是完全不同的两个思路;
先说结论:和RDB相比的话,AOF的实时性更好,因此已经成为了主流的持久化方案
有咩有想过为什么呢?
1.开启AOF
redis服务器默认开启RDB,关闭AOF,如果要开启AOF,需要在配置文件中配置
appendonly yes
2.执行流程
由于需要记录Redis的每条写命令,所以AOF不需要触发,AOF的主要执行流程包括下面三个阶段:
- 命令追加,就是持续将客户端的写入追加到aof_buf的缓冲区中
- 文件写入和文件同步;根据不同的同步策略将aof_buf缓冲区中的数据同步到磁盘中去;
- 文件重写,非必须,定期重写AOF文件,可以降低文件体积和执行耗时;
(1)命令追加
redis是将写命令追加到缓冲区中,而不是直接写入到文件,主要是为了避免每次有写命令的时候都直接写入到硬盘,导致磁盘io成为redis负载的平静;在AOF文件中,除了用于指定数据库的select命令(比如select 0 指的是选中0号数据库)是又redis添加的,其余的都是客户端发送过来的写命令;
(2)文件写入 (write)和文件同步(sync)
redis提供了多种AOF缓存区的文件同步策略,策略涉及到操作系统的write函数和fsync函数,接下来看下这两个函数的解释说明:
为了提高文件写入效率,在现代操作系统中,当用户调用套接字对应的文件描述符执行write写入操作时,操作系统并不会真的立刻将内容写入到磁盘中去,而是先写入到一个叫内存缓冲区的区域里,当缓冲区满或者超过了指定的时间限制后,才会真正的将缓冲区的数据写入到硬盘里。这样虽然能够一定程度上提高效率,节省磁盘IO,但是存在于写入缓冲区的内容一旦系统崩溃之后就会有数据丢失的风险;所以操作系统也同时提供了fsync,fdatasync等同步函数,执行指令可以立刻将缓冲区的数据刷写到磁盘中去,确保数据的安全性;
在redis的conf文件中也提供了AOF命令刷写到磁盘的配置选项,即appendfsync,它一共有三个可以选择的配置项:
- always,每一条写入指令写完aof_buf之后会立马调用操作系统的fsync函数写入到磁盘中完成AOF文件同步,完成后线程返回,这种配置就是每一个写命令都会调用磁盘写IO,所以磁盘成为了redis性能瓶颈;
- no: 命令写入aof_buf之后调用系统的write函数,不对AOF做fsync的同步,即同步的时间redis选择不控制,而是完全交给操作系统来做,这样缓冲区中挤压的数据是最多的,效率虽然最高,但是数据丢失的风险也最大
- everysec:命令写入aof_buf之后,调用write操作,write写完之后返回,fsync文件同步操作由专门的线程每秒调用一次,everysec是前面两种策略的折中,是redis推荐的配置;
(3)文件重写
首先,文件重写的存在有意义吗?是有意义的,为什么呢?
- 过期的数据可以不再写入文件
- 无效的命令或者多条命令重复的对一个内存区域的写入变更可以重写合并为一条
- 无效的命令可以不必再写入
既然确定了它是有意义的操作,我们再来看下它的触发条件:
<1 手动触发
执行bgrewriteof命令,可以立马重写aof文件,过程和rdb的备份类似,fork紫禁城进行具体的工作,且只有在fork的时候会阻塞;
<2 自动触发
自动触发也和rdb比较类似,需要配置两个参数,当策略判断条件满足的时候就会触发
auto-aof-rewrite-min-size: 指的是执行aof重写时候,文件的最小体积,默认64m
auto-aof-rewrite-percentage:指的是执行aof重写的时候,当前aof文件大小和上次aof文件大小的比值,推荐配置00%
因此,当这两个参数的条件同时满足的时候,redis会自动fork子进程来完成aof文件的重写;
AOF文件重写流程如下:
这个流程有需要注意的地方,也是前面rdb备份的流程里面我有疑问的地方;
fork子进程的时候,子进程所能看到的内存区域仅仅是fork指令发生之前的父进程的内存区域,所以在fork之后,aof文件重写之前的指令改动是没有办法看到的;
fork操作返回之后,redis主进程继续执行,相应客户端的变更操作,这时候aof_buf仍然可以接受写入,按照配置号的appendfsync的策略,完成磁盘的落盘,但是对于子进程而言,后续的这部分改动它看不见了呀,所以父进程需要想一个策略来区分出这部分增量的改动的数据,那就是利用 aof_rewrite_buf,后续增量的数据除了写入aof_buf之外,还会再写一份到aof_rewrite_buf中去,当子进程的aof文件重写完成之后,会发送信号量给父进程,父进程再根据aof_rewrite_buf中的增量数据,写入到新的aof文件中去,然后使用新的aof文件替换旧的,就完成整个aof重写过程;
启动时加载
redis默认的持久化策略是rdb,但是从优先级上来讲aof的优先级感觉又高于rdb,为什么呢?
当aof开启时,会执行aof的文件加载来进行数据恢复,只有当aof关闭时,才会选择去找rdb文件;
当aof开启 但是aof文件不存在的时候,即使rdb文件存在也不会去加载;
aof文件也又一定的校验机制,如果检测aof文件不完整的话,redis server会报错,但是如果
aof-load-truncated参数开启的话,redis server会忽略aof文件的尾部,启动成功,也就是说这个参数一旦开启,即使文件最终损坏也会去执行加载,只不过加载的时候会忽略后续错误的文件格式;
aof文件存在的时候是如何恢复的?
因为redis的命令只能够在客户端上下文中执行,而载入aof文件是直接从文件中获取的,并不是客户端发送的,所以,redis服务器在载入aof文件之前,会创建一个没有网络连接的客户端,之后用这个傀儡客户端来完成指令的发送,server接受了这些指令之后就会执行数据的修改操作,最终数据的恢复和同步了;
PS:这个过程好像乾隆老师和和珅啊,乾隆老师想贪污,它自己不说出来,然后创建一个傀儡和珅,然后让和珅搞钱交给自己,补充自己的小金库!
两种持久化策略的选择
这里我就不详细的补充持久化策略的选择规则了,各位读者应该根据自己的业务场景来灵活的判断是应该采用rdb备份还是aof备份,如果你的业务场景对十几分钟的数据丢失无所谓,那么可以高优考虑rdb备份,毕竟rdb备份性能相对较好,如果业务场景只能接受秒级别的数据丢失,那么就只能采用aof的数据备份策略了;
通常我们会对redis 服务器做主从,这时候推荐的配置是,主redis服务器不执行任何的备份策略,而从服务器上用aof进行备份,并且可以关闭aof自动重写,添加定时任务,在每天redis的闲时执行重写操作,这样子基本可以实现性能最优;(主redis不用任何持久化策略性能最好,从redis执行持久化且定期执行兼顾数据安全和性能)
完结,撒花!
参考:
《redis深度历险》
《编程迷思:深入学习redis2:持久化》