Redis 持久化
Redis 的数据存储在内存中,停机后内存中的数据就没有了。为了防止因为机器停机、进程崩溃等意外导致数据丢失,此 Redis 提供持久化功能,可以将内存中的数据保存在硬盘上。
Redis 提供的持久化方案有两种,分别是 RDB 持久化和 AOF 持久化。RDB 持久化做法是将内存中的数据序列化到到硬盘上,存储为 rdb 格式的文件,rdb 是 Redis database 的缩写,系统重启时可以通过载入并解析 rdb 文件来恢复内存中的数据。AOF 持久化,是将所有修改数据库状态的命令记录下来,存储到一个文件中,这里 AOF 意思是 Append Only File,所有命令都是通过追加的方式写入文件,当系统重启时可以通过重新执行全部命令来从零恢复内存中的数据。
RDB 持久化
RDB 持久化就是给 Redis 中存储的数据做个快照,然后存储在硬盘上,文件是二进制格式,默认名称是 dump.rdb。可以主动发命令手动触发 RDB 持久化,可以可以配置让 Redis 自动在合适的条件下执行持久化。
可以通过发送 SAVE 或者 BGSAVE 两个命令来手动触发 RDB 持久化,当 Redis 收到 SAVE 命令后,他就会将内存中的所有数据通过一种自定义的编码格式序列化到 RDB 文件中。在执行 SAVE 命令的过程中,无法处理其他的请求,而保存所有的数据是一个耗时的操作,因此 Redis 可能会阻塞较长一段时间。为了避免在 SAVE 执行期间无法处理其他请求,Redis 提供了 BGSAVE 命令。Redis 收到 BGSAVE 命令后,会 fork 一个进程,在子进程中来保存数据。因为写时复制(copy on write,COW)机制,fork 之后的子进程拥有在 fork 的时间点之前 Redis 的全部数据,持久化的操作可以在一个独立的进程中进行,主进程可以继续提供服务。
另外可以在配置文件中配置 SAVE 这个选项来让 Redis 自动执行 RDB 持久化,下面是 SAVE 配置内容的例子:
save 600 10 # 如果在 600 秒以内,数据进行 10 次以上的修改,就执行 RDB 持久化
save 60 1000 # 在 60 秒内,执行了 1000 次以上的修改,就执行 RDB 持久化
可以配置多个 save 条件,只要其中一个达成,就会执行 RDB 持久化。
RDB 持久化的优点
RDB 持久化生成的 rdb 文件中包含了完整的数据,且数据格式比较紧凑,很方便进行数据的备份。可以周期性地执行 RDB 持久化来备份数据,这方便数据版本的回滚。为了容灾,也可以将 RDB 文件同步到不同的机房,方式数据丢失。
生成 RDB 的时候只需要进行一次 fork,后续操作只需要在子进程中进行,这可以让主进程继续以最高的性能提供服务。另外,使用 RDB 来恢复数据比 AOF 要快速。
RDB 文件还用于主从节点之间的同步,主节点生成 RDB 文件,将其发送给从节点,从节点使用 RDB 文件恢复数据。然后主节点在将开始生成此 RDB 文件之后的命令同步到从节点,这样就实现了主从同步。另外,在从节点上如果有 RDB 文件,还可以实现增量同步,从节点只需要从主节点上获取 RDB 文件之后的命令即可。
RDB 持久化的缺点
因为每次 RDB 持久化都会保存内存中的所有数据,这代价很大,尤其是数据量很多的时候,因此 RDB 持久化必然不能频繁地执行。在上一次执行了 RDB 持久化后,需要较长的时间才会再次执行持久化,如果此时出现了宕机,会丢失最近数分钟的产生的数据。
RDB 持久化使用 fork 产生子进程,并利用写时拷贝机制,在子进程中执行持久化操作。整个过程看起来很完美,但 fork 其实是一个很耗时的操作,因为它需要拷贝页表,尤其是虚拟内存中分配的页很多的时候。经过实验 Redis 数据量在 50 G的时候,fork 会阻塞近 1 秒(不同机器存在差异,但至少在数百毫秒量级),因此执行 fork 会对主进程阻塞一段时间,对于数据量很大,请求量很高的场景,fork 的影响不容忽视。而且如果对数据修改的比较多,那么会频繁地执行复制内存页的操作,会拉低 Redis 的响应速度。
RDB 持久化的实现
执行 RDB 持久化的过程,就是把所有的 KV 序列化到文件中,做法就是遍历 Redis 的每个 db,以及 db 中的每一个键值对,实现细节请关注 rdb.c 中的 rdbSave 函数。关于序列化的编码格式可以看代码实现,或者 Redis RDB Dump File Format 这篇文章,这篇文章 Redis RDB Version History 中记录了 RDB 文件版本变更情况。
大致结构如下,首先是一个 magic string,格式是 REDIS + RDB Version,之后就是遍历 db 了,先写一个 SELECT 命令,指出选择的数据库,然后写入数据库的大小,这样做在载入 RDB 的时候就可以一次性分配大小合适的 dict,无需在载入过程中扩容。接着就是一个个键值对,键值对根据 value 类型不同存储的格式也不同,但大体思路是先使用一个 TYPE 来标明类型,类型确定后,按照该类型规定的格式存储即可。最后,在 RDB 后面追加一个 EOF 作为结束。
REDIS0009
SELECT 1
RESIZE n1
DBSIZE EXPIRE_SIZE
KEY-VALUE-PAIR
KEY-VALUE-PAIR
KEY-VALUE-PAIR
...
SELECT 2
RESIZE n2
DBSIZE EXPIRE_SIZE
KEY-VALUE-PAIR
KEY-VALUE-PAIR
KEY-VALUE-PAIR
...
EOF
对于 BGSAVE,执行过程就是先 fork 出子进程,然后在子进程中执行 RDB 持久化。主进程 fork 完成后立刻返回响应给客户端,不阻塞其他的命令。
在执行 SAVE 和 BGSAVE 时如果存在子进程正在执行 RDB 持久化或者,就会报错提示客户端 RDB 持久化正在进行。如果当前有 AOF 重新的操作,也不会执行 RDB 持久化,如果非要执行 RDB 持久化,可以使用 BGSAVE SCHEDULE 在子进程中执行完 AOF 重写后启动 RDB 持久化。(AOF 重写也需要在子进程中进行,Redis 不允许同时有太多的子进程存在,因为那样会消耗比较多的计算机性能,而且会占用不少内存)。
AOF 持久化
AOF 持久化是将所有修改数据库的命令记录下来,这些命令都是通过追加写的形式保存在 AOF 文件中。AOF 中记录的数据符合 Redis 协议,可以很方便地读取和解析,在 Redis 启动的时通过在空数据库上执行这些命令,可以从零恢复出所有的数据。
比如给 Redis 发送如下命令:
127.0.0.1:6379> set name redis
OK
127.0.0.1:6379> set name redis
OK
127.0.0.1:6379> get name
"redis"
127.0.0.1:6379> del name
(integer) 1
会产生如下 AOF 文件,首先是一个 SELECT 命令选择当前的 db,接着是 set 命令,在后来是 del 命令。
*2
$6
SELECT
$1
0
*3
$3
set
$4
name
$5
redis
*2
$3
del
$4
name
启动了 AOF 持久化后,服务端每收到一条命令,它都会将此命令写到 AOF 文件中,然后在发送响应给客户端。为什么不先发响应,然后异步地写入命令呢?假如用户执行 set name redis,Redis 回复 OK,正准备写入到 AOF 时出现了故障,这样数据就丢失了,但客户端却认为 Redis 成功执行了这条命令,数据应该被记录下来了,就算出了故障,也有持久化机制来保证。反过来,先记录、后回复,如果未记录完成、或者发送回复之前就宕机了,客户端会收到超时的错误信息,在超时的情况下,数据有可能写入也有可能没有写入,客户端不能做任何假定。
每次处理一条命令前都执行写入 AOF 操作,那岂不是很慢?Redis 在每一轮事件循环中,会读取到用户请求并执行用户的命令,对哪些会修改 Redis 数据库的命令,Redis 会将其保存在 server.aof_buf 中,本轮事件循环执行完后会,会将其写入 AOF 文件,客户端的响应则会在下一轮事件循环中写给客户端。所以,写 AOF 是发生在发送响应之前的。这里一次性写入的是多条命令的,而不是针对每一条写命令都做一次写操作。
def event_loop():
while True:
# 处理命令,得到响应,处理过程中修改了数据库
# 这里的响应内容,在下一轮事件循环才会发送给客户端
processFileEvents();
processTimeEvents();
# 将修改数据库的命令写入 AOF
flushAppendOnlyFile();
Redis 小白:这里写入 AOF 文件是指写入内核的缓冲区,等待内核同步到磁盘呢,还是写入内核并立即同步到磁盘?
Redis 大佬:这是个好问题,这取决于你的需求。执行 write 后,数据会先保存在内核的缓冲区中,内核会定期将数据同步到磁盘上。因此,就算执行 write 把数据写入 AOF 文件, 在短时间内数据还存在于内核中。要想数据立刻被同步到磁盘上,需要手动执行 fsync。但是单执行 write 和执行 write + fsync 两者耗时差别很大,因为 fsync 需要和磁盘交互速度更慢。在 Redis 中提供了 appendfsync 配置项,它有三种取值:always、everysec、no,你可以依次来配置 Redis 什么时候进行同步。
Redis 小白:这三个参数什么意思呢?
Redis 大佬:always 是指每次执行了 write 操作后就立刻调用 fsync 来同步,everysec 是每秒同步一次,no 则表示不显式地执行同步,而是依赖于操作系统来定期执行同步(大约 30s)。always 的性能最差,但可以保证数据不丢失。no 可提供最高的性能,但是数据丢失的风险很大。everysec 则是折中方案,提供不错的性能,数据也最多会丢失一秒。
Redis 小白:哦,我懂了,谢谢大佬。
AOF 持久化的优缺点
AOF 持久化保存数据的粒度更小,通过选择不同的 fsync 策略,可以在数据安全和性能之间进行折中。AOF 文件追加写入的,写入性能很好,文件中的格式是 RESP,就算中途宕机,某条命令只写了一半,也很容易删除这条部分写入的命令。
随着命令的持续写入,AOF 文件会变得越来越大,文件中存在大量的无用记录(A = 100,其过程是 100 个 A += 1,这些过程不重要,我们只需要 A = 100 这一条信息即可)。Redis 提供了 AOF 重写的功能,AOF 重写启动一个子进程,在子进程中据当前的数据库状态,生成一条条生成这些数据的命令,并将这些命令写入新的 AOF 文件。在写重写 AOF 文件期间,新来的命令还需要写到旧的 AOF 文件中(因为新 AOF 文件可能出错,不能成功产生),另外需要把命令保存在 AOF 重写缓冲区中。当子进程重写完了 AOF 后,再将 AOF 重写期间产生的命令写入新的 AOF 文件,然后替换旧 AOF 文件。如此,就得到了体积更小的 AOF 文件。
AOF 文件的大小通常比 RDB 持久化产生的文件大很多,在载入的时候也比 RDB 持久化要慢。AOF 持久化是将数据持久化分散在平时,它通过稍稍拉低命令处理的性能,来保证数据的可靠性。而 RDB 持久化是周期性地大幅度拉低 Redis 性能,但在其他时候可以让 Redis 以最高速度提供服务,代价是有丢失数据的风险。
关于 AOF 的优缺点,建议阅读 这篇文档 获取更详细的描述。
AOF 持久化的实现
Redis 处理命令的时候如果发现命令修改了数据库,就会调用一个 propagate 函数,这个函数的作用是将此命令发送给从节点或者写入 AOF 文件,其他写入 AOF 文件是通过调用在 aof.c 中的 feedAppendOnlyFile 函数实现的,下面是函数调用栈:
#0 0x000000000047f917 in feedAppendOnlyFile ()
#1 0x00000000004391bd in propagate () at server.c:3603
#2 0x0000000000439873 in call () at server.c:3834
#3 0x000000000043a86d in processCommand () at server.c:4257
#4 0x000000000044da8c in processCommandAndResetClient () at networking.c:2010
#5 0x000000000044ffc5 in processInputBuffer () at networking.c:2111
#6 0x00000000004d89f3 in connSocketEventHandler ()
#7 0x00000000004329b5 in aeProcessEvents ()
#8 0x0000000000432cfd in aeMain ()
Redis 如何知道一条命令修改了数据库呢?在 server 上存在一个 dirty ,这是一个整数,用于记录对数据库的修改次数。在执行命令期间,各种命令的具体实现自然知道自己是否会修改数据库,如果修改了就增加这个变量。
看下面代码,在执行命令之前保存了 dirty 这个字段,执行完毕后如果 server.dirty - dirty 的值大于零,则说明这条命令修改了数据库,这就需要调用 propagate 函数将此命令传播到从节点或者写入 AOF 文件(如果开启了 AOF)。
dirty = server.dirty;
/* 略 */
c->cmd->proc(c);
/* 略 */
dirty = server.dirty - dirty;
feedAppendOnlyFile 做的事情就是把命令还原为 Redis 协议的字符串,然后将其追加到 server.aof_buf 中,等待当前事件循环结束后写入到 AOF 文件。写入 AOF 文件的逻辑在 flushAppendOnlyFile 函数中,该函数将 server.aof_buf 写入 AOF 文件,并根据 appendfsync 配置项来决定是否执行 fsync。该函数在 serverCron 和 beforeSleep 回调中被调用。在 serverCron 调用是为了周期性地执行 fsync,比如每秒一次 。在 beforeSleep 中调用是为了执行 write 操作,并处理 appendfsync = always 的情况。另外,在系统关闭前,和关闭 AOF 功能之前也会调用此函数。
AOF 重写
随着命令的持续写入,AOF 文件会变得越来越大,里面包含很多无用的记录。为了解决这个问题,Redis 提供了 AOF 重写的功能。AOF 重写功能,周期性地基于当前数据库中当前的数据得出产生这些数据的命令,并将其写入 AOF 文件,写入完成后替换原来的 AOF 文件。重写后的 AOF 文件不包含任何无用的记录,使用 AOF 恢复数据库的时候速度将更快。
AOF 重写可以使用 BGREWRITEAOF 命令手动触发,也可以由 Redis 在合适的时机自动触发。当 AOF 文件的大小超出设定范围后,Redis 就会进行一次重写。重写之后会记录下重写后的文件大小,之后如果发现 AOF 文件的大小扩大了 100%,就会再次执行 AOF 重写,并记录下新的 AOF 文件大小。
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
AOF 重写是在子进程中进行的,先执行 fork,然后遍历数据库中所有 key,生成一个临时的 AOF 文件。子进程在写 AOF 文件的时候,主进程不受影响还可以继续处理命令。子进程在写 AOF 的时候,还会有新的命令到来,对这部分命令 Redis 将其写入旧的 AOF 文件,同时将其写入 AOF 重写缓冲区 server.aof_rewrite_buf 中。子进程写完 AOF 后,主进程再将 server.aof_rewrite_buf 中的数据追加到 AOF 中。此时新产生的 AOF 中就包含了所有的命令了,Redis 使用新的 AOF 文件替换旧的 AOF 文件,这就完成了一次 AOF 重写。
AOF 重写并不是对 AOF 文件进行重写,而是重新创建一个新的 AOF 文件,之后替换旧的 AOF 文件。如果在 AOF 重写期间出现了故障,那也不会有什么影响,因为旧的 AOF 文件依然存在。
RDB + AOF
另外一种持久化方案结合 RDB 和 AOF 两种方式,首先使用 RDB 恢复数据库,然后使用 AOF 来做增量的修改。在 Redis 中如果设置了 aof-use-rdb-preamble 选项,就会采用这种方式。
aof-use-rdb-preamble yes
在重写 AOF 文件时,会先做 RDB 持久化,但是将结果保存在 AOF 文件中,然后将做 RDB 持久化阶段新产生的 AOF 数据追加到 AOF 文件中。先使用 RDB 得到一个基础,在使用 AOF 增量地修改,非常直观的想法。
关于 Redis 持久化的一些问题
1. Redis 会丢数据吗
Redis 是内存型数据库,它提供了 RDB 和 AOF 两种持久化的机制来数据安全。RDB 和 AOF 真的能保证数据不丢失吗?
首先想一想什么情况才算是数据丢失,客户端发送来一个 set name redis 命令,Redis 执行成功并返回响应。之前返回响应之后数据丢失了(没有持久化到磁盘上),这就算数据丢失。如果收到命令,但是还没有返回给用户响应,这个时候数据丢失了,那么只需要返回出错,这不算数据丢失。下面将分析 Redis 的持久化方式,看看它们会不会丢失数据。
首先看 RDB 持久化,RDB 是对数据做快照,它只能周期性地进行,因此在上一次 RDB 持久化后产生的数据,需要等到下次 RDB 成功执行后才能保证不丢失。如果执行下次 RDB 持久化之前系统宕机了,这部分数据就会丢失。
AOF 持久化则是对命令记录日志,这将持久化的粒度细分到单条命令。保证 Redis 高性能的是它将数据都放在内存中,如果每一条命令都需要写日志,那会大大拉低性能,Redis 的 AOF 持久化提供了配置,可以控制写入磁盘的频率。
通过配置 appendfsync = always 可以保证每条命令在返回给用户之前都记录到磁盘,这样在每次事件循环结束都会会做一次 write + fsync 操作,保证 AOF 被同步到磁盘上,然后在返回客户端响应。这种配置会严重拉低 Redis 的响应速度,通常只在完全无法容忍数据丢失的情况下采用。
配置 appendfsync = everysec 时,Redis 每次事件循环后执行一次 write 将数据写入内核,然后每秒执行一次 fsync 操作。执行 write 操作后,可以保证 Redis 进程退出后,数据是安全的。但如果机器停电了,保存在内存的缓冲区中的数据依然会丢失。因此这种配置下,如果进程奔溃可以保证不会丢数据,如果机器停电,则在极端情况下会丢失停机前 1 秒内的数据。(实际上,Redis 在一个单独的线程中执行 fsync 这可以保证主线程不会被 fsync 阻塞,write 操作是在主线程中进行的。但是,如果内核正在执行 fsync,此时 write 调用也会阻塞。因此,如果在准备调用 write 的时候发现 fsync 正在进行,就会跳过这次写入,等待下次事件循环的时候再执行写入。但是这个 write 最多被延迟两秒,如果 fsync 执行两秒以上,那就无论如何在主线程执行一次 write 操作。)
如果配置 appendfsync = no,Redis 只执行 write 写入数据给内核,从来不执行 fsync 操作。数据同步到磁盘上的频率取决于操作系统,在 Linux 上这一时间通常为 30 秒。
综上,如果系统只使用 RDB 持久化,进程奔溃或者机器宕机时候会丢失较多的数据。如果采用 AOF 持久化,那么进程奔溃不会丢失任何数据,在意外机器停机时候,如果配置 appendfsync = always 也不会丢失数据,否则会丢失部分写入内核但尚未同步到磁盘的数据。通常情况下 appendfsync 被设置为 everysec,因此就算停电了,最多只会丢失最近一秒的数据。
Redis 作者在 Redis persistence demystified 一文中解释了 Redis 持久化方案的很多细节,对数据是否会丢失做了精辟的剖析。并拿 Redis 的持久化方式和其他数据库的持久化方案进行对比。
总结
本文综述了 Redis 的 RDB 和 AOF 两种持久化方案的工作原理,并对其实现细节做了简要分析,最后分析了 Redis 持久化方案是否会丢失数据。