Redis 不同于 Memcached 的很重要一点就是,Redis 支持持久化,而且支持两种不同的持久化操作:
快照(snapshotting,RDB)
只追加文件(append-only file, AOF)
前沿知识
数据库在进行写操作时主要有下面五个过程:
- 客户端向服务端发送写操作(此时数据在客户端的内存中)
- 数据库服务端接收到写请求的数据(数据在服务端的内存中)
- 服务端调用
write
这个系统调用,将数据往磁盘上写(数据在系统内存的缓冲区中) - 操作系统将缓冲区中的数据转移到磁盘控制器上(数据在磁盘缓存中)
- 磁盘控制器将数据写到磁盘的物理介质中(数据真正落到磁盘上)
结合上面的5个流程看一下各种级别的故障:
-
当
数据库系统故障
时,但是操作系统还可以继续工作。那么此时只要执行完
第3步(数据就已经不在内存中了),那么数据就是安全的,因为操作系统会来完成后面几步,保证数据最终会落到磁盘上。 -
当
断电
时,上面5项中提到的所有缓存
都会失效,并且数据库和操作系统都会停止工作。所以只有当数据在完成第5步后(在磁盘上),才能保证在断电后数据不丢失。
通过上面5步的了解,可能我们会希望搞清下面一些问题:
-
数据库多长时间调用一次write,将数据写到内核缓冲区?
-
内核多长时间会将内核缓冲区中的数据写到磁盘控制器?
-
磁盘控制器又在什么时候把缓存中的数据写到物理介质上?
对于第一个问题,通常数据库层面会进行全面控制。
对于第二个问题,操作系统有其默认的策略,但是我们也可以通过POSIX API提供的fsync系列命令强制操作系统将数据从内核区写到磁盘控制器上。
对于第三个问题,好像数据库已经无法触及,但实际上,大多数情况下磁盘缓存是被设置关闭的,或者是只开启为读缓存,也就是说写操作不会进行缓存,而是直接写到磁盘。
- 建议的做法是仅仅磁盘设备有备用电池时才会开启写缓存。
数据损坏
所谓数据损坏,就是数据无法恢复,上面讲的都是如何保证数据是确实写到磁盘上去,但是写到磁盘上可能并不意味着数据不会损坏。比如一次写请求会进行两次不同的写操作,当意外发生时,可能会导致一次写操作安全完成,但是另一次还没有进行。如果数据库的数据文件结构组织不合理,可能就会导致数据完全不能恢复的状况出现。
通常有三种策略来组织数据,以防止数据文件损坏到无法恢复的情况:
- 第一种是最粗糙的处理,就是不通过数据的组织形式保证数据的可恢复性。而是通过配置数据同步备份的方式,在数据文件损坏后通过数据备份来进行恢复。实际上MongoDB在不开启操作日志,通过配置Replica Sets时就是这种情况。
- 另一种是在上面基础上添加一个
操作日志
,每次操作时记一下操作的行为,这样我们可以通过操作日志来进行数据恢复。因为操作日志是顺序追加的方式写的,所以不会出现操作日志也无法恢复的情况。
这也类似于MongoDB开启了操作日志的情况。 - 更保险的做法是数据库不进行旧数据的修改,只是以追加方式去完成写操作,这样数据本身就是一份日志,这样就永远不会出现数据无法恢复的情况了。实际上CouchDB就是此做法的优秀范例。
持久化-RDB*
快照:在指定的时间间隔内,将内存中的数据集快照
写入磁盘,当恢复(Redis启动)时,将快照文件直接读到内存中。
快照,顾名思义可以理解为拍照一样,把整个内存数据映射到硬盘中,保存一份到硬盘,因此恢复数据起来比较快,把数据映射回去即可,不像AOF,一条条的执行操作命令。
RDB文件保存过程
总的过程就是:
- Redis 会单独 fork 一个子进程来进行持久化(父进程继续处理客户端请求),会先将数据写到一个
临时文件
中,等持久化过程结束了,再用这个临时文件替换掉上次持久化好的文件(上次持久化好的文件肯定还在磁盘中)。
下面是详细过程:
-
触发RDB机制,会产生一个快照文件。
-
Redis默认会将快照文件存储在Redis当前进程的工作目录中的
dump.rdb
文件中(可见快照文件在磁盘中),可以通过配置dir
和dbfilename
两个参数分别指定快照文件的存储路径
和文件名
。 -
快照文件
即整个
数据库的一个快照,就像给数据库拍个照一样,把整个数据库都有哪些数据全“记录”下来。 -
RDB 文件是经过压缩(可以配置rdbcompression 参数以禁用压缩节省CPU占用)的二进制格式,所以占用的空间会小于内存中的数据大小,更加利于传输。
-
-
Redis调用fork,现在有了子进程和父进程。
-
父进程继续处理client请求(也就是父进程负责将client远端数据写到内存),子进程负责将内存中数据写入到磁盘中的
临时文件
。- 由于OS的
写时复制机制(copy on write)
,父子进程会共享相同的物理页面,当父进程处理写请求时OS会为父进程要修改的页面创建副本,而不是对整个共享的页面操作。 - 写时复制机制:为了节约物理内存,在调用fork()生成新进程时,新进程与父进程会共享同一内存区。当父进程要更改其中某片数据时(如执行一个写命令),操作系统会将该片数据另外复制一份以保证子进程的数据不受影响。
- 所以新的RDB文件存储的是执行fork一刻的内存数据
- 由于OS的
-
当子进程将快照写入
dump.rdb
临时文件完毕后,将原来的rbd
文件替换掉,然后子进程退出。
关于
bgsave
命令和save
命令
可以通过执行bgsave
命令或执行save
命令通知redis做一次快照持久化,区别是:
- 执行
bgsave
命令,此时redis会fork一个子进程,父进程负责继续接受命令。 - 执行
save
命令后,到系统创建快照完成之前系统不会再接收新的命令。因为save操作是在主线程中保存快照的,由于redis是用一个主线程来处理所有 client的请求,这种方式会阻塞所有client请求。所以不推荐使用。
触发机制
下面三点会触发持久化:
save
规则满足的情况下,会自动触发 RDB 规则- 比如
save 900 1
,只要我们在900秒内修改了一次,就会触发持久化机制
- 比如
- 执行
flashall
命令 - 执行
SHUTDOWN
退出 Reids 时
如何恢复 RDB 文件?
只需要将 RDB 文件放入 Redis 的启动目录中即可,Redis 启动时会自动检查 dump.rdb
文件,恢复其中的数据。
优点
- 适合大规模的数据恢复,与 AOF 相比会更快;
- 对数据的完整性要求不高。
- 通过上述过程可以发现Redis在进行快照的过程中不会修改RDB文件,只有快照结束后才会将旧的文件替换成新的,也就是说任何时候 RDB 文件都是完整的。这使得我们可以通过定时备份 RDB 文件来实现 Redis 数据库备份。
缺点
- 最后一次快照以后更改的所有数据可能会丢失
- 因为RDB方式持久化需要一定的时间进行操作,如果在此期间 Redis 意外宕机了,最后一次修改的数据就没了(联系上面的前沿知识进行理解)
- 这就需要开发者根据具体的应用场合,通过组合设置自动快照条件的方式来将可能发生的数据损失控制在能够接受的范围。例如,使用Redis存储缓存数据时,丢失最近几秒的数据或者丢失最近更新的几十个键并不会有很大的影响。如果数据相对重要,希望将损失降到最小,则可以使用AOF方式进行持久化。
- fork 进程的时候,会占用一定的内存空间。
- 由于每次快照持久化都是将内存数据完整写入到磁盘一次,并不是增量的只同步脏数据。如果数据量大的话,而且写操作比较多,必然会引起大量的磁盘io操作,可能会严重影响性能。
持久化-AOF*
将所有命令都记录下来(有个 history
文件),恢复的时候将该文件全部执行一遍。
- Redis默认采用RDB方式进行持久化,可用
appendonly yes
启用AOF持久化方式。
即提供一个操作日志,以日志的形式记录每个写操作,将所有执行过的指令记录下来(读操作不记录),只许追加文件,但不许改写文件,当Redis重启时会通过重新执行文件中保存的写命令来在内存中重建整个数据库的内容。
- Redis会将每一个收到的写命令都通过
write
函数追加到文件中(默认是appendonly.aof
文件) - 当然由于OS会在内核中缓存 write 做的修改,所以可能不是立即写到磁盘上。
- 这样AOF方式的持久化也还是有可能会丢失部分修改,不过我们可以通过配置文件告诉Redis我们想要通过
fsync
函数强制OS写入到磁盘的时机(默认是:每秒fsync
一次),有如下三种方式:appendfsync always
每次收到写命令就立即强制写入磁盘,效率最低,但可保证完全的持久化,不推荐使用apendfsync everysec
每秒钟强制写入磁盘一次,在性能和持久化方面做了很好的折中,推荐appendfsync no
完全依赖OS,性能最好,持久化没保证
关于
appendonly.aof
文件
AOF 默认的是文件的无限追加,这样的话持久化文件会变得越来越大。
- 例如我们调用
incr test
命令100次,文件中必须保存全部的100条命令,其实有99条都是多余的。因为要恢复数据库的状态其实文件中保存一条set test 100
就够了。
为了压缩AOF的持久化文件。Redis提供了bgrewriteaof
命令。收到此命令Redis将使用与快照类似的方式将内存中的数据以命令的方式保存到临时文件中,最后替换原来的文件。具体过程如下:
- Redis调用fork ,现在有父子两个进程
- 子进程根据内存中的数据库快照,往临时文件中写入重建数据库状态的命令
- 父进程继续处理client请求,除了把写命令写入到原来的aof文件中。同时把收到的写命令缓存起来。这样就能保证如果子进程重写失败的话并不会出问题。
- 当子进程把快照内容以命令方式写到临时文件中后,子进程发信号通知父进程,然后父进程把缓存的写命令也写入到临时文件。
- 现在父进程可以使用临时文件替掉换老的aof文件,并重命名,后面收到的写命令也开始往新的aof文件中追加。
重弄规则说明:
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64m
Redis 会记录上次文件的大小。
如果 AOF 文件过大(大于64m),将会 fork 一个新的进程将文件重写。
优点
-
每一次修改都同步,文件的完整性会更好;
-
使用 AOF 持久化会让 Redis 变得非常耐久(much more durable)
- 可以设置不同的 fsync 策略,比如无 fsync ,每秒钟一次 fsync ,或者每次执行写入命令时 fsync 。
- AOF 的默认策略为每秒钟 fsync 一次,在这种配置下,Redis 仍然可以保持良好的性能,并且就算发生故障停机,也最多只会丢失一秒钟的数据
- fsync 会在后台线程执行,所以主线程可以继续努力地处理命令请求。
-
AOF 文件是一个只进行追加操作的日志文件(append only log), 因此对 aof文件的写入不需要进行 seek , 即使日志因为某些原因而包含了未写入完整的命令(比如写入时磁盘已满、写入中途停机等等),如果该文件有错误,Redis 是启动不了的,我们就需要修复该文件。
redis-check-aof
工具也可以轻易地修复这种问题:redis-check-aof --fix appendonly.aof
Redis 可以在 AOF 文件体积变得过大时,自动地在后台对 AOF 进行重写: 重写后的新 AOF 文件包含了恢复当前数据集所需的最小命令集合。 整个重写操作是绝对安全的,因为 Redis 在创建新 AOF 文件的过程中,会继续将命令追加到现有的 AOF 文件里面,即使重写过程中发生停机,现有的 AOF 文件也不会丢失。 而一旦新 AOF 文件创建完毕,Redis 就会从旧 AOF 文件切换到新 AOF 文件,并开始对新 AOF 文件进行追加操作。
-
AOF 文件有序地保存了对数据库执行的所有写入操作, 这些写入操作以 Redis 协议的格式保存, 因此 AOF 文件的内容非常容易被人读懂, 对文件进行分析(parse)也很轻松。 导出(export) AOF 文件也非常简单: 举个例子, 如果你不小心执行了 FLUSHALL 命令, 但只要 AOF 文件未被重写, 那么只要停止服务器, 移除 AOF 文件末尾的 FLUSHALL 命令, 并重启 Redis , 就可以将数据集恢复到 FLUSHALL 执行之前的状态。
缺点
- 对于相同的数据集来说,AOF文件远远大于 RBD,修复的速度也比 RDB 慢;
- AOF 运行效率也要比 RDB 慢,所以 Redis 默认的配置就是 RDB。
- 根据所使用的 fsync 策略,AOF 的速度可能会慢于 RDB 。 在一般情况下, 每秒 fsync 的性能依然非常高, 而关闭 fsync 可以让 AOF 的速度和 RDB 一样快, 即使在高负荷之下也是如此。 不过在处理巨大的写入载入时,RDB 可以提供更有保证的最大延迟时间(latency)。
- AOF 在过去曾经发生过这样的 bug : 因为个别命令的原因,导致 AOF 文件在重新载入时,无法将数据集恢复成保存时的原样。 (举个例子,阻塞命令 BRPOPLPUSH 就曾经引起过这样的 bug ) 测试套件里为这种情况添加了测试: 它们会自动生成随机的、复杂的数据集, 并通过重新载入这些数据来确保一切正常。 虽然这种 bug 在 AOF 文件中并不常见, 但是对比来说, RDB 几乎是不可能出现这种 bug 的。