Redis 5.x ——数据持久化(RDB AOF)
数据持久化(RDB & AOF)
- 1、RDB方式持久化
- 1.1、save触发方式
- 1.2、bgsave触发方式
- 1.3、后台触发
- 1.4、RDB备份的应用场景
- 2、AOF方式持久化
- 2.1、AOF持久化流程
- 2.2、相关参数配置
- 2.3、重写操作
- 3、AOF和RDB的数据恢复顺序
- 4、RDB和AOF持久化的优缺点
- 4.1、RDB持久化的优缺点
- 4.2、AOF持久化的优缺点
- 5、AOF+RDB混合模式配置
Redis是一种内存型数据库,即服务器在运行时,系统为其分配了一部分内存来存储数据,当服务器突然宕机了,那么数据库里面的数据将会丢失,为了应对这种情况,必须通过持久化的方式将数据从内存保存到磁盘中,如图所示。
Redis持久化存储有两种持久化方案:RDB(RedisDataBase)和AOF(Append-Only File)。其中,RDB是将内存中数据的快照存储到磁盘内,AOF则是通过日志记录Redis内的所有操作。
Redis 4之后支持AOF+RDB混合持久化的方式,结合了两者的优点,可以通过aof-use-rdb-preamble配置项开启混合持久化功能的开关。
Redis提供了不同范围的持久化选项:
- RDB持久化按指定的时间间隔存储数据集的时间点快照。
- AOF持久化会记录服务器接收的每个写入操作,这些操作将在服务器启动时再次“播放”,以重建原始数据集。使用与Redis协议相同的格式记录命令,并且仅采用追加方式。当日志太大时,Redis会在后台重写日志。
Redis持久化的RDB和AOF示意图,如图所示。
1、RDB方式持久化
RDB是将Redis内存中数据的快照存储在磁盘内,是Redis的默认持久化方案。RDB持久化默认有三种策略,可在redis.conf中配置,会以一段时间内达到指定修改的次数为规则来触发快照操作,快照文件名为dump.rdb。每当Redis服务重启的时候都会从该文件中把数据加载到内存中。
RDB持久化除了可以根据配置中的策略来触发外,还可以使用save和bgsave命令手动来触发。这两个命令的区别在于save会阻塞服务器进程。在执行save命令的过程中,服务器不能处理任何请求,但是bgsave(background save,后台保存)命令会通过一个子进程在后台处理数据RDB持久化。本质上save和bgsave调用的都是rdbSave函数,所以Redis不允许save和bgsave命令同时执行,当然这也是为了避免RDB文件数据出现不一致性的问题。
1.1、save触发方式
save命令的源码如下:
voidsaveCommand(client*c){// bgsave命令执行时不能执行save命令if(server.rdb_child_pid!=-1){addReplyError(c,"Background save already in progress");return;}rdbSaveInfo rsi,*rsiptr;rsiptr=rdbPopulateSaveInfo(&rsi);// 调用rdbSave函数进行备份if(rdbSave(server.rdb_filename,rsiptr)==C_OK){addReply(c,shared.ok);}else{addReply(c,shared.err);}}save命令将内存数据的镜像保存为RDB文件。由于Redis是以单线程方式执行命令,因此save命令执行期间会阻塞Redis服务进程,Redis服务不再处理任何命令,直到RDB文件创建完成为止。一般不建议使用save命令执行持久化。Redis执行save命令时会阻塞其余客户端的命令,如图所示。
1.2、bgsave触发方式
执行bgsave命令时Redis服务器的处理流程如图所示。
bgsave命令使用CopyOnWrite机制来执行写时的复制操作,由一个子进程将内存中的最新数据依次写入临时文件,此时父进程仍旧处理客户端的操作,当子进程执行完毕后再将该临时文件重命名为dump.rdb(替换掉原来的dump.rdb文件)。所以,无论bgsave命令是否执行成功,dump.rdb都不会受影响,因此建议使用bgsave命令。
bgsave命令的源码如下:
voidbgsaveCommand(client*c){intschedule=0;// 当AOF正在执行时,bgsave会在AOF完成之后执行if(c->argc>1){// 参数只能是"schedule"if(c->argc==2&&!strcasecmp(c->argv[1]->ptr,"schedule")){schedule=1;}else{addReply(c,shared.syntaxerr);return;}}// bgsave命令正在执行时,不会执行save命令if(server.rdb_child_pid!=-1){addReplyError(c,"Background save already in progress");}elseif(server.aof_child_pid!=-1){if(schedule){server.rdb_bgsave_scheduled=1;addReplyStatus(c,"Background saving scheduled");}else{addReplyError(c,"An AOF log rewriting in progress: can't BGSAVE right now. ""Use BGSAVE SCHEDULE in order to schedule a BGSAVE whenever ""possible.");}}// 否则调用rdbSaveBackground执行备份操作elseif(rdbSaveBackground(server.rdb_filename,NULL)==C_OK){addReplyStatus(c,"Background saving started");}else{addReply(c,shared.err);}}执行bgsave命令时Redis还能继续处理客户端的操作,如图所示。
随即在后台开始处理此命令,如图所示。
1.3、后台触发
如下是后台触发RDB持久化的配置文件:
// 定时持久化规则save9001save30010save6010000# 默认值为yes,当启用了RDB且最后一次在后台保存数据失败,Redis是否停止接收数据: # yes代表可以继续写入数据;no代表不会写入成功,通知用户持久化出现错误 stop-writes-on-bgsave-error yes # 持久化的数据是否进行压缩 rdbcompression yes # 存储的快照是否进行CRC64算法的数据校验,如果希望获取到最大的性能提升,可以关闭此功能 Rdbchecksum yes # 设置快照的文件名,默认是dump.rdb dbfilename dump.rdbRDB持久化策略默认有三种方式:
- 在60秒内有10000次操作即触发RDB持久化。
- 没有满足第一种条件时,在900秒内有1次操作即触发RDB持久化。
- 没有满足第二种条件时,在300秒内有10次操作即触发RDB持久化。
同样,我们可以手动修改该参数或新增策略。
# 自定义持久化策略,在seconds秒内有changes次数据修改就触发RDB持久化 save <seconds> <changes>具体源码如下:
structsaveparam{time_tseconds;// 记录秒数intchanges;// 记录次数,每次执行完RDB操作之后清零};structredisServerserver;/* Server global state *//* RDB persistence RDB持久化相关的对象*/longlongdirty;/* 记录最后一次save之后的数据变化 */longlongdirty_before_bgsave;/* 用于在失败的bgsave上还原脏数据 */pid_trdb_child_pid;/* RDB进程的子进程号 */structsaveparam*saveparams;/* RDB操作存储数据点 */intsaveparamslen;/* 保存存储的点数 */char*rdb_filename;/* RDB文件名称 */intrdb_compression;/* 是否压缩RDB文件 */intrdb_checksum;/* 是否对RDB文件校验 */// 记录上一次save的时间time_tlastsave;/* 记录最后一次保存成功的时间 */time_tlastbgsave_try;/* 上次尝试保存的UNIX时间 */time_trdb_save_time_last;/* 上次RDB操作运行所用的时间 */time_trdb_save_time_start;/* 当前RDB操作开始时间 */intrdb_bgsave_scheduled;/* 标记是否可以执行RDB 操作 */intrdb_child_type;/* RDB 子进程操作类型 */intlastbgsave_status;/* 最后一次RDB操作的结果 */intstop_writes_on_bgsave_err;/* 如果无法保存,则不允许写入 */intrdb_pipe_write_result_to_parent;/* 用于返回RDB管道的状态*/intrdb_pipe_read_result_from_child;/* 无磁盘同步每个从节点的结果,用于子父 * 信息共享的管道和数据结构*/从上述源码可以看出计数器记录了在上一次成功的持久化后Redis执行了多少次写操作,其值在每次写操作之后都加1,在满足配置条件并成功完成持久化后记录就会清零。Redis还有一个周期性操作函数,默认每隔100ms执行一次,其中的一项工作就是检查自动触发bgsave命令的条件是否成立。
RDB文件结构如图所示。
1.4、RDB备份的应用场景
RDB是Redis中按时间点保存的紧凑数据文件,非常适合备份。比如,在最近的24小时内每小时将数据存档一次(保存为RDB文件),并在30天之内每天保存一次RDB快照,这样就可以在发生灾难时轻松把数据集还原到在不同时间点备份的数据。
RDB是一个紧凑文件,对于灾难恢复非常有用,并可传输到远程的数据服务中心。
RDB最大限度地提高了Redis的性能,因为Redis父进程为了持久化所做的唯一工作就是分派一个子进程,把其余的工作都交给子进程去做了,父进程永远不会执行类似于磁盘I/O这类操作,这样可以让Redis具有更快速还原数据的能力。
如果需要最大限度地减少数据丢失的可能性(例如在断电之后Redis停止了工作),那么使用RDB就非常适合。我们可以在生成RDB的时候设置不同的规则(例如,在至少五分钟之后对数据集进行100次写入,产生多个保存点),通常每隔五分钟或更长时间创建一次RDB快照。如果Redis在没有正确关闭的情况下停止工作,则最多丢失最近一次备份操作后的一些数据。
2、AOF方式持久化
RDB全量备份总是非常耗时的,而且不能提供强一致性(Strict Consistency),如果Redis进程崩溃,那么在最近一次RDB备份之后的数据也会随之消失。AOF(AppendOnly File)以独立日志的方式记录每次的写命令,可以很好地解决了数据持久化的实时性。系统重启时可以重新执行AOF文件中的命令来恢复数据。AOF会先把命令追加在AOF缓冲区,然后根据对应策略写入硬盘。
2.1、AOF持久化流程
AOF持久化流程示意图如图所示。
AOF的实现流程有三个步骤(append→write→fsync):首先append把命令追加到AOF缓冲区,然后write将缓冲区的内容写入程序缓冲区,最后fsync将程序缓冲区的内容写入文件。当AOF持久化功能处于开启状态时,服务器每执行完一个命令就会将命令以协议格式追加写入redisServer结构体的aof_buf缓冲区。而在服务重启的时候会把AOF文件加载到缓冲区中。
AOF有如表所示的三种触发机制。
- always:每次发生数据变更都会被立即记录到磁盘,性能较差,但数据完整性比较好。
- everysec:每秒钟将aof_buf缓冲区的内容写入AOF文件,如果宕机,就会有1秒内的数据丢失。
- no:将数据同步操作交给操作系统来处理,性能最好,但是数据可靠性最差。在配置文件中设置appendonly=yes后,若没有指定apendfsync,默认会使用everysec选项。
AOF持久化的源码如下:
// AOF定时触发的源码intserverCron(structaeEventLoop*eventLoop,longlongid,void*clientData){/*AOF延迟刷新:如果fsync速度慢,则在每个cron周期中尝试完成 */if(server.aof_flush_postponed_start)flushAppendOnlyFile(0);/*AOF写错误:在这种情况下,我们还有一个缓冲区要刷新, *如果成功,清除AOF错误,使数据库重新可写*/run_with_period(1000){if(server.aof_last_write_status==C_ERR)flushAppendOnlyFile(0);}}// 执行write和fsync操作voidflushAppendOnlyFile(intforce){ssize_tnwritten;intsync_in_progress=0;mstime_tlatency;// 如果没有数据,则直接返回,不需要刷盘if(sdslen(server.aof_buf)==0)return;// 使用bio_pending来判断是否有后台fsync操作正在进行if(server.aof_fsync==AOF_FSYNC_EVERYSEC)sync_in_progress=bioPendingJobsOfType(BIO_AOF_FSYNC)!=0;// 如果没有设置强制刷盘的选项,可能不会立即进行// 而是延迟执行AOF刷盘if(server.aof_fsync==AOF_FSYNC_EVERYSEC&&!force){if(sync_in_progress){if(server.aof_flush_postponed_start==0){server.aof_flush_postponed_start=server.unixtime;return;// 如果距离上次执行刷盘操作没有超过2秒,就直接返回}elseif(server.unixtime-server.aof_flush_postponed_start<2){return;}/* 如果后台还有 fsync 在执行,并且 write 已经推迟2秒以上 * 那么执行写操作(write 将被阻塞) * 假如此时出现死机等故障,可能会丢失2秒左右的AOF日志数据 */server.aof_delayed_fsync++;serverLog(LL_NOTICE,"Asynchronous AOF fsync is taking toolong(disk is busy?).Writing the AOF buffer without waitingforfsync to complete,this may slow down Redis.");}}// 将server.aof_buf中缓存的AOF日志数据写盘latencyStartMonitor(latency);nwritten=write(server.aof_fd,server.aof_buf,sdslen(server.aof_buf));latencyEndMonitor(latency);// 重置延迟刷盘时间server.aof_flush_postponed_start=0;// 如果write失败,那么尝试将该情况记入日志if(nwritten!=(signed)sdslen(server.aof_buf)){statictime_tlast_write_error_log=0;intcan_log=0;if((server.unixtime-last_write_error_log)>AOF_WRITE_LOG_ERROR_RATE){can_log=1;last_write_error_log=server.unixtime;}if(nwritten==-1){if(can_log){serverLog(LL_WARNING,"Error writing to the AOF file: %s",strerror(errno));server.aof_last_write_errno=errno;}}else{if(can_log){serverLog(LL_WARNING,"Short write while writing to ""the AOF file: (nwritten=%lld,""expected=%lld)",(longlong)nwritten,(longlong)sdslen(server.aof_buf));}// 尝试删除新追加到AOF中的不完整的数据内容if(ftruncate(server.aof_fd,server.aof_current_size)==-1){if(can_log){serverLog(LL_WARNING,"Could not remove short write ""from the append-only file. Redis may refuse ""to load the AOF the next time it starts. ""ftruncate: %s",strerror(errno));}}else{nwritten=-1;}server.aof_last_write_errno=ENOSPC;}// 处理写入AOF文件时出现的错误if(server.aof_fsync==AOF_FSYNC_ALWAYS){serverLog(LL_WARNING,"Can't recover from AOF write error when the AOF fsync policy is'always'.Exiting...");exit(1);}else{server.aof_last_write_status=C_ERR;// 如果已经写入了部分数据,是不能通过ftruncate进行撤销的// 通过sdsrange清除aof_buf中已经写入磁盘的那部分数据if(nwritten>0){server.aof_current_size+=nwritten;sdsrange(server.aof_buf,nwritten,-1);}return;}}else{if(server.aof_last_write_status==C_ERR){serverLog(LL_WARNING,"AOF write error looks solved, Redis can write again.");server.aof_last_write_status=C_OK;}}// 更新写入后的 AOF 文件大小server.aof_current_size+=nwritten;/* 当server.aof_buf足够小时,就重新利用内存空间,防止频繁的内存分配。 * 当server.aof_buf占据大量的内存空间时,采取的策略是释放空间。 */if((sdslen(server.aof_buf)+sdsavail(server.aof_buf))<4000){sdsclear(server.aof_buf);}else{sdsfree(server.aof_buf);server.aof_buf=sdsempty();}/* 如果no-appendfsync-on-rewrite选项为激活状态 * 并有bgsave或bgrewriteaof命令正在执行,那么就不执行fsync */if(server.aof_no_fsync_on_rewrite&&(server.aof_child_pid!=-1||server.rdb_child_pid!=-1))return;// 执行 fysncif(server.aof_fsync==AOF_FSYNC_ALWAYS){/* aof_fsync is defined as fdatasync() for Linux in order to avoid * flushing metadata. */latencyStartMonitor(latency);aof_fsync(server.aof_fd);/* Let's try to get this data on the disk*/latencyEndMonitor(latency);latencyAddSampleIfNeeded("aof-fsync-always",latency);server.aof_last_fsync=server.unixtime;}elseif((server.aof_fsync==AOF_FSYNC_EVERYSEC&&server.unixtime>server.aof_last_fsync)){if(!sync_in_progress)aof_background_fsync(server.aof_fd);server.aof_last_fsync=server.unixtime;}}在flushAppendOnlyFile函数中,通过write写盘之后根据apendfsync选项来执行刷盘策略:如果是AOFFSYNCALWAYS,就立即执行刷盘操作;如果是AOFFSYNCEVERYSEC,就创建一个后台异步刷盘任务。在bioCreateBackgroundJob函数中会创建bio后台任务,在bioProcessBackgroundJobs函数中会执行bio后台任务的处理。当flushAppendOnlyFile函数被调用时,可能会出现以下四种情况:
- save(保存)操作的执行时间未超过2秒,那么程序直接返回,并不执行write(写入)操作或新的save操作。
- 这个save操作的执行时间已经超过2秒,那么程序执行write操作,但不执行新的save操作。注意,这时执行write操作必须等待子线程先完成save操作,因此这里执行write操作会阻塞更长时间。
- 距上次成功执行save操作不超过1秒,那么程序执行write操作,但不执行save操作。
- 距上次成功执行save操作已经超过1秒,那么程序执行write操作和save操作。
执行save时的操作流程图如图所示。
下图所示是一个简单的AOF文件内容。
我们可以把此文件内容分解成两个命令:
- 第一个命令:包含两个参数,第一个参数6字节,为select;第二个参数1字节,为0;原始命令为“select0”。
- 第二个命令:包含三个参数,第一个参数3字节,为set;第二个参数4字节,为name;第三个参数4字节,为 clay;原始命令为“set name clay”。
2.2、相关参数配置
开启AOF持久化的相关配置:
# 开启AOF持久化 appendonly yes # AOF文件名 appendfilename "appendonly.aof" # AOF文件存储路径 dir "/opt/app/redis6/data" # aof文件比上次重写时增长100%(配置可以大于100%)时触发重写[auto-aof-rewrite-percentage 100 # aof文件大小超过64MB时触发重写 auto-aof-rewrite-min-size 64mb // aof 持久化策略,任选一个,默认是everysec # appendfsync always # appendfsync everysec # appendfsync no2.3、重写操作
在AOF持久化模式下,每个写命令都会追加到AOF文件。随着对数据库的不断操作,AOF文件会越来越大,为了避免AOF产生的文件太大,服务器会对AOF文件进行重写,将操作相同键(Key)的相同命令合并,从而减少该文件的大小。AOF文件重写流程如图所示。
重写流程解析如下:
- bgrewriteaof触发重写,判断是否有bgsave或者bgrewriteaof在运行,如果有,则等待该命令结束后再继续执行。
- 主进程复刻(fork)出子进程执行重写操作,保证主进程不会被阻塞。注:fork也可称为派生,分叉,分支等。这里指创建出子进程。
- 子进程遍历Redis内存中的数据同时写入新的AOF临时文件,在写入新文件的过程中,客户端的写请求在写入aof_buf缓冲区的同时也写入aof_rewrite_buf重写缓冲区,这是为了保证原AOF文件的完整以及新AOF文件生成期间的新数据修改操作不会被遗漏。
- 子进程写完新的AOF文件后,向主进程发信号,然后把aof_rewrite_buf中的数据写入新的AOF文件。
- 最后使用新的AOF文件覆盖旧的AOF文件,完成AOF重写。
AOF重写的源码如下:
defAOF_REWRITE(tmp_tile_name):f=create(tmp_tile_name)# 遍历所有数据库(16个db)fordb in redisServer.db:# 如果数据库为空,就跳过这个数据库ifdb.is_empty():continue# 写入 SELECT 命令,用于切换数据库 f.write_command("SELECT "+db.number)# 遍历所有键forkey in db:# 如果键带有过期时间,并且键已经过期,就跳过这个键ifkey.have_expire_time()and key.is_expired():continueifkey.type==String:# 用 SET key value 命令来保存字符串键 value=get_value_from_string(key)f.write_command("SET "+key+value)elif key.type==List:# 用 RPUSH key item1 item2...itemN 命令来保存列表键 item1,item2,...,itemN=get_item_from_list(key)f.write_command("RPUSH "+key+item1+item2+...+itemN)elif key.type==Set:# 用 SADD key member1 member2...memberN 命令来保存集合键 member1,member2,...,memberN=get_member_from_set(key)f.write_command("SADD "+key+member1+member2+...+memberN)elif key.type==Hash:# 用 HMSET key field1 value1 field2 value2...fieldN valueN命令来保存哈希键 field1,value1,field2,value2,...,fieldN,valueN=\get_field_and_value_from_hash(key)f.write_command("HMSET "+key+field1+value1+field2+value2+\...+fieldN+valueN)elif key.type==SortedSet:# 用 ZADD key score1 member1 score2 member2...scoreN memberN # 命令来保存有序集键 score1,member1,score2,member2,...,scoreN,memberN=\get_score_and_member_from_sorted_set(key)f.write_command("ZADD "+key+score1+member1+score2+member2+\...+scoreN+memberN)else:raise_type_error()# 如果键带有过期时间,就用 EXPIREAT key time 命令来保存键的过期时间ifkey.have_expire_time():f.write_command("EXPIREAT "+key+key.expire_time_in_unix_timestamp())# 关闭文件 f.close()3、AOF和RDB的数据恢复顺序
Redis服务重启时的数据恢复流程如图所示。
流程解析如下:
首先判断是否开启了AOF持久化,如果开启了AOF,则使用AOF持久化文件恢复数据,否则使用RDB持久化文件恢复数据。需要注意的是,若AOF文件和RDB文件都不存在,则直接启动Redis。
4、RDB和AOF持久化的优缺点
4.1、RDB持久化的优缺点
RDB持久化后的文件是二进制文件,更适用于备份、全量复制及灾难恢复,而且RDB恢复数据的性能要优于AOF恢复数据的性能。当数据库操作越来越频繁、数据量不断增大时,RDB需要经常复刻(fork)出子进程,让子进程将数据持久化在磁盘上。如果数据集很大,复刻子进程会比较耗时,若计算机的计算性能不佳,就很有可能导致Redis停止为客户端服务几毫秒甚至一秒钟。AOF也需要复刻出子进程,不过我们可以调整要重写日志的频率,这样就无须在持久化的选择上进行权衡。在默认情况下,RDB数据持久化实时性比较差,而配置为高时效性时,频繁操作的成本则会很高。
4.2、AOF持久化的优缺点
使用AOF持久化可以根据不同的fsync策略来备份数据,因为AOF采用的是追加的日志方式,因此即使断电也不会出现磁盘寻道或磁盘被损坏的问题。如果由于某种原因(磁盘已满或其他原因)日志只记录了一半,那么可以使用redis-check-aof工具轻松修复。当数据量太大时,Redis能够在后台自动重写AOF,并生成一个全新的文件,其中包含创建当前数据集所需的最少操作集,一旦准备好新的文件,Redis就会切换新的文件并开始把日志追加到新的文件。
AOF文件包含了所有操作的日志,而且很容易看懂,当用户不小心使用了flushall命令,flushall会把所有的数据删除,但是可以根据AOF文件找到错误的命令,把这些错误的指令删除,然后重新启动Redis,就可以恢复对应的业务数据。但是在此期间,AOF文件不能被重写,重写之后的AOF文件不再是可以让用户理解的内容。
AOF文件会以文本格式保存所有写操作命令,且未经压缩,因此对于同一数据集,AOF文件通常大于等效的RDB文件。
恢复数据时会重放(replay)AOF文件中的所有命令(即重新执行),因此数据恢复的性能要弱于RDB方式时数据恢复的性能。
RDB和AOF持久化的区别如表所示。
5、AOF+RDB混合模式配置
在掌握了上面两种持久化方式后,我们可以了解到,使用RDB持久化会有数据丢失的风险,但是数据恢复的速度快;使用AOF持久化可以保证数据的完整性,但数据恢复的速度慢。在Redis 4之后的版本新增了AOF+RDB混合模式,先使用RDB存储快照,然后使用AOF持久化记录所有的写操作,当满足重写策略或手动触发重写的时候,将最新的数据存储为新的RDB记录。重启服务时会从RDB和AOF两部分恢复数据,既保证了数据的完整性,又提高了数据恢复的性能。
开启混合模式后,在bgrewriteaof命令之后会在AOF文件中以RDB格式写入当前最新的数据,之后的写操作继续以AOF的追加形式追加写命令。当Redis重启的时候,先加载RDB的部分再加载剩余的AOF部分。AOF+RDB混合模式持久化的流程图如图所示。
开启混合持久化模式后,重写之后的AOF文件和RDB文件都存储的是二进制数据,继续往Redis中进行写操作时,在AOF中仍然以追加的方式追加命令。因此,重写后AOF文件由两部分组成,一部分是类似RDB文件的二进制快照,另一部分是追加的命令文本,完美地结合了AOF和RDB持久化的优势。
开启AOF+RDB混合模式持久化的配置命令如下:
// redis.conf aof-use-rdb-preamble yes只要在redis.conf配置文件中写入上面这行代码就可以开启AOF+RDB混合模式。值得注意的是,此模式在Redis 4及以上版本才支持。