目录
前言
本文从整体上详细介绍Redis的两种持久化方式,包含工作原理、持久化流程及实践策略,以及背后的一些理论知识。
Redis是一个内存数据库,所有的数据将保存在内存中,这与传统的MySQL、Oracle、SqlServer等关系型数据库直接把数据保存到硬盘相比,Redis的读写效率非常高。但是保存在内存中也有一个很大的缺陷,一旦断电或者宕机,内存数据库中的内容将会全部丢失。为了弥补这一缺陷, Redis提供了把内存数据持久化到硬盘文件,以及通过备份文件来恢复数据的功能,即Redis持久化机制。
一、RDB持久化
RDB快照:RDB持久化方案是按照指定时间间隔对你的数据集生成的时间点快照。它以紧缩的二进制文件保存Redis数据库某一时刻所有数据对象的内存快照,可用于Redis的数据备份、转移与恢复。到目前为止,仍是官方的默认支持方案。
1.RDB工作原理
既然说RDB是Redis中数据集的时间点快照,那我们先简单了解一下Redis内的数据对象在内存中是如何存储与组织的。
默认情况下,Redis中有16个数据库,编号从0-15,每个Redis数据库使用一个redisDb
对象来表示,redisDb
使用hashtable存储K-V对象。为方便理解,我以其中一个db为例绘制Redis内部数据的存储结构示意图。
时间点快照也就是某一时刻Redis内每个数据库(DB)中每个数据对象的状态,先假设在这一时刻所有的数据对象不再改变,我们就可以按照上图中的数据结构关系,把这些数据对象依次读取出来并写入到文件中,以此实现Redis的持久化。然后,当Redis重启时按照规则读取这个文件中的内容,再写入到Redis内存即可恢复至持久化时的状态。
当然,这个前提时我们上面的假设成立,否则面对一个时刻变化的数据集,我们无从下手。我们知道Redis中客户端命令处理是单线程模型,如果把持久化作为一个命令处理,那数据集肯定时处于静止状态。另外,操作系统提供的fork()函数创建的子进程可获得与父进程一致的内存数据,相当于获取了内存数据副本;fork完成后,父进程继续处理客户端请求,子进程负责持久化工作。
很显然,第一种情况不可取,持久化备份会导致短时间内Redis服务不可用,这对于高HA(高可用性)的系统来讲是无法容忍的。所以,第二种方式是RDB持久化的主要实践方式。由于fork子进程后,父进程数据一直在变化,子进程并不与父进程同步,RDB持久化必然无法保证实时性;RDB持久化完成后发生断电或宕机,会导致部分数据丢失;备份频率决定了丢失数据量的大小,提高备份频率,意味着fork过程消耗较多的CPU资源,也会导致较大的磁盘I/O。
2.RDB持久化流程
在Redis内完成RDB持久化的方法有rdbSave和rdbSaveBackground两个函数方法,先简单说下两者差别:
- rdbSave:是同步执行的,方法调用后就会立刻启动持久化流程。由于Redis是单线程模型,持久化过程中会阻塞,Redis无法对外提供服务;
- rdbSaveBackground:是后台(异步)执行的,该方法会fork出子进程,真正的持久化过程是在子进程中执行的(调用rdbSave),主进程会继续提供服务;
RDB的触发条件
RDB持久化触发的方式分为手动和自动。手动触发是指我们通过Redis客户端人为的对Redis服务端发起持久化备份指令,然后Redis服务端开始执行持久化流程,这里的指令有save和bgsave。自动触发是Redis根据自身运行要求,在满足预设条件时自动触发的持久化流程,自动触发的场景有如下几个:
(1)serverCron是Redis内的一个周期性函数,每隔100毫秒执行一次,它的其中一项工作就是:根据配置文件中save规则来判断当前需要进行自动持久化流程,如果满足条件则尝试开始持久化。
表示900秒(15分钟)内至少有1个key的值发生变化,则执行
save 900 1
表示300秒(5分钟)内至少有1个key的值发生变化,则执行
save 300 10
表示60秒(1分钟)内至少有10000个key的值发生变化,则执行
save 60 10000
该配置将会关闭RDB方式的持久化
save ""dirty计数器和lastsave属性
■ dirty计数器记录距离上一次成功执行SAVE命令或者BGSAVE命令之后,服务器对数据库状态(服务器中的所有数据库)进行了多少次修改(包括写入、删除、更新等操作)。
■ lastsave属性是一个UNIX时间戳,记录了服务器上一次成功执行SAVE命令或者BGSAVE命令的时间。
(2)从节点全量复制时,主节点发送rdb文件给从节点完成复制操作,主节点会触发bgsave;
(3)默认情况下(未开启AOF)执行shutdown命令时,自动执行bgsave;
SHUTDOWN命令执行以下操作:
- 停止所有客户端
- 如果有至少一个保存点在等待,执行SAVE命令
- 如果 AOF 选项被打开,更新 AOF 文件
- 关闭 redis 服务器(server)
如果持久化被打开的话,SHUTDOWN命令会保证服务器正常关闭而不丢失任何数据。
rdbSaveBackground
rdbSaveBackground是RDB持久化的辅助性方法,主要工作是fork子进程,然后根据调用方(父进程或者子进程)不同,有两种不同的执行逻辑。
- 如果调用方是父进程,则fork出子进程,保存子进程信息后直接返回。
- 如果调用方是子进程则调用rdbSave执行RDB持久化逻辑,持久化完成后退出子进程。
rdbSave是真正执行持久化的方法,它在执行时存在大量的I/O、计算操作,耗时、CPU占用较大,在Redis的单线程模型中持久化过程会持续占用线程资源,进而导致Redis无法提供其他服务。为了解决这一问题Redis在rdbSaveBackground中fork出子进程,由子进程完成持久化工作,避免了占用父进程过多的资源。
需要注意的是,如果父进程内存占用过大,fork过程会比较耗时,在这个过程中父进程无法对外提供服务;另外,需要综合考虑计算机内存使用量,fork子进程后最坏情况下会占用双倍的内存资源,需要确保内存够用。通过info stats命令查看latest_fork_usec选项,可以获取最近一个fork以操作的耗时。
rdbSave
Redis的rdbSave函数是真正进行RDB持久化的函数,整体流程可以总结为:创建并打开临时文件、Redis内存数据写入临时文件、临时文件写入磁盘、临时文件重命名为正式RDB文件、更新持久化状态信息(dirty、lastsave)。
上图右下角“遍历当前数据库写入键值对数据”这个环节会根据不同类型的Redis数据类型及底层数据结构采用不同的格式写入到RDB文件中。
3.RDB文件格式
完整的RDB文件包含如下各个部分
- RDB文件的最开头是REDIS部分,这个部分的长度为5字节,保存着"REDIS"五个字符。通过这五个字符,程序可以在载入文件时,快速检查所载入的文件是否RDB文件。
- db_version长度为4字节,它的值是一个字符串表示的整数,这个整数记录了RDB文件的版本号。
- databases部分包含着零个或任意多个数据库,以及各个数据库中的键值对数据。
- EOF常量的长度为1字节,这个常量标志着RDB文件正文内容的结束,当读入程序遇到这个值的时候,它知道所有数据库的所有键值对都已经载入完毕了。
- check_sum是一个8字节长的无符号整数,保存着一个校验和,这个校验和是程序通过对REDIS、db_version、databases、EOF四个部分的内容进行计算得出的。服务器在载入RDB文件时,会将载入数据所计算出的校验和与check_sum所记录的校验和进行对比,以此来检查RDB文件是否有出错或者损坏的情况出现。
一个RDB文件的databases部分可以保存任意多个非空数据库。每个非空数据库在RDB文件中都可以保存为SELECTDB、db_number、key_value_pairs三个部分:
- SELECTDB常量的长度为1字节,当读入程序遇到这个值的时候,它知道接下来要读入的将是一个数据库号码。
- db_number保存着一个数据库号码,根据号码的大小不同,这个部分的长度可以是1字节、2字节或者5字节。当程序读入db_number部分之后,服务器会调用SELECT命令,根据读入的数据库号码进行数据库切换,使得之后读入的键值对可以载入到正确的数据库中。
- key_value_pairs部分保存了数据库中的所有键值对数据,如果键值对带有过期时间,那么过期时间也会和键值对保存在一起。根据键值对的数量、类型、内容以及是否有过期时间等条件的不同,key_value_pairs部分的长度也会有所不同。
不带过期时间的key_value_pairs 部分由TYPE、key、value三部分组成,带过期时间的key_value_pairs 部分由TYPE、key、value以及新增的EXPIRETIME_MS和ms五部分组成。
- TYPE常量都代表了一种对象类型或者底层编码,当服务器读入RDB文件中的键值对数据时,程序会根据TYPE的值来决定如何读入和解释value的数据。
- key总是一个字符串对象,它的编码方式和REDIS_RDB_TYPE_STRING类型的value一样。
- 根据TYPE类型的不同,以及保存内容长度的不同,保存value的结构和长度也会有所不同。
- EXPIRETIME_MS常量的长度为1字节,它告知读入程序,接下来要读入的将是一个以毫秒为单位的过期时间。
- ms是一个8字节长的带符号整数,记录着一个以毫秒为单位的UNIX时间戳,这个时间戳就是键值对的过期时间。
过期时间必须是绝对值,这样不管 RDB 文件何时被载入,该过期的 key 都会正确地过期;AOF同理。
4.数据加载
通过使用rioRead()函数读取RDB文件中的数据,在读取过程中rioRead()函数会实时计算已读数据的校验和,当读取完毕后,将rioRead()计算的校验和与RDB文件末尾自带的校验和进行比较,即可判断文件内容是否完整。
二、AOF持久化
RDB是一种时间点(point-to-time)快照,适合数据备份及灾难恢复,由于工作原理的“先天性缺陷”无法保证实时性持久化,这对于缓存丢失零容忍的系统来说是个硬伤,于是就有了AOF。
1.AOF工作原理
AOF是Append Only File的缩写,它是Redis的完全持久化策略;这里的file存储的是引起Redis数据修改的命令集合(比如:set/hset/del等),这些集合按照Redis Server的处理顺序追加到文件中。当重启Redis时,Redis就可以从头读取AOF中的指令并重放,进而恢复关闭前的数据状态。
AOF持久化默认是关闭的,修改redis.conf即可开启AOF持久化功能。
AOF本质是为了持久化,持久化对象是Redis内每一个key的状态,持久化的目的是为了在Reids发生故障重启后能够恢复至重启前或故障前的状态。相比于RDB,AOF采取的策略是按照执行顺序持久化每一条能够引起Redis中对象状态变更的命令,命令是有序的、有选择的。把aof文件转移至任何一台Redis Server,从头到尾按序重放这些命令即可恢复如初。
2.AOF持久化流程
从流程上来看,AOF的工作原理可以概括为几个步骤:命令追加(append)、文件写入与同步(fsync)、文件重写(rewrite)、重启加载(load),接下来依次了解每个步骤的细节及背后的程序设计。
命令追加
当 AOF 持久化功能处于打开状态时,Redis 在执行完一个写命令之后,会以协议格式把被执行的写命令追加到 Redis 服务端维护的 AOF 缓冲区aof_buf末尾。如果此时正在进行AOF rewrite,还需将命令追加到Redis 服务端维护的 AOF 重写缓冲区aof_rewrite_buffer末尾。一方面是写入aof_buf缓冲区并根据appendfsync策略同步到磁盘,保证原有AOF文件完整和正确。另一方面写入aof_rewrite_buf重写缓冲区,保存fork之后的客户端的写请求,防止新AOF文件生成期间丢失这部分数据。
文件写入与同步
AOF文件的写入与同步离不开操作系统的支持,开始介绍之前,我们需要补充一下Linux I/O缓冲区相关知识。硬盘I/O性能较差,文件读写速度远远比不上CPU的处理速度,如果每次文件写入都等待数据写入硬盘,会整体拉低操作系统的性能。为了解决这个问题,操作系统提供了延迟写机制来提高硬盘的I/O性能。
传统的UNIX实现在内核中设有缓冲区高速缓存或页面高速缓存,大多数磁盘I/O都通过缓冲进行。 当将数据写入文件时,内核通常先将该数据复制到其中一个缓冲区中,如果该缓冲区尚未写满,则并不将其排入输出队列,而是等待其写满或者当内核需要重用该缓冲区以便存放其他磁盘块数据时, 再将该缓冲排入到输出队列,然后待其到达队首时,才进行实际的I/O操作。这种输出方式就被称为延迟写。
延迟写减少了磁盘读写次数,但是却降低了文件内容的更新速度,使得欲写到文件中的数据在一段时间内并没有写到磁盘上。当系统发生故障时,这种延迟可能造成文件更新内容的丢失。为了保证磁盘上实际文件系统与缓冲区高速缓存中内容的一致性,UNIX系统提供了sync、fsync和fdatasync三个函数为强制写入硬盘提供支持。
Redis每次事件处理前beforeSleep都会调用函数flushAppendOnlyFile
把AOF缓冲区aof_buf
中的数据写入内核缓冲区,并且根据appendfsync
配置来决定采用何种策略把内核缓冲区中的数据写入磁盘,即调用fsync()
。该配置有三个可选项always
、no
、everysec
,具体说明如下:
- always:每次都调用
fsync()
,是安全性最高、性能最差的一种策略。- no:不会调用
fsync()
。性能最好,安全性最差。- everysec:仅在满足同步条件时调用
fsync()
。这是官方建议的同步策略,也是默认配置,做到兼顾性能和数据安全性,理论上只有在系统突然宕机的情况下丢失1秒的数据。
注意1:上面介绍的策略受配置项no-appendfsync-on-rewrite
的影响,它的作用是告知Redis:AOF文件重写期间是否禁止调用fsync(),默认是no。
如果appendfsync
设置为always
或everysec
,后台正在进行的BGSAVE
或者BGREWRITEAOF
消耗过多的磁盘I/O,在某些Linux系统配置下,Redis对fsync()的调用可能阻塞很长时间。因为即使是在不同的线程中执行fsync()
,同步写入操作也会被阻塞。
为了缓解此问题,可以使用该选项,以防止在进行BGSAVE
或BGREWRITEAOF
时在主进程中调用fsync()。
- 设置为
yes
意味着,如果子进程正在进行BGSAVE
或BGREWRITEAOF
,AOF的持久化能力就与appendfsync
设置为no
有着相同的效果。最糟糕的情况下,这可能会导致30秒的缓存数据丢失。- 如果你的系统有上面描述的延迟问题,就把这个选项设置为
yes
,否则保持为no
。
注意2:在这种模式appendfsync everysec中,SAVE原则上每隔一秒钟就会执行一次。但在实际运行中,Redis主进程在这种模式下对fsync的调用并不是每秒一次,它和调用flushAppendOnlyFile函数时Redis所处的状态有关。
每当flushAppendOnlyFile函数被调用时, 可能会出现以下四种情况:
根据以上说明可以知道,在“每一秒钟保存一次”模式下,如果在情况1中发生故障停机,那么用户最多损失小于2秒内所产生的所有数据。如果在情况2中发生故障停机,那么用户损失的数据是可以超过2秒的。
Redis官网上所说的,AOF在“每一秒钟保存一次”时发生故障,只丢失1秒钟数据的说法,实际上并不准确。
文件重写
AOF是redis的一种持久化方式,用来记录所有的写操作,但是随着时间增加,aof文件会越来越大,所以需要进行重写,它会根据Redis内数据对象的最新状态生成新的AOF文件,新旧文件对应的数据状态一致,但是新文件会具有较小的体积。重写既减少了AOF文件对磁盘空间的占用,又可以提高Redis重启时数据恢复的速度。
在重写的过程中,由于redis还会有新的写入,为了避免数据丢失,会开辟一块内存用于存放重写期间产生的写入操作,等到重写完毕后会将这块内存中的操作再追加到aof文件中。
AOF文件重写既可以手动触发,也会自动触发。手动触发直接调用bgrewrite命令,如果当时无子进程执行会立刻执行,否则安排在子进程结束后执行。自动触发由Redis的周期性方法serverCron
检查在满足一定条件时触发。先了解两个配置项:
- auto-aof-rewrite-percentage:代表当前AOF文件大小(aof_current_size)和上一次重写后AOF文件大小(aof_base_size)相比,增长的比例。
- auto-aof-rewrite-min-size:表示运行
BGREWRITEAOF
时AOF文件占用空间最小值,默认为64MB;
当满足以下两个条件时,AOF文件重写就会触发:
增长比例:(aof_current_size - aof_base_size) / aof_base_size > auto-aof-rewrite-percentage
文件大小:aof_current_size > auto-aof-rewrite-min-size
从4.0版本开始,Redis在AOF模式中引入了混合持久化方案,即:纯AOF方式、RDB+AOF方式,这一策略由配置参数aof-use-rdb-preamble
(使用RDB作为AOF文件的前半段)控制,默认关闭(no),设置为yes可开启。所以,在AOF重写过程中文件的写入会有两种不同的方式。当aof-use-rdb-preamble
的值是:
- no:按照AOF格式写入命令,与4.0前版本无差别;
- yes:先按照RDB格式写入数据状态,然后把重写期间AOF缓冲区的内容以AOF格式写入,文件前半部分为RDB格式,后半部分为AOF格式。
AOF重写(BGREWRITEAOF)流程图:
当rewrite子进程开始后,父进程接受到的命令会添加到aof_rewrite_buf_blocks中,使得rewrite成功后,将这些命令添加到新文件中。在rewrite过程中,原来的AOF也可以选择是不是继续添加,由于存在性能上的问题,在rewrite过程中,如果fsync()继续执行,会导致IO性能受损影响Redis性能。所以一般情况下rewrite期间禁止fsync()到旧AOF文件。这策略可以在配置文件中修改。
在rewrite结束后,在将新rewrite文件重命名为配置中指定的文件时,如果旧AOF存在,那么会unlink掉旧文件。这是就存在一个问题,处理rewrite文件迁移的是主线程,rename(oldpath, newpath)过程会覆盖旧文件,这时rename会unlink(oldfd),而unlink操作会导致block主线程。这时,我们就需要类似libeio这样的库去进行异步的底层IO。作者在bio.c有一个类似的机制,通过创建新线程来进行异步操作。
子进程根据内存快照,按照命令合并规则写入到新的AOF文件。每次批量写入硬盘数据量由配置aof-rewrite-incremental-fsync
控制,默认为32MB,防止单次刷盘数据过多造成硬盘阻塞。
数据加载
只有当AOF关闭时,才会载入RDB文件恢复数据。在AOF方式下,开启混合持久化机制生成的文件是“RDB头+AOF尾”,未开启时生成的文件全部为AOF格式。考虑两种文件格式的兼容性,如果Redis发现AOF文件为RDB头,会使用RDB数据加载的方法读取并恢复前半部分;然后再使用AOF方式读取并恢复后半部分。由于AOF格式存储的数据为RESP协议命令,Redis采用伪客户端执行命令的方式来恢复数据。
如果在AOF命令追加过程中发生宕机,由于延迟写的技术特点,AOF的RESP命令可能不完整(被截断)。遇到这种情况时,Redis会按照配置项aof-load-truncated
执行不同的处理策略。这个配置是告诉Redis启动时读取aof文件,如果发现文件被截断(不完整)时该如何处理:
- yes:则尽可能多的加载数据,并以日志的方式通知用户;
- no:则以系统错误的方式崩溃,并禁止启动,需要用户修复文件后再重启。
对于错误格式的AOF文件,先进行备份,然后采用redis-check-aof--fix
命令进行修复,修复后使用diff -u
对比数据的差异,找出丢失的数据,有些可以人工修改补全
当AOF开启时,Redis启动时会优先载入AOF文件来恢复数据;当AOF开启,且AOF文件存在时,Redis加载AOF文件;但AOF文件不存在时,即使RDB文件存在也不会加载。
文件校验: 与载入RDB文件类似,Redis载入AOF文件时,会对AOF文件进行校验,如果文件损坏,则日志中会打印错误,Redis启动失败。但如果是AOF文件结尾不完整(机器突然宕机等容易导致文件尾部不完整),且aof-load-truncated参数开启,则日志中会输出警告,Redis忽略掉AOF文件的尾部,启动成功。
三、RDB vs AOF
RDB优点
- RDB是一个紧凑压缩的二进制文件,代表Redis在某一个时间点上的数据快照,非常适合用于备份、全量复制等场景。
- RDB对灾难恢复、数据迁移非常友好,RDB文件可以转移至任何需要的地方并重新加载。
- RDB是Redis数据的内存快照,数据恢复速度较快,相比于AOF的命令重放有着更高的性能。
- 启动效率高:相比于AOF机制,如果数据集很大,RDB的启动效率会更高。
RDB缺点
- RDB方式无法做到实时或秒级持久化。因为持久化过程是通过fork子进程后由子进程完成的,子进程的内存只是在fork操作那一时刻父进程的数据快照,而fork操作后父进程持续对外服务,内部数据时刻变更,子进程的数据不再更新,两者始终存在差异,所以无法做到实时性。
- RDB持久化过程中的fork操作,会导致内存占用加倍,而且父进程数据越多,fork过程越长。
- Redis请求高并发可能会频繁命中save规则,导致fork操作及持久化备份的频率不可控;
- RDB文件有文件格式要求,不同版本的Redis会对文件格式进行调整,存在老版本无法兼容新版本的问题。
AOF优点
- AOF持久化有更好的实时性,我们可以选择三种不同的方式(appendfsync):no、every second、always,every second作为默认的策略具有最好的性能,极端情况下可能会丢失一秒的数据。
- AOF文件只有append操作,无复杂的seek等文件操作,没有损坏风险。即使最后写入数据被截断,也很容易使用
redis-check-aof
工具修复; - 当AOF文件变大时,Redis可在后台自动重写。重写过程中旧文件会持续写入,重写完成后新文件将变得更小,并且重写过程中的增量命令也会append到新文件。
- AOF文件以易于理解与解析的方式包含了对Redis中数据的所有操作命令。即使不小心错误的清除了所有数据,只要没有对AOF文件重写,我们就可以通过移除最后一条命令找回所有数据。
- AOF已经支持混合持久化,文件大小可以有效控制,并提高了数据加载时的效率。
AOF缺点
- 对于相同的数据集合,AOF文件通常会比RDB文件大;
- 在特定的fsync策略下,AOF会比RDB略慢。一般来讲,fsync_every_second的性能仍然很高,fsync_no的性能与RDB相当。但是在巨大的写压力下,RDB更能提供最大的低延时保障。
- 在AOF上,Redis曾经遇到一些几乎不可能在RDB上遇到的罕见bug。一些特殊的指令(如BRPOPLPUSH)导致重新加载的数据与持久化之前不一致,Redis官方曾经在相同的条件下进行测试,但是无法复现问题。
使用建议
对RDB和AOF两种持久化方式的工作原理、执行流程及优缺点了解后,我们来思考下,实际场景中应该怎么权衡利弊,合理的使用两种持久化方式。如果仅仅是使用Redis作为缓存工具,所有数据可以根据持久化数据库进行重建,则可关闭持久化功能,做好预热、缓存穿透、击穿、雪崩之类的防护工作即可。
一般情况下,Redis会承担更多的工作,如分布式锁、排行榜、注册中心等,持久化功能在灾难恢复、数据迁移方面将发挥较大的作用。建议遵循几个原则:
- 不要把Redis作为数据库,所有数据尽可能可由应用服务自动重建。
- 使用4.0以上版本Redis,使用AOF+RDB混合持久化功能。
- 合理规划Redis最大占用内存,防止AOF重写或save过程中资源不足。
- 避免单机部署多实例。
- 生产环境多为集群化部署,可在slave开启持久化能力,让master更好的对外提供写服务。
- 备份文件应自动上传至异地机房或云存储,做好灾难备份。
关于fork()
通过上面的分析,我们都知道RDB的快照、AOF的重写都需要fork,这是一个重量级操作,会对Redis造成阻塞。因此为了不影响Redis主进程响应,我们需要尽可能降低阻塞。
- 降低fork的频率,比如可以手动来触发RDB生成快照、与AOF重写;
- 控制Redis最大使用内存,防止fork耗时过长;
- 使用更高性能的硬件;
- 合理配置Linux的内存分配策略,避免因为物理内存不足导致fork失败。
总结
Redis提供了两种持久化的选择:RDB支持以特定的实践间隔为数据集生成时间点快照;AOF把Redis Server收到的每条写指令持久化到日志中,待Redis重启时通过重放命令恢复数据。日志格式为RESP协议,对日志文件只做append操作,无损坏风险。并且当AOF文件过大时可以自动重写压缩文件。
参考博客:
一文带你详解Redis常用的数据类型以及面试常碰到的数据持久化机制原理
Redis持久化(六)-RDB+AOF混合持久化Redis专题:万字长文详解持久化原理
深度剖析Redis持久化机制 - 掘金