redis的持久化机制
Redis 持久化机制
Redis 是一个内存数据库,数据保存在内存中。众所周知,内存中的数据变化快且易丢失,因此 Redis 提供了两种持久化机制:快照(RDB) 和 追加日志(AOF)。
注意:Redis 4.0 版本新增了混合持久化机制,将 RDB 的内容和增量 AOF 放在一起。这里的 AOF 记录的是从 RDB 生成开始到结束之间的增量日志。
快照持久化(RDB)
执行快照操作时,Redis 必须进行 I/O 读写操作。文件 I/O 操作可能会严重影响服务器请求的性能。此外,如果在保存过程中数据发生修改或删除,可能会导致数据不一致。为了解决这些问题,Redis 使用了 COW 机制(Copy-On-Write,写时复制) 来实现持久化。
COW 实现原理
fork()之后,Kernel 把父进程中所有的内存页的权限都设为read-only,然后子进程的地址空间指向父进程。当父子进程都只读内存时,相安无事。当其中某个进程写内存时,CPU 硬件检测到内存页是read-only的,于是触发页异常中断(page-fault),陷入 Kernel 的一个中断例程。中断例程中,Kernel 就会把触发的异常的页复制一份,于是父子进程各自持有独立的一份。
COW 的优缺点
优点:
- 可减少分配和复制大量资源时带来的瞬间延时。
- 可减少不必要的资源分配。例如
fork进程时,并不是所有的页面都需要复制,父进程的代码段和只读数据段都不被允许修改,所以无需复制。
缺点:
- 如果在
fork()之后,父子进程都还需要继续进行写操作,那么会产生大量的分页错误(页异常中断 page-fault,需要将触发的异常的内存页复制一份),这样就得不偿失。
Redis 中的 COW 应用
Redis 在持久化时,如果是采用BGSAVE命令或者BGREWRITEAOF的方式,那 Redis 会fork出一个子进程来读取数据,从而写到磁盘中。
总体来看,Redis 还是读操作比较多。如果子进程存在期间,发生了大量的写操作,那可能就会出现很多的分页错误(页异常中断 page-fault),这样就得耗费不少性能在复制上。
而在 rehash 阶段上,写操作是无法避免的。所以 Redis 在 fork 出子进程之后,将负载因子阈值提高,尽量减少写操作,避免不必要的内存写入操作,最大限度地节约内存。
追加日志持久化(AOF)
AOF 日志存储的是 Redis 服务器的顺序指令序列,只记录对内存进行修改的指令记录。持久化功能的实现可以分为 命令追加、文件写入、文件同步 三个步骤。
命令追加
当 AOF 持久化功能打开时,服务器在执行完一个写命令之后,会以协议格式将被执行的写命令追加到服务器状态的 aof_buf 缓冲区的末尾。
AOF 文件的写入与同步
每当服务器常规任务函数被执行、或者事件处理器被执行时,flushAppendOnlyFile 函数都会被调用。这个函数执行以下两个工作:
- WRITE:根据条件,将
aof_buf中的缓存写入到 AOF 文件。 - SAVE:根据条件,调用
fsync或fdatasync函数,将 AOF 文件保存到磁盘中。
两个步骤都需要根据一定的条件来执行,而这些条件由 AOF 所使用的保存模式来决定。Redis 目前支持三种 AOF 保存模式:
AOF_FSYNC_NO:不保存。AOF_FSYNC_EVERYSEC:每一秒钟保存一次。AOF_FSYNC_ALWAYS:每执行一个命令保存一次。
第一种模式(NO)
每次调用 flushAppendOnlyFile 函数,WRITE 都会被执行,但 SAVE 会被略过。以下三种场景的 SAVE 操作都会引起 Redis 主进程阻塞:
- Redis 被关闭。
- AOF 功能被关闭。
- 系统的写缓存被刷新(可能是缓存已经被写满,或者定期保存操作被执行)。
第二种模式(EVERYSEC)
SAVE 原则上每隔一秒钟就会执行一次。因为 SAVE 操作是由后台子线程调用的,所以它不会引起服务器主进程阻塞。但是在 flushAppendOnlyFile 函数被调用后会出现四种情况:

