前言
相关系列
- 《Redis & 目录》(持续更新)
- 《Redis & 持久化 & 源码》(学习过程/多有漏误/仅作参考/不再更新)
- 《Redis & 持久化 & 总结》(学习总结/最新最准/持续更新)
- 《Redis & 持久化 & 问题》(学习解答/持续更新)
参考文献
- 《Redis的持久化详解》
概述
Redis持久化机制用于避免/减少内存数据的丢失。Redis是完全基于内存实现的非关系型数据库,其数据会被统一保存至内存中。因此一旦其遭遇了宕机/停电/关机/重启等情况的发生,则其内存数据就将全部丢失。这对某些需求/开发者来说是无法被接受的,因为其可能会在Redis中预存某些确保系统正常运行的重要数据。内存数据的丢失不但会导致系统的全量崩盘,并且也会提升修复的难度,因为数据的预存往往并不便利。对此Redis提供了持久化机制用于在遭遇上述情况时避免/减少内存数据的丢失,该机制的本质是将内存数据以某些特定的规则写入磁盘中。由于磁盘数据理论上会被永久保存,因此Redis就可以在启动时加载这些数据至内存以恢复/还原异常情况前的内存数据。
Redis存在RDB/AOF两种持久化机制。RDB/AOF分别基于快照/日志的思想实现,这其中RDB的性能更佳,但可能会丢失较多的数据;而AOF虽然性能较差,但数据完整性却可以达到基本不丢失的程度…这些知识点都会在下文详述。
RDB @ Redis Database @ Redis数据库
快照/COW/恢复
RDB会在磁盘中生成/保存Redis数据快照。所谓数据快照本质是Redis在某个时间点的数据备份,例如如果我们将Redis在T1时间点的数据以某种形式完整的备份下来,那么该数据备份就是Redis在T1时间点的数据快照,同理我们也可以得到Redis在T2/T3乃至任意时间点上的数据快照。而通过在磁盘中生成/保存快照文件的方式,Redis便可以在内存数据在因为宕机/停电/关机/重启等原因而丢失时将之恢复至最新数据快照的程度。
RDB支持“自动/手动”执行。关于RDB自动/手动执行的必要指令/配置具体如下:
- SAVE:执行阻塞式RDB,不推荐使用。
- BGSAVE:执行非阻塞式RDB,推荐使用。
- FLUSHALL:清空所有Redis数据,并会生成空dump.rdb文件,于持久化而言没有意义。
- LASTSAVE:获取最后一次RDB的时间。
名称:save
作用:设置RDB是否开启,并同时设置自动触发规则。
默认:无配置
示例:
---- 无配置 & 关闭RDB;
---- save “” & 开启RDB,但不支持自动触发,只能通过指令手动触发;
---- save 3600 1 & 开启RDB,且30分钟内写入1次时触发;
---- save 300 100 & 开启RDB,且5分钟内写入100次时触发;
---- save 60 10000 & 开启RDB,且1分钟内写入10000次时触发。
名称:dbfilename
作用:设置RDB快照文件名称。
默认:dump.rdb
名称:dir
作用:设置Redis的工作目录,其同时也是RDB快照文件的生成地址所在。
默认:./,即Redis根目录
RDB由子“进程”异步执行。对于“单进程单线程”的Redis而言,RDB会造成两大负面影响:一是磁盘I/O会大幅拉低其整体的读/写性能,因为磁盘I/O的速度必然远低于内存I/O;二是其执行必须阻塞内存读/写的执行,因为内存数据需要在持久化期间固定不变以保证数据快照的一致性,因此Redis不可以在中途接收/执行由客户端发送而来的请求,即不可以暂时停止快照生成去执行事务/指令。很显然这两大负面影响都会对Redis的性能造成严重负担,因此该持久化机制实际上被设计交由Redis主进程fork出来的子进程负责异步执行。该设计不但可以将磁盘I/O与内存I/O分离以保证整体/内存的读/写性能,更可以借助进程的底层机制来快速避免RDB执行对内存读/写所造成的阻塞,这也正是Redis会fork进程而不是线程的原因。而当RDB快照文件生成后,子进程会原子地重命名该RDB快照文件并删除可能存在的旧RDB快照文件,从而完成新RDB快照文件对旧RDB快照文件的替换。
RDB通过进程资源的独立性来保证数据快照的一致性并避免内/存读写的阻塞。对于本该资源独立的进程来说,如果进程A需要访问进程B的资源,那么操作系统就应该提供相应的机制来保证进程B资源的独立性不被破坏。例如进程B可以直接拒绝访问,这样其资源就必然是独立的;又例如进程B可以将相应资源全量拷贝至进程A中,这样进程A本质上就是在访问私有资源,那么进程B的资源就必然也是独立的。相关机制在各类操作系统中数量繁多,但其中最著名的必然当属Linux系统提供的COW @ 写时拷贝,因为其在确保资源独立性的同时还实现了内存开销的最小化。
COW允许多进程共享资源。所谓共享资源是指进程A被允许直接访问进程B的资源,目的是避免进程B资源的全量拷贝以实现内存开销的最小化。我们可以发现该行为似乎并未保证资源的独立性,这是因为COW认为资源独立性的核心在于确保资源的安全性,因此只要保证访问安全,那么将私有资源适当外放也并无不可。此外该共享虽然直观表现为进程A在非法访问进程B的资源,但本质却是进程A对私有资源的合法访问。因为资源共享时其本质便已不再是进程B的私有资源了,而是进程B资源与进程A资源拷贝的“暂时合并”。由于这两份资源初始时必然完全相同,故而该行为可显著减少内存开销,也因此COW本质上依然完整保证了进程资源的独立性。
COW不允许访问进程写入资源。无论资源的本质为何,其在内存上都已直接对外暴露。因此为了保证资源访问的安全性COW规定只有资源直接所在/所属的进程才允许写入资源,即只有进程B可以写入资源,而访问进程/进程A则只允许读取资源。此外由于资源共享时对所有进程都持有独立性,因此在进程B写入资源期间进程A的读取要保证不变,即不会读取到进程B新写入的内容,否则就会被破坏进程A资源的独立性。因此COW会通过写时拷贝来实现这一点,这是该机制名称的具体由来,这里我们就使用Redis内存数据的RDB持久化过程来举例讲解这一点。
在执行RDB的子进程被主进程fork出来的初始阶段,两者会共享主进程中的内存数据。此时子进程会遍历内存数据并在磁盘中生成RDB快照文件,而主进程则会继续进行本职的指令执行工作,从而避免RDB执行对内存读/写造成的阻塞。而当主进程试图写入内存数据时,由于必须保证子进程的资源独立性不被破坏,因此主进程不会直接对内存数据进行写入,而是会先“增量”拷贝所要写入的部分内存数据,随后再于拷贝中完成写入并用于后续读取,直至子进程完成RDB后再原子性覆盖原内存数据…具体图示如下:
增量拷贝同样是COW实现内存开销最小化的核心手段,因为绝大多数内存数据在RDB执行期间都不会被写入,因此这种拷贝方式往往只需耗费极少量的内存资源,并且上限也不会大于整体内存数据的大小。此外子进程完成RDB后会通知主进程,目的是令其执行记录时间/覆盖数据/释放资源等后续操作。
Redis会在启动时异步加载RDB快照文件以恢复内存数据。当Redis启动时,其会在工作目录(即“redis.conf”文件中“dir”配置项所指定的目录,其同时也是RDB快照文件的生成目录,默认为Redis的根目录)存在RDB快照文件的情况下fork子进程来执行加载以恢复内存数据,因此开发者需确保工作地址/RDB快照文件的正确性/存在来保证内存数据的正常恢复。Redis异步加载RDB快照文件是出于性能方面的考量,因为这有助于提升Redis初始化的整体效率。可虽说是异步加载,但该行为实际上依然可能导致主进程陷入阻塞。因为主进程虽然也会在此期间并发执行初始化操作,但其对内存数据的读/写却需要在原数据完全恢复后再执行才能保证安全性,因此主进程依然可能进入阻塞状态以确保子进程完成对RDB快照文件的整体加载。
优缺
- 适合大规模数据恢复;
- RDB快照文件相对较小,比较节省磁盘空间;
- RDB快照文件基于二进制存储的,数据恢复速度较快。
- 主进程会为fork出来的子进程拷贝内存数据,这部分内存开销会因为操作系统的差异而动态变化,但最大可达2倍膨胀,需要注意;
- 虽然Redis在fork的时候使用了写时拷贝技术,但是如果数据庞大时还是比较消耗性能;
- 如果Redis出现宕机/停电/关机/重启等情况,那么最后一次RDB后写入的新数据将会丢失/无法恢复。
AOF @ Append Only File @ 只追加文件
日志/恢复
AOF会在磁盘中记录Redis写指令。在AOF开启的情况下,Redis会在“写指令”正式执行后将之写入日志文件。而由于日志文件顺序记录了Redis自启动以来执行的所有写指令,因此Redis便可以在内存数据在因为宕机/停电/关机/重启等原因而丢失时通过“重放”日志的方式将之恢复…开启AOF的基础配置如下:
名称:appendonly
作用:设置AOF是否开启。
默认:no
名称:appendfilename
作用:设置AOF日志文件名。
默认:appendonly.aof
名称:dir
作用:设置Redis的工作目录,其同时也是AOF日志文件的生成地址所在。
默认:./,即Redis根目录
指令的写入分为缓冲/日志两步。指令写入日志属于标准的磁盘I/O,出于性能上的考量,Redis不可能允许主进程执行该类操作来拖慢高效的内存I/O,因此指令的写入实际上分为缓冲/日志两个步骤。这其中主进程负责将指令同步写入aof_buf @ AOF缓冲区中,而缓冲指令写入日志则由操作系统异步负责。如果要问主进程是否在缓冲指令写入日志的过程中有所参与,那么只能说操作系统对缓冲指令的正式写入是由主进程触发的,即调用fsync函数以告知操作系统将缓冲指令写入至磁盘日志的行为会由主进程负责。
AOF存在always/everysec/no三种写回机制。所谓写回机制是指控制缓冲指令何时写入日志的机制,我们可以从上文内容中料想到的是:既然指令会先写入缓存后再写入日志,那么如果Redis突然宕机,则缓冲指令就会因未能及时写入日志而丢失。事实上也确实如此,因此Redis引入了写回机制的概念并提供了三种具体实现来处理该项问题,其目的是为了令开发者可以在安全/性能之间实现自我选择…具体配置/机制如下:
名称:appendfsync
作用:设置AOF的写回机制。
默认:everysec
- aleays:主进程每向缓存中写入一个指令便调用一次FSYNC函数。该写回机制理论上(磁盘实际的写入效率是不可控的)可保证最多只丢失一个指令,因此其安全性是最高的。但由于该写回机制下主进程会与操作系统进行频繁交互,因此虽然其并不参与具体的文件写入,但其性能也是最差的,故而只推荐在安全性要求极高的业务中使用。
- everysec:主进程每秒调用一次FSYNC函数。该写回机制理论上可保证最多只丢失一秒内的指令,因此其安全/性能都较为可观,是最为推荐/常用的AOF写回机制;
- no:主进程不调用FSYNC函数,而是由操作系统自身决定何时将缓冲指令写入日志。该写回机制的性能虽然是最高的,但基本没有安全性可言,因此也是最不推荐使用的AOF写回机制。
Redis会在启动时自动读取AOF日志文件以恢复内存数据。虽然持久化方式并不相同,但AOF的数据恢复方式与RDB是相同的。当工作目录(即“redis.conf”文件中“dir”配置项所指定的目录,其同时也是AOF日志文件的生成目录,默认为Redis的根目录)中存在AOF日志文件时,Redis会在启动时自动读取并恢复内存数据,因此开发者需确保工作地址/AOF日志文件的正确性/存在来保证内存数据的正常恢复。
重写
AOF支持重写日志文件。由于AOF日志文件会记录Redis执行的所有写指令,因此其必然会随着程序的不断运行而愈加庞大。庞大的AOF日志文件一方面会占用大量的磁盘空间;另一方面还会增加Redis内存数据的恢复时长。因此出于开销/性能的两方面考量Redis支持对AOF日志文件进行“瘦身”,即通过重写指令的方式来达到减少指令数量/日志大小的效果…AOF重写的手动执行指令/自动执行配置如下:
- BGREWRITEAOF:重写AOF日志文件。
名称:auto-aof-rewrite-min-size
作用:设置自动触发AOF重写的最小日志文件大小。{auto-aof-rewrite-percentage}配置项会与{auto-aof-rewrite-min-size}配置项相互影响,即如果两者都存在,那么只有同时满足两个配置时才能自动触发AOF重写。例如某初始AOF日志文件的大小为64mb,则当其满足{auto-aof-rewrite-min-size}配置项而自动触发AOF重写后,如果新AOF日志文件为54mb,那么再次自动触发AOF重写的日志文件大小即为108mb,因为当AOF日志文件大小再次到达64mb时其不满足增长率 >= 100%的要求。
默认:64mb
名称:auto-aof-rewrite-percentage
作用:设置自动触发AOF重写的日志文件大小增长率,即AOF日志文件大小增长率大于该值时将自动触发AOF重写。{auto-aof-rewrite-percentage}配置项会与{auto-aof-rewrite-min-size}配置项相互影响,即如果两者都存在,那么只有同时满足两个配置时才能自动触发AOF重写。例如某初始AOF日志文件的大小为64mb,则当其满足{auto-aof-rewrite-min-size}配置项而自动触发AOF重写后,如果新AOF日志文件为54mb,那么再次自动触发AOF重写的日志文件大小即为108mb,因为当AOF日志文件大小再次到达64mb时其不满足增长率 >= 100%的要求。
默认:100(mb)
AOF重写由子进程异步执行。AOF重写指令时需要访问内存数据,因此主进程会fork子进程来执行AOF重写,目的是避免内存读/写的阻塞,并借助进程资源的独立性来保证内存数据在指令重写时的保持不变。子进程遍历内存数据的目的是为生成相应的指令,并将这些指令存入新AOF日志文件中。这里可能存在的疑惑是:既然指令重写的目的是为了对AOF日志文件进行瘦身,那么子进程不是应该读取AOF日志文件中的指令再执行合并/删除等手段吗?怎么会去遍历内存数据呢?事实上该方案理论上应该也是可行的,但现实情况是AOF日志文件往往过于臃肿且存在大量类似于“前指令值被后指令值覆盖/数据过期”的无效指令。同时又因为磁盘I/O的速度还远低于内存I/O,故而其工作量/重写效率都远大/低于直接从内存中读取最新数据,也因此子进程并不基于AOF日志文件来重写指令。
主进程会将指令同步写入aof_rewrite_buf @ AOF重写缓冲区。AOF重写缓冲区类似于AOF缓冲区,用于在子进程重写指令时暂存最新请求的写指令,也因此在此期间主进程会将写指令写入上述两个缓冲区中。
主进程负责将重写缓冲指令写入新AOF日志文件,并令新AOF日志文件取代旧AOF日志文件。当子进程完成对指令的重写后,其会向主进程发送信号并消亡。而得知指令重写已结束的主进程会先将重写缓冲指令全部写入新AOF日志文件中以确保指令完整性,随后再原子地重命名新AOF日志文件并删除可能存在的旧AOF日志文件,从而完成新AOF日志文件对旧AOF日志文件的取代以达成“瘦身”效果。这里可能产生的疑惑是:“重写缓存指令写入新AOF日志文件/新AOF日志文件取代旧AOF日志文件”属于标准的磁盘I/O操作,那这种操作怎么会被交由主进程来执行呢?实际上这是为了功能实现所做出的必要取舍,因为如果在上述磁盘I/O操作执行时没有阻塞内存的读/写,即磁盘I/O操作继续由子进程执行,那么AOF重写缓冲区的持续/并发写入将导致重写缓冲指令难以/无法被全量写入新AOF日志文件中,并且新AOF日志文件也难以/无法安全的替换旧AOF日志文件。因此将上述磁盘I/O操作交由主进程执行实际上也是为了快速/简单实现所做出的选择,因为主进程执行可以自然达到阻塞内存读/写的效果而无需任何附加行为。并且由于AOF重写本身也不是高频操作,因此主进程少许的几次磁盘I/O并不会对整体性能造成太大影响。
优缺
- 丢失的指令较少,因此数据恢复率更高;
- AOF日志文件的可读性更强,对自定义操作的支持度更高。
- AOF日志文件占用的磁盘空间更多;
- 内存数据恢复速度相对较慢;
- 在写回机制为Always的情况下,对整体性能会造成严重影响。
损坏
RDB快照/AOF日志文件可能损坏。无论是RDB还是AOF,其生成磁盘文件都存在损坏可能,而导致其损坏的具体原因包含但不限于以下几种:
- 硬件故障:磁盘损坏等硬件问题可能导致RDB快照/AOF日志文件在生成/写入过程中发生错误并损坏;
- 系统异常:宕机/断电等系统问题可能导致RDB快照/AOF日志文件在生成/写入过程中发生错误并损坏;
- 软件BUG:Redis软件及其依赖的库可能存在BUG,从而导致RDB快照/AOF日志文件在生成/写入过程中发生错误并损坏;
- 人为操作:不恰当的人为操作可能导致RDB快照/AOF日志文件被直接损坏,例如直接/错误的编辑RDB快照/AOF日志文件内容。
AOF日志文件的损坏概率大于RDB快照文件。虽说都是磁盘I/O操作,但AOF日志文件的损坏概率实际上远高于RDB快照文件。这是因为RDB快照文件会在一次生成后固定不变,而AOF日志文件却会在后续不断地追加指令,因此其磁盘I/O操作的总量/频率远高于RDB快照文件,故而其在过程中遭遇上述问题并损坏的概率自然也远高于RDB快照文件。
- REDIS-CHECK-RDB :检查RDB快照文件是否损坏,是则报告错误,并“可能”提供具体的错误信息。
------------------------- 入参 -------------------------
rdb-file-path @ RDB文件路径:RDB快照文件地址。 - REDIS-CHECK-AOF [–fix]:检查AOF日志文件是否损坏,是则报告错误,并“可能”提供具体的错误信息。该指令执行时建议关闭Redis,避免日志文件的持续追加对检查的结果造成影响。
------------------------- 入参 -------------------------
aof-file-path @ AOF文件路径:AOF日志文件地址。
------------------------- 选项 -------------------------
–fix:设置此次检查会尝试修复AOF日志文件文件,并返回修复结果。成功修复的日志文件可能(大概率)会丢失指令,因此在修复前应先备份。
Redis支持检查RDB快照/AOF日志文件是否损坏,并可以尝试修复AOF日志文件。Redis提供了相关指令用于检查/修复RDB快照/AOF日志文件,具体内容可在上文各自的指令中查询。AOF日志文件之所以支持修复是因为其损坏往往源于最后指令的写入中断,因此Redis只需将AOF日志文件从最后完整指令处截断便可以还原内容的合法性,但同理该行为也会加剧AOF日志文件的完整性丢失,即从原本的指令残缺加剧为指令丢失,因此在正式修复AOF日志文件前最好先进行备份。此外Redis还支持对AOF日志文件的其它损坏进行修复,例如某些语法/格式错误等,前提是其能够推演出原本的正确内容。这当然不是必定能做到的,也因此AOF日志文件的修复并不保证成功。那为什么Redis不支持对RDB快照文件进行修复呢?这是因为其更倾向于直接生成新的RDB快照文件,这具体有以下两点原因:
- 实现难度高:RDB快照文件有特定的存储格式,并且会被压缩以节省空间,而在这种情况下修复的难度极高并很容易出现新的错误;
- 数据不完整:RDB快照文件要求有绝对的一致性/完整性,而损坏会破坏这些特性,并且修复还会加剧这种情况。
为了尽可能避免RDB快照/AOF日志文件损坏及损坏后数据难以/无法恢复的情况发生,建议选择性的对Redis执行以下行为:
- 写回机制:配置合适的写回机制以降低AOF日志文件追加写入的频率,从而同步降低损坏的概率;
- 定期备份:定期备份对AOF日志文件进行备份,从而在AOF日志文件损坏时使用最新备份还原内存数据,即使这会导致最新数据的丢失/错误;
- 混合搭配:配合RDB一同使用,从而在AOF日志文件损坏时使用RDB快照文件还原内存数据,即使这会导致最新数据的丢失/错误;
- 实时监控:实时监控RDB快照/AOF日志文件的正确性并及时处理损坏问题。
使用/建议
混合持久化
混合持久化会令RDB/AOF协调执行。除了做备份/数据完整性要求不高的情况外,我们通常不会单独使用RDB来执行持久化,因为这样会导致最新数据丢失/错误。而虽说AOF的数据完整度更高,但相对而言其缺点也很明显,即数据的恢复速度要远低于RDB,故而当AOF日志文件较大时其文件加载/数据恢复的时间会相当长,也因此Redis在4.0版本中针对该情况推出了新的持久化方案 —— 混合持久化。该方案结合了RDB/AOF的优点,使得持久化可以同时达到数据完整度高/数据恢复快的效果。
混合持久化在5.0版本后默认开启。设置混合持久化的配置如下:
名称:aof-use-rdb-preamble
作用:设置是否开启混合持久化。
默认:yes
混合持久化的实现核心在于将AOF重写的指令生成替换为快照生成。在上文中我们说过:Redis会在AOF日志文件同时满足指定大小/比率的情况下对之进行AOF重写以实现瘦身。而在此期间主进程会fork子进程去遍历内存数据,目的是基于最新内存数据来生成指令并写入新AOF日志文件。但在开启混合持久化后,该行为便会由指令生成变更为快照生成,即子进程会遍历最新内存数据来生成快照,从而使得新AOF日志文件的内容从原本的“全指令”变更为“前快照&后指令”的状态…具体流程如下:
快照加速了AOF日志文件对内存数据的还原/恢复。由于二进制快照替换了本该数量庞大的旧内存数据指令,因此当Redis基于该AOF日志文件对内存数据进行恢复时其速度将获得大幅提升。而又因为新指令会持续向该AOF日志文件中增量追加,从而就使得持久化同时达到了数据完整度高/数据恢复快的效果。但话虽如此,混合持久化也同样具有两面性,其带来的负面影响包含但不限于以下几点:
- AOF日志文件添加了二进制快照数据后其可读性/可操作性会变差;
- AOF日志文件添加了二进制快照数据后其兼容性会变差,因为其无法用于4.0版本之前的Redis。
建议
- 如果希望内存数据会随着Redis的关闭而消失,那么不推荐使用任何持久化机制,而RDB/AOF默认也都是关闭的;
- 在对内存数据完整性要求不高的情况下,推荐只开启RDB,这样对性能更加友好,因为AOF对磁盘的压力较大且重写是会阻塞主进程;
- 即使在对内存数据完整度要求较高的情况也不推荐只开启AOF,因为其日志文件的损坏率很高,建议搭配RDB一同使用,以便在AOF日志文件损坏时使用RDB快照文件来保底还原内存数据;
- 建议将触发AOF重写的最小文件大小由默认的64MB调整至5G(可以根据磁盘大小动态选择)以上,从而降低AOF重写的执行频率以提升性能;
- Redis集群部署时,建议将主节点的RDB/AOF同时开启,以便在确保完整性的同时保障安全性;
- Redis集群部署时,不建议为从节点开启持久化机制。但如果有防止主节点不可用的需要,那建议只开启RDB,并且自动触发频率设置为15分钟即可;
- 在版本 > = 4.0的情况下,所有需要同时开启RDB/AOF的场景都建议开启混合持久化。