Redis 的持久化

Redis 是一个非关系型的内存数据库,使用内存存储数据是它能够进行快速存取数据的原因之一。

在实际应用中,常有人提倡把 Redis 只作为一种能够提高用户体验的组件来使用, 也就是说即使 Redis 服务挂掉之后也要保证系统正常使用。不过,在很多系统中还是希望既能发挥 Redis 基于内存快速存取的特性,又希望机器断电或 Redis服务停止后数据不丢失。所以,才引出了 Redis 的持久化功能。

在许多技术文章中,提到 Redis 的持久化时往往都会直接抛出两个名词 RDB 和 AOF。然后接下来就是分别介绍这两个名词。当然,如果要谈 Redis 的持久化肯定避免不了讲 RDB 和 AOF,但这是介绍持久化最恰当的方式吗?这样的文章是不是显得有些生硬呢?所以,在尝试弄明白一个事物的原理时一定要从头到尾的思考它存在的意义?为了解决什么问题?采用了什么方式?达到了什么目的?自己有没有其它的方案?这样从问题的源头切入,才能对这个事物理解的更加深刻,从而能够更好的帮助自己进行举一反三。而不是人云亦云,对于一些知识仅仅是背诵下来,这种死记硬背下来的知识在脑海里的保质期也是短的可怜。

在前面,我们已经提到为什么需要引入持久化?简单的来说持久化就是把内存中的数据存储到外存上,这样服务停止后,当再启动的时候就可以把外存的数据读取到内存中从而达到了不丢失数据的目的。

RDB

如果让你设计一个持久化的方案,你会怎么做呢?(假装绞尽脑汁… …)首先,我们可以使用一种简单的策略,将 Redis 中所有的数据按照一定格式全部写到磁盘上,即创建数据的快照文件。然后,你为了尽量保证不丢数据需要考虑使用实时写还是定时写,又或者用其它策略。其实,现在的你已经在尝试着去实现 RDB (Redis Database)持久化的机制了。所以,你看它其实并不难。万丈高楼从地起,先从一个简单的 idea 开始,逐渐去完善它,丰富它的过程便是解决问题的过程。例如用这种思路去学习计算机网络也是同样适用的,你可以给自己出一个问题“如何让两台电脑进行通信?”,自己想办法解决这个问题的过程肯定会比在计算机网络课堂上收获的知识更多,也更牢固。

尽管不需要我们写代码来实现 RDB 持久化,但是并不妨碍我们来思考一下假如让我们来实现的话大概会遇到哪些问题?例如:什么时候生成数据快照?文件数据格式的定义?如果在主进程中进行持久化,阻塞客户端的请求后会不会有影响?接下来,我们就看一下 RDB 是如何做的吧。

基本命令

在 Redis 中,提供了两个 RDB 持久化的命令: SAVE BGSAVE 。执行 SAVE 时,Redis 服务会停止处理任何客户端的命令请求;执行 BGSAVE 时,Redis 服务则会创建一个子进程,由子进程来负责数据的持久化,而此时 Redis 服务就可以正常处理客户端的请求。

BGSAVE 解决了我们对于持久化时是否会影响 Redis 服务处理客户端的请求的担心。

自动间隔性保存

自动间隔性保存,则解决了“什么时候生成数据快照?”的问题。在 Redis 的配置文件中我们可以写入以下配置:

save 600 1
save 300 10
save 60 100
save 30 1000

上面的配置表示,如果在 600 秒内对数据库进行了 1 次修改,就执行执行一次 BGSAVE 命令;如果在 300 秒内对数据库进行了 10 次修改,就执行一次 BGSAVE 命令;以此类推。你可以根据你的业务场景,配置 save 的参数,也不仅仅局限于 4 条配置。

实现原理

在 Redis 启动时,会把上述配置存储到 Redis 服务器的状态中,具体的结构体则是 redisServer,存储 save 参数的结构体为 saveparam。

// Redis 服务器状态信息结构体
struct redisServer {
    // ... ...

    // 记录多个 save 配置参数
    struct saveparam *saveparams;
    // 修改次数计数器
    long long dirty;
    // 上次执行保存的时间
    time_t lastsave;
    
    // ... ...
}
// Save 参数结构体 saveparam 
struct saveparam {
    // 秒数
    time_t seconds;
    // 修改数
    int changes;
}

看到上面 redisServer 结构体的属性信息,你心里应该有答案了吧?dirty 表示的是自从上次执行 SAVE 或者 BGSAVE 命令完成之后对数据库进行修改的次数;lastsave 表示的是上次成功执行SAVE 或者 BGSAVE 命令的时间。这个时候,如果再有个机制能够定时检查是否有满足条件的配置参数就可以了。

Redis 提供了一个周期性操作函数 serverCron,每 100 ms 会执行一次。它其中的一项工作就是来检查是否有符合条件的 save 参数,如果存在符合条件的参数则执行 BGSAVE 命令,执行完毕之后将 dirty 和 lastsave 的值重置。相信只要有基础的编程知识,根据这些变量就能实现这个检查的过程吧。

文件结构

RDB 文件结构示意图

在上图中,大写字母的单词表示的常量,小写字母单词则是变量和数据。RDB 文件开头的“REDIS”是我们习惯称为的魔数,类似于 class 文件的 COFFEE,用来识别文件类型;紧接着长度为四个字节的 db_version 记录的是 RDB 文件的版本号;database 表示的是所存储的数据;EOF 则表明数据内容结束了;check_sum 的值是整个文件的校验和,用来检查文件是否损坏。

AOF