第三种模式(ALWAYS)
每次执行完一个命令之后,WRITE 和 SAVE 都会被执行。另外,因为 SAVE 是由 Redis 主进程执行的,所以在 SAVE 执行期间,主进程会被阻塞,不能接受命令请求。
AOF 文件的读取和数据还原
AOF 文件保存了 Redis 的数据库状态,而文件里面包含的都是符合 Redis 通讯协议格式的命令文本。这也就是说,只要根据 AOF 文件里的协议,重新执行一遍里面指示的所有命令,就可以还原 Redis 的数据库状态了。
Redis 读取 AOF 文件并还原数据库的详细步骤如下:
- 创建一个不带网络连接的伪客户端(fake client)。
- 读取 AOF 所保存的文本,并根据内容还原出命令、命令的参数以及命令的个数。
- 根据命令、命令的参数和命令的个数,使用伪客户端执行该命令。
- 执行步骤 2 和 3,直到 AOF 文件中的所有命令执行完毕。
完成第 4 步之后,AOF 文件所保存的数据库就会被完整地还原出来。
注意:因为 Redis 的命令只能在客户端的上下文中被执行,而 AOF 还原时所使用的命令来自于 AOF 文件,而不是网络,所以程序使用了一个没有网络连接的伪客户端来执行命令。伪客户端执行命令的效果,和带网络连接的客户端执行命令的效果完全一样。
AOF 重写
Redis 在长期运行的过程中,AOF 的日志会越变越长。如果实例宕机重启,重放整个 AOF 日志会非常耗时,因此需要对 AOF 进行重写。
Redis 提供了 BGREWRITEAOF 指令用于对 AOF 日志进行瘦身。其原理就是开辟一个子进程对内存进行遍历转换成一系列 Redis 的操作指令,序列化到一个新的 AOF 日志文件中。序列化完毕后再将操作期间发生的增量 AOF 日志追加到这个新的 AOF 日志文件中,追加完毕后就立即替代旧的 AOF 日志文件。使用子进程也有一个问题需要解决:AOF 重写期间如果有新的写命令进来,不能漏掉,否则会导致数据不一致。于是 Redis 服务器设置了一个 AOF 重写缓冲区,最后流程变为:
- 执行客户端发来的命令。
- 将执行的写命令追加到 AOF 缓冲区。
- 将执行后的写命令追加到 AOF 重写缓冲区。
这样一来可以保证:
- AOF 缓冲区的内容会定期被写入和同步到 AOF 文件,对现有 AOF 文件的处理工作会照常进行。
- 从创建子进程开始,服务器执行的所有写命令会被记录到 AOF 重写缓冲区里面。
当子进程完成 AOF 重写工作之后,它会向父进程发送一个信号,父进程收到信号后,会调用一个信号处理函数,并执行以下工作:
- 将 AOF 重写缓冲区中的所有内容写入新的 AOF 文件中,这时新 AOF 文件所保存的数据库状态和服务器当前状态一致。
- 对新的 AOF 文件进行改名,原子性操作地覆盖现有的 AOF 文件,完成新旧 AOF 文件的替换。
这个信号函数执行完毕以后,父进程就可以继续像往常一样接受命令请求了。在整个 AOF 后台重写过程中,只有信号处理函数执行时会对服务器进程造成阻塞,其他时候都可以继续处理请求,这样 AOF 重写对服务器性能造成的影响降到了最低。
说明:本文内容基于 Redis 核心机制整理,其中混合持久化特性适用于 Redis 4.0 及以上版本。
版权声明:本文为原创文章,版权归 戴老师的博客 所有,转载请联系博主获得授权。
本文地址:https://1diff.fun/archives/redis-de-chi-jiu-hua-ji-zhi.html
如果对本文有什么问题或疑问都可以在评论区留言,我看到后会尽量解答。