数据持久化RDB AOF1、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、AOFRDB混合模式配置Redis是一种内存型数据库即服务器在运行时系统为其分配了一部分内存来存储数据当服务器突然宕机了那么数据库里面的数据将会丢失为了应对这种情况必须通过持久化的方式将数据从内存保存到磁盘中如图所示。Redis持久化存储有两种持久化方案RDB(RedisDataBase)和AOF(Append-Only File)。其中RDB是将内存中数据的快照存储到磁盘内AOF则是通过日志记录Redis内的所有操作。Redis 4之后支持AOFRDB混合持久化的方式结合了两者的优点可以通过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命令的过程中服务器不能处理任何请求但是bgsavebackground 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;rsiptrrdbPopulateSaveInfo(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){intschedule0;// 当AOF正在执行时bgsave会在AOF完成之后执行if(c-argc1){// 参数只能是scheduleif(c-argc2!strcasecmp(c-argv[1]-ptr,schedule)){schedule1;}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_scheduled1;addReplyStatus(c,Background saving scheduled);}else{addReplyError(cAn AOF log rewriting in progress: cant 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将数据同步操作交给操作系统来处理性能最好但是数据可靠性最差。在配置文件中设置appendonlyyes后若没有指定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_statusC_ERR)flushAppendOnlyFile(0);}}// 执行write和fsync操作voidflushAppendOnlyFile(intforce){ssize_tnwritten;intsync_in_progress0;mstime_tlatency;// 如果没有数据则直接返回不需要刷盘if(sdslen(server.aof_buf)0)return;// 使用bio_pending来判断是否有后台fsync操作正在进行if(server.aof_fsyncAOF_FSYNC_EVERYSEC)sync_in_progressbioPendingJobsOfType(BIO_AOF_FSYNC)!0;// 如果没有设置强制刷盘的选项可能不会立即进行// 而是延迟执行AOF刷盘if(server.aof_fsyncAOF_FSYNC_EVERYSEC!force){if(sync_in_progress){if(server.aof_flush_postponed_start0){server.aof_flush_postponed_startserver.unixtime;return;// 如果距离上次执行刷盘操作没有超过2秒就直接返回}elseif(server.unixtime-server.aof_flush_postponed_start2){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);nwrittenwrite(server.aof_fd,server.aof_buf,sdslen(server.aof_buf));latencyEndMonitor(latency);// 重置延迟刷盘时间server.aof_flush_postponed_start0;// 如果write失败那么尝试将该情况记入日志if(nwritten!(signed)sdslen(server.aof_buf)){statictime_tlast_write_error_log0;intcan_log0;if((server.unixtime-last_write_error_log)AOF_WRITE_LOG_ERROR_RATE){can_log1;last_write_error_logserver.unixtime;}if(nwritten-1){if(can_log){serverLog(LL_WARNING,Error writing to the AOF file: %sstrerror(errno));server.aof_last_write_errnoerrno;}}else{if(can_log){serverLog(LL_WARNING,Short write while writing to the AOF file: (nwritten%lldexpected%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_errnoENOSPC;}// 处理写入AOF文件时出现的错误if(server.aof_fsyncAOF_FSYNC_ALWAYS){serverLog(LL_WARNING,Cant recover from AOF write error when the AOF fsync policy isalways.Exiting...);exit(1);}else{server.aof_last_write_statusC_ERR;// 如果已经写入了部分数据是不能通过ftruncate进行撤销的// 通过sdsrange清除aof_buf中已经写入磁盘的那部分数据if(nwritten0){server.aof_current_sizenwritten;sdsrange(server.aof_buf,nwritten,-1);}return;}}else{if(server.aof_last_write_statusC_ERR){serverLog(LL_WARNINGAOF write error looks solved, Redis can write again.);server.aof_last_write_statusC_OK;}}// 更新写入后的 AOF 文件大小server.aof_current_sizenwritten;/* 当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_bufsdsempty();}/* 如果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_fsyncAOF_FSYNC_ALWAYS){/* aof_fsync is defined as fdatasync() for Linux in order to avoid * flushing metadata. */latencyStartMonitor(latency);aof_fsync(server.aof_fd);/* Lets try to get this data on the disk*/latencyEndMonitor(latency);latencyAddSampleIfNeeded(aof-fsync-always,latency);server.aof_last_fsyncserver.unixtime;}elseif((server.aof_fsyncAOF_FSYNC_EVERYSECserver.unixtimeserver.aof_last_fsync)){if(!sync_in_progress)aof_background_fsync(server.aof_fd);server.aof_last_fsyncserver.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):fcreate(tmp_tile_name)# 遍历所有数据库16个dbfordb 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.typeString:# 用 SET key value 命令来保存字符串键 valueget_value_from_string(key)f.write_command(SET keyvalue)elif key.typeList:# 用 RPUSH key item1 item2...itemN 命令来保存列表键 item1,item2,...,itemNget_item_from_list(key)f.write_command(RPUSH keyitem1item2...itemN)elif key.typeSet:# 用 SADD key member1 member2...memberN 命令来保存集合键 member1,member2,...,memberNget_member_from_set(key)f.write_command(SADD keymember1member2...memberN)elif key.typeHash:# 用 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 keyfield1value1field2value2\...fieldNvalueN)elif key.typeSortedSet:# 用 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 keyscore1member1score2member2\...scoreNmemberN)else:raise_type_error()# 如果键带有过期时间就用 EXPIREAT key time 命令来保存键的过期时间ifkey.have_expire_time():f.write_command(EXPIREAT keykey.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、AOFRDB混合模式配置在掌握了上面两种持久化方式后我们可以了解到使用RDB持久化会有数据丢失的风险但是数据恢复的速度快使用AOF持久化可以保证数据的完整性但数据恢复的速度慢。在Redis 4之后的版本新增了AOFRDB混合模式先使用RDB存储快照然后使用AOF持久化记录所有的写操作当满足重写策略或手动触发重写的时候将最新的数据存储为新的RDB记录。重启服务时会从RDB和AOF两部分恢复数据既保证了数据的完整性又提高了数据恢复的性能。开启混合模式后在bgrewriteaof命令之后会在AOF文件中以RDB格式写入当前最新的数据之后的写操作继续以AOF的追加形式追加写命令。当Redis重启的时候先加载RDB的部分再加载剩余的AOF部分。AOFRDB混合模式持久化的流程图如图所示。开启混合持久化模式后重写之后的AOF文件和RDB文件都存储的是二进制数据继续往Redis中进行写操作时在AOF中仍然以追加的方式追加命令。因此重写后AOF文件由两部分组成一部分是类似RDB文件的二进制快照另一部分是追加的命令文本完美地结合了AOF和RDB持久化的优势。开启AOFRDB混合模式持久化的配置命令如下// redis.conf aof-use-rdb-preamble yes只要在redis.conf配置文件中写入上面这行代码就可以开启AOFRDB混合模式。值得注意的是此模式在Redis 4及以上版本才支持。