其实持久化数据除了 RDB 这种方式,肯定会有同学能想到另一种方式,就是把服务端执行的所有客户端请求增加、修改和删除等会改变数据的命令全都存储起来。通过存储这些命令数据,在遇到机器宕机和服务进程异常中断的情况下重启服务时只要执行一遍这些持久化的命令即可恢复之前的数据了。(也是一个相当好的办法呀!)

原理就是如此,那么问题来了,假如同样让你来实现这个过程,你会考虑到哪些问题呢?

一是性能问题,执行完命令之后是否直接将此命令持久化到磁盘上还是由操作系统控制文件同步?在这个问题上如何做取舍?二是文件大小问题,随着 Redis 服务运行越来越久,数据文件势必会越来越大?应该使用什么办法解决?… …

我们来看下 Redis 的 AOF 的过程吧!

持久化过程

首先,通过在配置文件中增加一行配置 appendonly yes 来开启 AOF 持久化。

像 RDB 机制所依赖 redisServer 结构体中的 saveparams、dirty、lastsave 参数一样,AOF 的实现依赖 redisServer 结构体中的 aof_buf 参数。

struct redisServer{
    // ... ...
    
    // AOF 缓冲区
    sds aof_buf;

   // ... ...
}

aof_buf 参数用来以协议格式缓存会对数据进行变更的命令。

在 Redis 服务器执行完命令,并将命令以协议的格式追加到 aof_buf 缓冲区之后,在当前这个事件循环结束之前,Redis 还会调用一个函数 flushAppendOnlyFile,这个函数会根据配置文件中 appendfsync 的值来决定接下来的持久化行为。appendfsync 有三个可选值,分别是 always、everysec、no

  • always: 将 aof_buf 缓冲区中的内容写入并同步到 AOF 文件。(性能最低,安全最高)
  • everysec: 将 aof_buf 缓冲区中的内容写入到 AOF 文件,如果上次同步 AOF 文件的时间距离现在超过一秒钟,那么再次对 AOF 文件同步,并且这个同步是由一个线程专门负责的。(同时兼顾性能与安全,推荐)
  • no: 将 aof_buf 缓冲区中的内容写入到 AOF 文件,但并不负责对 AOF 文件的同步,把同步的控制权交由操作系统控制。(性能最高,安全最低)

在 MySQL 持久化 binlog 日志数据时,也面临了同样的问题,即是写入后立即同步还是过段时间再同步。MySQL提供了一个参数 sync_binlog 来控制,当此参数的值为 0 时表示每次提交事务都只负责写入 binlog ,什么时候同步到磁盘由操作系统决定;当参数值为 1 的时表示每次提交事务都会执行 fsync 同步操作,这也是数据安全级别最高的配置。另外你也可以配置为 N(N>1),表示每次提交事务都 wirte 但是累积 N 个事务后才执行 fsync 同步操作。比较常见的配置是将此参数设置为 100—1000 中的某个值。

以上就是 AOF 持久化的基本过程。

数据载入

由于命令数据是以协议格式存储至文件中的,所以在启动 Redis 服务时检测到 AOF 文件的存在后会启动载入程序。(如果 RDB 和 AOF 持久化的文件同时存在则会优先载入 AOF 文件数据)

启动载入程序后,其载入过程如下图所示:

AOF 文件载入过程

AOF 重写

在前面,我们提到 AOF 的这种机制会造成 AOF 数据文件越来越大,并且可能会存在许多无意义的命令。例如,先执行了一个命令 set chang xuan ,随后又执行了命令 del chang 。其实这两条语句都会被持久化到 AOF 文件中,但实际上除了能证明曾经执行过这两条命令之外对于我们要持久化数据的目的而言并没有什么作用。

对此,Redis 提供了 AOF 重写的机制。

Redis 的 AOF 重写其实是根据当前存储的数据,生成命令的过程。并且会采用一些策略尽量减小 AOF 文件的大小,例如对于 List 中的数据会尽量使用较少的命令操作较多的数据。当然,如果在当前进程中进行重写处理并且数据量特别大的情况下肯定会阻塞客户端的请求,所以和 RDB 一样,Redis 提供了 AOF 后台重写的机制。

后台重写(BGREWRITEAOF)

AOF 通过 fork 子进程的方式进行后台重写有两个优点:

  1. 重写期间服务器进程可以继续处理请求。
  2. 子进程带有服务器进程的数据副本,能充分利用操作系统提供的写时复制机制从而提升效率,还可以在避免使用锁的情况下保证数据的安全性。

天下没有免费的午餐,这种方式还带来一个问题。就是在使用子进程重写期间,如果父进程还在处理着客户端请求,如何保证重写后 AOF 文件数据的一致性呢?

对于这个问题,Redis 设置了一个 AOF 重写缓冲区。在子进程被创建后,Redis 服务器就会启用这个重写缓冲区。在将命令以协议格式追加到 AOF 缓冲区之后,同时也会追加到 AOF 重写缓冲区。

当子进程完成重写工作后会向父进程发送一个信号。父进程接收到信后之后会进行调用相关函数,进行以下工作:

  1. 将 AOF 重写缓冲区中的内容写入到新的 AOF 文件中。
  2. 对新的 AOF 文件进行改名,原子地覆盖现有的 AOF 文件,完成新旧文件的替换。

这时,就完成了一次 AOF 后台重写。

总结

通过前文内容,我们可以大致清楚 Redis 所提供的 RDB 和 AOF 两种持久化机制的过程以及基本原理。它们各有特点,也各有适合使用的场景所以并不能说谁一定比谁好。通过搭配使用,能够确保线上环境数据的安全性就是最好的。

发布者

Avatar photo

常轩

总要做点什么吧!

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注