Redis 在最初的版本就提供了 RDB 持久化,在 1.100 版之后才新增了 AOF 持久化的特性。这篇文章探讨 RDB 持久化的实现。
为了保证高效读写,Redis 所有数据都保存到内存中。但一旦进程发生崩溃就会导致数据丢失,所以需要一个持久化策略来保障数据安全。所谓 RDB 持久化,就是 Redis 将内存中存储的数据全量地保存在 rdb 文件中,在默认的配置下这个文件是存放在 Redis 当前目录下的 dump.rdb 文件。
在启动 Redis 服务的时候,会加载 dump.rdb,将数据存储到内存。也可以手动或自动地启动 RDB 持久化。那 RDB 持久化触发的时机是什么呢?
Redis 提供有 save 命令,执行该命令时,Redis 会遍历所有的 key 然后将这些数据写入到文件系统,直到写入完成即为持久化完成。由于 Redis 是单线程的,执行 save 命令的过程中会阻塞所有其他操作。Redis 也提供有 bgsave
命令(background save),顾名思义也就是后台存储,执行 bgsave 的过程不影响 Redis 的正常运行。不管是 save
还是 bgsave
,都是用户主动操作,Redis 也提供有自动执行持久化的配置,配置方式形如:
save 900 1
save 300 10
save 60 10000
save m n
表示在距离上一次执行备份的 m 秒内,如果至少有 n 个key被修改,则执行 RDB 持久化。当配置多条规则的时候,满足其中一条则可。Redis 在全局有一个 dirty 计数器,在执行 RDB 持久化前,每写入一个 key,dirty 计数器就会累加,上面的 n 就是 dirty 计数器的值,执行完 RDB 持久化后 dirty 计算器归零, dirty 可以理解为未被持久化的热数据。
Redis 是怎样实现后台存储的?多线程和子进程都可以获得 Redis 的内存数据,我们来看看这两种方式实现的可行性。先来看看多线程的方案,Redis 创建多线程直接就共享当前线程的内存空间,但如果在后台执行RDB持久化的过程中,Redis 有了新 key-value 改动,那么在执行持久化的线程访问到的内存数据也会是开始持久化动作后的新的数据,这样就无法保证持久化数据的时间节点,这样如果要使用线程,可能需要引入锁的操作,这可能是个复杂并难以实现的方案。
再来看看子进程的方案,使用 fork 去创建子进程,子进程拥有父进程内存的一份完整拷贝,父子进程独享内存,父进程的内存更新不会影响子进程的内存。这就避免了上述多线程带来的问题。那是否子进程的方案就能满足需求呢?这里带来两个新的疑问:
* 使用子进程,内存占用会成倍增加吗?即设Redis服务占用1G,那么fork之后总内存占用是不是变成了2G?
* 子进程的内存空间相当于父进程 fork 时内存空间的完整拷贝,那这个拷贝内存的过程,是否会占用很多系统资源?
事实上,子进程拷贝内存采取的是写时复制(Copy On Write)策略,能避免以上两个问题。
父子进程理论上独占了一片内存空间。这个独占的内存空间指的是由操作系统抽象的虚拟内存空间,而它们虚拟内存映射的其实是同一片物理内存空间。如果共享部分的内存空间没有发生更改,那么父子进程实际上读取的是同一片物理内存空间,既不会发生拷贝,也不会增加内存的占用。而当父进程对共享部分内存进行修改的时候,系统会以页为单位将这部分内存拷贝到新的物理内存空间,子进程会访问新的这部分内存空间。由于读的时间一般远大于写的时间,所以子进程读取内存空间时候一般不会产生大量的物理内存空间拷贝。总的来说使用 fork 创建子进程既不成倍地增加内存占用,也不会因为拷贝大量内存而占用很多资源。
持久化成功后,数据会被保存到 dump.rdb 文件。那么在执行持久化的过程中,dump.rdb 会上文件锁吗?假如执行持久化的同时,rdb文件也正在被使用,会怎样?举一个常见的需求,每天可能会对 rdb 文件进行备份,执行下面的命令拷贝一份新的文件:
cp dump.rdb dump.2021.x.x
在执行 cp 的过程,会影响到 Redis 持久化吗?
实际上并不会产生影响。Redis 在执行持久化的时候,并不是直接写入 dump.rdb 文件,而是会先将所有数据保存在 tmp.rdb 这一个临时文件,然后再使用 rename 方法将 tmp.rdb 改名为 dump.rdb。查阅 rename 的文档:
可以发现他有两个特性:
* Open file descriptors for oldpath are also unaffected.
* If newpath already exists, it will be atomically replaced, so that there is no point at which another process attempting to access newpath will find it missing.
如果旧的文件已经打开了文件描述符,rename 不影响这次使用。
如果新文件路径已存在,会原子地替换这个文件。
拥有这两个特性,上述的问题就能得以解决。