目录
认识持久化
我们都知道Mysql有四大特征,原子性,持久性,隔离性,一致性。其中持久性就是因为mysql会把数据存到硬盘中,即使计算机掉电数据也不会丢失。如果把数据存储到内存中,那么就是不持久的了。而我们的redis刚好就是这样,所以要想让redis持久化,就要想办法把redis的数据存储到硬盘上。
但是redis的一个很重要的特点就是效率高,而效率高又离不开将数据存储到内存,所以该怎么办呢?答案就是‘我全都要’,将数据同时存储在内存和硬盘中,当重新打开机器后只需要从硬盘中把数据读取到内存即可,这样既保证了效率右让redis可以持久化储存数据,不过这样的方法必然会付出更大的硬盘资源。
持久化方案
redis提供的持久化方式主要有两种RDB和AOF
RDB(定期备份):redis会定期将内存里的所有数据都写入硬盘中,并生成一个快照。
AOF:redis会将每个写操作记录下来当重启后会将这些操作自动再执行一遍
RDB(Redis DataBase)
redis会定期将内存里的所有数据都写入硬盘中,并生成一个快照。这个快照就类似于一张照片,将这一时刻的数据和状态以文件的形式写到硬盘上。
这个定期又分为两种方式手动触发和自动触发
手动触发
程序员通过特定的命令来手动触发快照
- save:当redis执行save操作时,就会全力以赴的进行快照生成,也就会阻塞其他redis的客户端命令,就可能会使redis直接阻塞(一般不建议使用)
- bgsave(background save):这个操作的效果和save一样,但是不会阻塞其他命令,因为此处redis采取了多进程的方式,会单独生成一个子进程来完成快照生成的操作,而父进程则继续处理客户端的请求
bgsave的执行过程
- 首先判定当前是否还存在其他的子进程,如果已经在执行一个bgsave命令的话,这个bgsave就会直接返回,保证只有一个子进程
- 如果没有其他子进程,就会通过fork操作创建出一个子进程来
- 子进程复制写文件,生成快照的过程,而父进程则是继续接受客户端请求
- 最后zjc完成持久化后,会通知父进程,父进程更新一些统计信息后,子进程就可以销毁了
//fork是linux提供的创建子进程的api,如果在其他系统上则行不通,fork会直接将父进程克隆一份,包括pcd,内存中的数据,文件描述符表等等,所以克隆出来的子进程就和父进程是完全一样的,此时就可以让父进程继续处理客户端请求,让子进程去进行持久化操作,因为内存数据完全一样,所以将子进程持久化就相当于持久化了父进程。而克隆完后的两个进程是互相独立,互不干扰的。
但是你会不会有疑问,完全拷贝下来如果有100G的内存那么开销岂不是很大?实际上,redis这里并不是直接无脑的拷贝,而是利用’写时拷贝‘,也就是如果子进程和父进程的内存数据完全一样,那么这两个进程共用一份内存,也就是不会触发真正的拷贝操作,只是在效果上像是两份,当有一方对内存数据进行了修改才会触发真正的拷贝操作,比如当fork拷贝进程时父进程来了一条指令对数据进行了修改,那么就会将数据拷贝到子进程。
举个例子假如父进程有一个int i = 1;那么子进程并不会拷贝而是和父进程共用这个数据的内存地址,如果在拷贝的过程中父进程发生了修改变成了int i = 2;那么fork就会触发真正的拷贝,将原来的int i = 1复制到一个新的内存空间,而原来的则被修改为int i = 2。(一般整个持久化过程很快,父进程不会有很多数据发生改变)
这样既不会有很大的开销,也让这两个进程可以互相独立。
当子进程写入数据时会把数据写入redis的工作目录里的dump.rdb文件,这个文件是rdb机制生成的镜像文件,而且只一个二进制的文件无法用一般的文本编辑器查看,当写入完成后就会把新的rdb文件替换旧的rdb文件。如果是save则不会触发文件替换而是直接在当前进程中往刚才的文件中写入数据
具体过程是当执行rdb镜像操作时,会把要生成的快照数据,先保存到一个临时文件中,当快照整成完毕后,再删除之前的rdb文件,把新生成的临时的rdb文件名字改成刚才的dump.rdb文件
自动触发
RDB会在Redi数据发生修改时自动保存快照进行持久化操作,不过生成一次rdb快照是一次成本比较大的操作,所以肯定不能每次修改都生成一次快照。redis是根据配置文件中的save选项来决定什么时候自动触发持久化操作的,当达到选项中的条件后redis就会自动进行快照生成。
解释:
save选项有三个,分别是
- 距离上次更新超过了900秒在这段时间内进行1次修改操作后进行自动触发
- 距离上次更新超过了300秒在这段时间内进行10次修改操作后进行自动触发
- 距离上次更新超过了60秒在这段时间内进行10000次修改操作后进行自动触发
//如果给save设置成' ',就代表取消自动触发
从选项中可以看到,最快的生成快照的时间是60秒,换句话说redis每两次快照之间的最短时间是60秒,假如现在完成一次快照生成,此时快照和内存的数据是一样的,但是redis突然进行了大量的修改,不过时间还没到60秒,如果这时redis突然挂了或者机器掉电,那么在这段时间内的数据因为还没来得及进行持久化操作就会直接丢失掉。
//如果是通过正常流程重新启动redis服务器,此时redis服务器会在退出的时候自动触发生成rdb操作,但是如果是异常重启(kill-9或者服务器掉电)此时服务器来不及生成rdb,内存中尚未保存到快照中的数据就会随着重启丢失
小结
- RDB是一个紧凑的二进制文件,代表redis某一个时间点上的数据快照,非常适用于备份,全量复制等场景。比如固定时间执行bgsave,并把RDB文件复制到远程机器或者文件中用于灾难恢复
- redis加载RDB文件的速度比AOF的方式快,因为RDB使用的是二进制的方式组织数据,可以直接把数据读到内存中,按照字节的格式取出来,而AOF则是使用文本的方式。
- RDB没法做到实时持久化,因为redis每次都要执行fork创建子进程,开销较大,也因此RDB可能会有数据丢失的风险,当数据来没来得及持久化redis就挂掉或者掉电,这部分数据就会丢失
AOF(Append-Only File)
AOF是Redis的一种替代的、完全持久化的策略。AOF机制的作用是,每当redis客户端执行写操作时,都会被记录再aof文件里通过特殊符号区分,如果发生服务器挂掉或者掉电后重启,redis就可以通过将这些命令再执行一遍而达到恢复数据的效果。
AOF的启动需要在配置文件里将appendonly.aof选项改成yes默认情况下是no,而且当启动aof机制时rdb机制是关闭的
AOF缓冲区刷新机制
引入AOF之后,redis既要写内存,又要写硬盘会不会影响效率?
AOF并不会对效率有太大的影响,实际上AOF机制并不是直接将数据写入硬盘,而是先写入内存中的一个缓冲区,积累了一定量的数据后再一次性写入硬盘,这样做就大大降低了redis写硬盘的次数(写硬盘的次数要比一次写多少数据对效率的影响大得多)。而且AOF是把每次的操作写在文件的末尾是顺序写入,要比随机写入快得多。
虽然通过刚刚的方法大大提高了AOF的效率,但是同时也带来了一个问题,缓冲区说到底还是在内存中,如果AOF正在将指令写入缓冲区时,redis挂掉或者掉电,那么此时缓冲区中的数据还没来得及写入硬盘中,也就全部丢失掉了。虽然缓冲区的方法可以提高效率,但是相应的也会来带来一些数据可靠性的问题,所谓鱼和熊掌不可兼得。
Redis提供了多种AOF缓冲区刷新策略,由参数appedfsync控制,可配置项如下:
- always:每写入一个命令到缓冲区,就放入硬盘,刷新缓冲区的频率最高,数据可靠性最高,性能最低
- everysec(默认):每秒清理一次缓冲区,刷新缓冲区频率低一些,数据可靠性也降低,性能相应的也提高了
- no:redis不进行刷新缓冲区,由操作系统自行刷新,刷新频率最低,数据可靠性最低,性能最高
AOF重写机制
随着我们的服务器一直运行,aof文件也会越来越大,虽然这个文件大小对aof效率没有什么影响,但是会影响redis的开机速度,redis启动就会读取这个文件里记录的指令操作如果文件较大,执行的操作就变多,启动之间也就变长。虽然aof文件变大是不可避免的,但是这个文件里会有很多多余的内容。比如
如果redis客户端执行指令,lpush key aaa,lpush key bbb,lpush key ccc,就相当于只执行了lpush key aaa bbb ccc。再或者一些数据插入后又删除,这样的操作对于结果显然是没用的,而redis启动时却会执行一遍,也就拖长了启动时间。所以实际上我们的aof不在乎中间过程,只在乎最终的数据结果。
所以aof就针对上面问题做出了应对,也就是重写机制,可以将aof中一些多余的操作指令给剔除掉,并且合并一些操作,从而缩小aof文件。
重写机制的触发方式也分为手动触发和自动触发
- 手动触发:调用bgrewriteaof命令redis就会对文件进行重写
- 自动触发:根据auto-aof-rewrite-min-size和auto-aof-rewrite-percentage的参数决定触发时机,auto-aof-rewrite-min-size表示触发重写时AOF的最小文件大小(默认是64mb)。auto-aof-rewrite-percentage代表AOF占用大小相比上次重写时增加的比例。
AOF重写流程
aof重写的方法和rdb很像,也是先fork一个子进程,子进程负责AOF文件的重写父进程依旧继续负责处理redis客户端的命令。但是我们重写的时候并不关心当前aof文件里都有啥,而只在乎内存中的数据状态,子进程只需要获取当前内存中的数据,然后以aof的形式写入一个新的aof文件,然后替换掉旧的文件即可。
这里的方法和RDB的快照也挺像,都是记录当前时间点的内存状态。只不过RDB是以二进制的形式,而AOF是以文本形式。
在fork创建子进程的一瞬间,子进程就继承了父进程当前的内存状态,但是重写也要一定时间,可能在重写的时候redis客户端会对内存进行一些修改
换句话说,我们的子进程记录的是fork之前的内存,但是fork之后的数据并不会丢失,父进程会再准备一个缓冲区aof_rewrite_buf当创建完子进程后,父进程就会将一些aof数据先存储到这个缓冲区。
子进程完成重写后会通知父进程,然后父进程再将aof_rewrite_buf中的数据写入新的aof文件,最后再让新的aof文件代替旧的aof文件
假如在redis正在重写的时候,又来了一个重写操作会怎么样?
答案是,新的重写操作会直接返回不执行,而且如果在要进行AOF操作时发现redis正在进行RDB快照操作,那么aof将会阻塞等待,等到RDB操作完成后再启动AOF操作。
父进程在重写的时候依然维护写入旧的aof文件是否有必要?
答案是,有必要因为假如redis在进行重写操作时,这时redis服务器挂掉了的话,新的aof文件还没有写入完全,缓冲区里的数据也都丢失,如果旧的aof文件再被销毁的话就会造成一部分数据的丢失。
混合持久化
虽然aof是以文本方式写入,但是当我们查看aof文件时会发现这里是用二进制存储,这是因为aof原本按照文本方式写入文件但是这样的成本较高,于是redis就引入混合持久化,也就是同时结合了rdb和aof。
按照aof的方式每个请求操作都写入文件,触发aof重写后,就会把当前内存状态按照rdb的二进制格式写入到新的aof文件中,后续在进行的操作还是按照aof文本的方式追加到文件后面。
混合持久化在配置文件里默认开启,当同时存在aof文件和rdb快照时,则以aof为主