作为 web 开发的一员, 相信大家的面试经历里少不了会遇到这个问题: redis 是怎么做持久化的?
不急着给出答案, 先停下来思考一下, 然后再看看下面的介绍. 希望看了这边文章后, 你能够回答这个问题.
为什么需要持久化?
由于 Redis 是一种内存型数据库, 即服务器在运行时, 系统为其分配了一部分内存存储数据, 一旦服务器挂了, 或者突然宕机了, 那么数据库里面的数据将会丢失, 为了使服务器即使突然关机也能保存数据, 必须通过持久化的方式将数据从内存保存到磁盘中.
对于进行持久化的程序来说, 数据从程序写到计算机的磁盘的流程如下:
1, 客户端发送一个写指令给数据库 (此时数据在客户端的内存)
2, 数据库接收到写的指令以及数据 (数据此时在服务端的内存)
3, 数据库发起一个系统调用, 把数据写到磁盘 (此时数据在内核的内存)
4, 操作系统把数据传输到磁盘控制器 (数据此时在磁盘缓存中)
5, 磁盘控制器执行真正写入数据到物理媒介的操作 (如磁盘)
如果只是考虑数据库层面, 数据在第三阶段之后就安全了, 在这个时候, 系统调用已经发起了, 即使数据库进程奔溃了, 系统调用会继续进行, 也能顺利将数据写入到磁盘中. 在这一步之后, 在第 4 步内核会将数据从内核缓存保存到磁盘缓存中, 但为了系统的效率问题, 默认情况下不会太频繁地执行这个动作, 大概会在 30s 执行一次, 这就意味着如果这一步失败了或者就在进行这一步的时候服务器突然关机了, 那么就可能会有 30s 的数据丢失了, 这种比较普通的灾难性问题也是需要考虑的.
POSIX API 也提供了一个系统调用让内核强制将缓存数据写入到磁盘中, 比较常见的就是 fsync 系统调用.
int fsync(int fd);
fsync 函数只对由文件描述符 fd 指定的一个文件起作用, 并且等待写磁盘操作结束后才返回. 每次调用 fsync 时, 会初始化一个写操作, 然后把缓冲区的数据写入到磁盘中. fsync() 函数在完成写操作的时候会阻塞进程, 如果其他线程也在写同一个文件, 它也会阻塞其他线程, 直到完成写操作.
持久化
持久化是将程序数据在持久状态和瞬时状态间转换的机制. 对于程序来说, 程序运行中数据是在内存的, 如果没有及时同步写入到磁盘, 那么一旦断电或者程序突然奔溃, 数据就会丢失了, 只有把数据及时同步到磁盘, 数据才能永久保存, 不会因为宕机影像数据的有效性. 而持久化就是将数据从程序同步到磁盘的一个动作过程.
Redis 的持久化
redis 有 RDB 和 AOF 两种持久化方式. RDB 是快照文件的方式, redis 通过执行 SAVE/BGSAVE 命令, 执行数据的备份, 将 redis 当前的数据保存到 *.rdb 文件中, 文件保存了所有的数据集合. AOF 是服务器通过读取配置, 在指定的时间里, 追加 redis 写操作的命令到 *.aof 文件中, 是一种增量的持久化方式.
RDB
RDB 文件通过 SAVE 或 BGSAVE 命令实现. SAVE 命令会阻塞 Redis 服务进程, 直到 RDB 文件创建完成为止. BGSAVE 命令通过 fork 子进程, 有子进程来进行创建 RDB 文件, 父进程和子进程共享数据段, 父进程继续提供读写服务, 子进程实现备份功能. BGSAVE 阶段只有在需要修改共享数据段的时候才进行拷贝, 也就是 COW(Copy On Write).SAVE 创建 RDB 文件可以通过设置多个保存条件, 只要其中一个条件满足, 就可以在后台执行 SAVE 操作.
SAVE 和 BGSAVE 命令的实现代码如下:
- void saveCommand(client *c) {
- // BGSAVE 执行时不能执行 SAVE
- if (server.rdb_child_pid != -1) {
- addReplyError(c,"Background save already in progress");
- return;
- }
- rdbSaveInfo rsi, *rsiptr;
- rsiptr = rdbPopulateSaveInfo(&rsi);
- // 调用 rdbSave 函数执行备份 (阻塞当前客户端)
- if (rdbSave(server.rdb_filename,rsiptr) == C_OK) {
- addReply(c,shared.ok);
- } else {
- addReply(c,shared.err);
- }
- }
- /*
- * BGSAVE 命令实现 [可选参数 "schedule"]
- */
- void bgsaveCommand(client *c) {
- int schedule = 0;
- /* 当 AOF 正在执行时, SCHEDULE 参数修改 BGSAVE 的效果
- * BGSAVE 会在之后执行, 而不是报错
- * 可以理解为: BGSAVE 被提上日程
- */
- if (c->argc> 1) {
- // 参数只能是 "schedule"
- if (c->argc == 2 && !strcasecmp(c->argv[1]->ptr,"schedule")) {
- schedule = 1;
- } else {
- addReply(c,shared.syntaxerr);
- return;
- }
- }
- // BGSAVE 正在执行, 不操作
- if (server.rdb_child_pid != -1) {
- addReplyError(c,"Background save already in progress");
- } else if (server.aof_child_pid != -1) {
- // aof 正在执行, 如果 schedule==1,BGSAVE 被提上日程
- if (schedule) {
- server.rdb_bgsave_scheduled = 1;
- addReplyStatus(c,"Background saving scheduled");
- } else {
- addReplyError(c,
- "An AOF log rewriting in progress: can't BGSAVE right now. ""Use BGSAVE SCHEDULE in order to schedule a BGSAVE whenever"
- "possible.");
- }
- } else if (rdbSaveBackground(server.rdb_filename,NULL) == C_OK) {// 否则调用 rdbSaveBackground 执行备份操作
- addReplyStatus(c,"Background saving started");
- } else {
- addReply(c,shared.err);
- }
- }
有了 RDB 文件之后, 如果服务器关机了, 或者需要新增一个服务器, 重新启动数据库服务器之后, 就可以通过载入 RDB 文件恢复之前备份的数据. 但是 bgsave 会耗费较长时间, 不够实时, 会导致在停机的时候丢失大量数据.
AOF(Append Only File)
RDB 文件保存的是数据库的键值对数据, AOF 保存的是数据库执行的写命令.
AOF 的实现流程有三步:
append->write->fsync
append 追加命令到 AOF 缓冲区, write 将缓冲区的内容写入到程序缓冲区, fsync 将程序缓冲区的内容写入到文件. 当 AOF 持久化功能处于开启状态时, 服务器每执行完一个命令, 就会将命令以协议格式追加写入到 redisServer 结构体的 aof_buf 缓冲区, 具体的协议这里不展开阐述.
AOF 的持久化发生时期有个配置选项: appendfsync. 该选项有三个值: always: 所有内容写入并同步到 aof 文件 everysec: 将 aof_buf 缓冲区的内容写入到 AOF 文件, 如果距离上次同步 AOF 文件的 no: 将 aof_buf 缓冲区中的所有内容写入到 AOF 文件, 但并不对 AOF 文件进行同步, 由操作系统决定何时进行同步, 一般是默认情况下的 30s.
AOF 持久化模式每个写命令都会追加到 AOF 文件, 随着服务器不断运行, AOF 文件会越来越大, 为了避免 AOF 产生的文件太大, 服务器会对 AOF 文件进行重写, 将操作相同 key 的相同命令合并, 从而减少文件的大小.
举个例子, 要保存一个员工的名字, 性别等信息:
- > hset employee_12345 name "hoohack"
- > hset employee_12345 good_at "php"
- > hset employee_12345 gender "male"
只是录入这个哈希键的状态, AOF 文件就需要保存三条命令, 如果还有其他, 比如删除, 或者更新值的操作, 那命令将会更多, 文件会更大, 有了重写后, 就可以适当地减少文件的大小.
AOF 重写的实现原理是先服务器中的数据库, 然后遍历数据库, 找出每个数据库中的所有键对象, 获取键值对的键和值, 根据键的类型对键值对进行重写. 比如上面的例子, 可以合并为下面的一条命令:
> hset employee_12345 name "hoohack" good_at "php" gender "male".
AOF 的重写会执行大量的写入操作, Redis 是单线程的, 所以如果有服务器直接调用重写, 服务器就不能处理其他命令了, 因此 Redis 服务器新起了单独一个进程来执行 AOF 重写.
Redis 执行重写的流程:
在子进程执行 AOF 重写时, 服务端接收到客户端的命令之后, 先执行客户端发来的命令, 然后将执行后的写命令追加到 AOF 缓冲区中, 同时将执行后的写命令追加到 AOF 重写缓冲区中. 等到子进程完成了重写工作后, 会发一个完成的信号给服务器, 服务器就将 AOF 重写缓冲区中的所有内容追加到 AOF 文件中, 然后原子性地覆盖现有的 AOF 文件.
RDB 和 AOF 的优缺点
RDB 持久化方式可以只通过服务器读取数据就能加载备份中的文件到程序中, 而 AOF 方式必须创建一个伪客户端才能执行.
RDB 的文件较小, 保存了某个时间点之前的数据, 适合做灾备和主从同步.
RDB 备份耗时较长, 如果数据量大, 在遇到宕机的情况下, 可能会丢失部分数据. 另外, RDB 是通过配置使达到某种条件的时候才执行, 如果在这段时间内宕机, 那么这部分数据也会丢失.
AOF 方式, 在相同数据集的情况下, 文件大小会比 RDB 方式的大.
AOF 的持久化方式也是通过配置的不同, 默认配置的是每秒同步, 最快的模式是同步每一个命令, 最坏的方式是等待系统执行 fsync 将缓冲同步到磁盘文件中, 大部分操作系统是 30s. 通常情况下会配置为每秒同步一次, 所以最多会有 1s 的数据丢失.
怎样的同步方式更好?
RDB 和 AOF 方式结合. 起一个定时任务, 每小时备份一份服务器当前状态的数据, 以日期和小时命名, 另外起一个定时任务, 定时删除无效的备份文件 (比如 48 小时之前).AOF 配置为 1s 一次. 这样一来, 最多会丢失 1s 的数据, 同时如果 redis 发生雪崩, 也能迅速恢复为前一天的状态, 不至于停止服务.
总结
Redis 的持久化方案也不是一成不变的, 纸上的理论还需要结合实践成果来证明其可行性.
原创文章, 文笔有限, 才疏学浅, 文中若有不正之处, 万望告知.
来源: https://juejin.im/post/5ac468c7518825556919337c