前言
Redis 是内存数据库,一旦 Redis 进程因为某种原因发生退出,内存中的数据就会全部丢失,即使只是把 Redis 当做缓存使用,在高并发的情况下也可能引起 缓存雪崩,影响系统正常运行。
为了解决这个问题,Redis 提供了两种持久化方式: RDB(Redis DataBase) 和 AOF(Append Only File),它们可以将内存中的数据保存到磁盘,在 Redis 进程异常退出重启后,可以从磁盘里把数据重新读回到内存。
RDB 持久化
RDB 持久化是将 Redis 在某一时刻的数据集快照(内存快照)写入 RDB 文件,在 Redis 重启时,把 RDB 文件载入内存就可以完成数据的恢复。
此外,RDB 文件还可以用于数据备份、主从节点间的数据同步等。
触发机制
RDB 持久化有两种触发机制:使用指令手动触发 、通过 redis.conf 配置自动触发。
手动触发
Redis 提供两个命令实现手动触发:save 和 bgsave:
- save 命令 由主线程完成 RDB 持久化,在这个过程中,主线程不能处理读写请求,直到完成 RDB 持久化,所以线上一定要谨慎使用;
- bgsave 命令 主线程会 fork 一个子进程来完成 RDB 文件的写入,这样也就避免了主线程的阻塞。
自动触发
除了手动触发,还可以通过配置文件来设定一定的触发条件,Redis 检测到条件满足后,会自动触发持久化。
自动触发执行过程和 bgsave 命令一样,也是 fork 一个子进程来完成文件写入。
save 900 1 # 900秒(15分钟)内有1个写入
save 300 10 # 300秒(5分钟)内有10个写入
save 60 10000 # 60秒(1分钟)内有10000个写入
save "" # 关闭 RDB 持久化
RDB 文件生成
首先,主线程 fork 一个子进程,fork 采用操作系统的 写时复制技术,子进程只会拷贝主进程的 内存页表项,以后子进程生成 RDB 文件时通过内存映射可以找到真正的物理内存。
如果在生成 RDB 文件的过程中,主线程要修改数据,它需要先拷贝数据得到一份数据副本,然后修改该副本,而子进程继续使用原来的数据写入到 RDB 文件。
RDB 存在的问题
1、虽然通过 fork 子进程在 RDB 文件生成阶段不会影响主线程的正常读写,但是 fork 本身是需要耗时的,在 fork 执行时,主线程是会被阻塞的,内存数据越多,页表也越大,fork 耗时也就越久,而 RDB 持久化记录的是内存在某一时刻的全部数据,因此,这个耗时不可忽略。
2、因为采用 写时复制,主子进程共享内存数据。如果在文件生成过程中,主线程存在大量的写命令,此时,操作系统必须分配大量的内存用于保存数据副本,这样会加大内存的占用,如果超过系统物理内存,数据就会在磁盘和内存之间来回置换,影响性能。(如果没有开启 swap 机制,会直接 OOM 报错)。
3、从 1 和 2 可以看出来,RDB 持久化不宜执行的太频繁,但是如果 RDB 执行间隔时间太久,又会引发另一个问题:一旦发生 Redis 宕机,那在上一次生成 RDB 文件之后到宕机发生这一时刻之间的所有数据都会丢失。
AOF 持久化
AOF 日志
AOF 日志记录发生在写命令后,以 Redis 协议的格式 保存,采用追加的方式写入文件中。这句话有几个要点:
1、只有写命令才会产生 AOF 日志;
2、写日志发生在写命令之后,即先修改数据,然后再记录日志;
3、日志文件中记录的是 Redis 协议格式的写命令;
4、每执行一个写命令,都会产生一条日志记录追加到日志文件。
为什么要采用 写后日志,它有什么好处呢?
因为采用写后日志可以省去命令检查,也就是说如果命令有误,在执行命令时就会出错返回,不用记录日志。
如果采用写前日志,就需要先检查命令是否正确,这会带来一定的开销。
三种写回策略
在所有写文件的场景都要考虑到文件系统的内存缓存 page cache,应用程序调用 write() 函数一般都只是写到 page cache 中,而不是直接同步到磁盘,而程序一般都会提供一个同步磁盘的策略配置,它指的就是将 page cache 中的内容同步到磁盘的时机。
我么看下redis 提供的配置项 appendfsync :
- Always: 同步写回。写命令执行完,立即同步写到磁盘;
- Everysec:每秒写回。写命令执行完,将命令日志写入到 AOF 日志的 page cache,每隔一秒将缓存区中的数据写到磁盘;
- No:redis 不控制写回,由操作系统决定。写命令执行完,将命令日志写入到 AOF 日志的 page cache,由操作系统决定何时将缓存区的数据写到磁盘。
选择哪种策略要根据业务特点来决定。要求高性能且允许数据丢失就选择 No,要高可靠就选择 Always。
一般推荐 Everysec 策略,每秒将数据写回磁盘不会影响 Redis 性能,同时最多也只会丢失一秒的数据。
AOF 重写机制
随着写命令的不断执行,AOF 文件会变得越来越大,这会形成一定的问题:
1、文件系统本身对单个文件大小存在限制;
2、往大文件追加内容的效率较低;
3、在 Redis 进程重启时,要利用 AOF 日志文件恢复数据,日志文件越大,恢复时间就越长。
所以 Redis 实现了重写机制,它可以让 AOF 日志文件变小。那它是基于什么实现的呢?
先前说过,AOF 日志记录的是所有的写命令,那么某个 key 当前的值可能是经历过多个写命令之后形成的。
在重写的时候,Redis 会读取当前缓存中所有数据的最新状态,并用 一条命令 记录到 新的 AOF 日志文件中。
比如 某个字符串键值 key = 10, 它是经历过多条命令之后最新的值,
那在重写的时候,我们只要记录一条 set key 10 即可。
AOF 重写过程
AOF 重写是将数据库当前数据的最新状态通过一条写命令来实现回放,这个过程是比较耗时的。为了避免阻塞主线程,Redis 会 fork 一个子进程来完成重写。
在开始重写时,先 fork 一个子进程,子进程首先拷贝一份当前进程的 内存页表,此后再通过内存页表查询真实物理内存 将数据写到 新的日志文件。
在重写过程中,当前进程不会阻塞,可以正常接收读写请求,为了不丢失最新的数据,对于所有的写命令,Redis 要把这个写命令记录一份到 旧的 AOF 日志文件,同时还要记录一份到 重写日志缓冲区,在子进程将拷贝的数据都写入新的 AOF 日志文件后,重写日志缓冲区里的内容也要写入到新的 AOF 日志文件,最后用新的 AOF 日志文件替换旧的文件。
AOF 重写为什么要写一个新的日志文件?
因为两个进程同写一个文件会存竞争问题,影响性能,而且,如果 AOF 重写过程失败,如果复用原来的日志文件,那这个文件也已经被污染变的不可用。
AOF 重写在哪几个时刻会阻塞主线程?
1、fork 子进程时需要拷贝内存页表,此时会阻塞主线程;
2、fork 使用写时复制技术,在主线程执行写命令时需要拷贝内存数据,也会阻塞主线程;
3、子进程完成拷贝数据的重写后,要将 AOF 重写缓冲区的数据也追加到新的AOF 日志文件,此时也会阻塞主线程。
小结
不管是 RDB 持久化 还是 AOF 持久化,虽然他们在生成文件的时候都不会阻塞主线程,但是他们都包含一个 fork 子进程的动作,而执行这个动作的时候进程必然是阻塞的,阻塞时间由 fork 时间决定,fork 子进程需要拷贝主进程的内存页表,内存越大,页表越大,fork 耗时也就越久,主线程被阻塞的时间就越久,因此,Redis 单个实例内存都不要搞的太大。
此外,fork 子进程采用写时复制技术,在主线程执行写命令时,父进程要重新分配内存空间来保存数据副本。对于 bigkey 的修改,需要重新申请大量的内存,这不仅会增加主线程的阻塞时间,严重时还可能导致内存使用超出物理内存大小,因此一定要尽量减少 bigKey 的存在。
知识扩展
写时复制
写时复制(英语:Copy-on-write,简称COW)是一种计算机程序设计领域的优化策略。其核心思想是,如果有多个调用者(callers)同时要求相同资源(如内存或磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这过程对其他的调用者都是透明的(transparently)。此作法主要的优点是如果调用者没有修改该资源,就不会有副本(private copy)被创建,因此多个调用者只是读取操作时可以共享同一份资源。
在 Linux 程序中,fork 调用会产生一个和父进程完全相同的子进程,但大多时候子进程在此后会执行 exec 系统调用,出于效率考虑,linux中引入了 写时复制 技术,也就是只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程。
fork 这里不深入讨论,下次再写,夜已深,不卷了。