Redis 的数据 所有存储 在 内存 中,若是 忽然宕机,数据就会所有丢失,所以必须有一套机制来保证 Redis 的数据不会由于故障而丢失,这种机制就是 Redis 的 持久化机制,它会将内存中的数据库状态 保存到磁盘 中。html
咱们来稍微考虑一下 Redis 做为一个 "内存数据库" 要作的关于持久化的事情。一般来讲,从客户端发起请求开始,到服务器真实地写入磁盘,须要发生以下几件事情:python
详细版 的文字描述大概就是下面这样:git
注意: 上面的过程实际上是 极度精简 的,在实际的操做系统中,缓存 和 缓冲区 会比这 多得多...程序员
若是咱们故障仅仅涉及到 软件层面 (该进程被管理员终止或程序崩溃) 而且没有接触到内核,那么在 上述步骤 3 成功返回以后,咱们就认为成功了。即便进程崩溃,操做系统仍然会帮助咱们把数据正确地写入磁盘。github
若是咱们考虑 停电/ 火灾 等 更具灾难性 的事情,那么只有在完成了第 5 步以后,才是安全的。web
因此咱们能够总结得出数据安全最重要的阶段是:步骤3、4、五,即:redis
咱们从 第三步 开始。Linux 系统提供了清晰、易用的用于操做文件的 POSIX file API
,20
多年过去,仍然还有不少人对于这一套 API
的设计津津乐道,我想其中一个缘由就是由于你光从 API
的命名就可以很清晰地知道这一套 API 的用途:数据库
int open(const char *path, int oflag, .../*,mode_t mode */); int close (int filedes);int remove( const char *fname ); ssize_t write(int fildes, const void *buf, size_t nbyte); ssize_t read(int fildes, void *buf, size_t nbyte);
因此,咱们有很好的可用的 API
来完成 第三步,可是对于成功返回以前,咱们对系统调用花费的时间没有太多的控制权。缓存
而后咱们来讲说 第四步。咱们知道,除了早期对电脑特别了解那帮人 (操做系统就这帮人搞的),实际的物理硬件都不是咱们可以 直接操做 的,都是经过 操做系统调用 来达到目的的。为了防止过慢的 I/O 操做拖慢整个系统的运行,操做系统层面作了不少的努力,譬如说 上述第四步 提到的 写缓冲区,并非全部的写操做都会被当即写入磁盘,而是要先通过一个缓冲区,默认状况下,Linux 将在 30 秒 后实际提交写入。安全
可是很明显,30 秒 并非 Redis 可以承受的,这意味着,若是发生故障,那么最近 30 秒内写入的全部数据均可能会丢失。幸亏 PROSIX API
提供了另外一个解决方案:fsync
,该命令会 强制 内核将 缓冲区 写入 磁盘,但这是一个很是消耗性能的操做,每次调用都会 阻塞等待 直到设备报告 IO 完成,因此通常在生产环境的服务器中,Redis 一般是每隔 1s 左右执行一次 fsync
操做。
到目前为止,咱们了解到了如何控制 第三步
和 第四步
,可是对于 第五步,咱们 彻底没法控制。也许一些内核实现将试图告诉驱动实际提交物理介质上的数据,或者控制器可能会为了提升速度而从新排序写操做,不会尽快将数据真正写到磁盘上,而是会等待几个多毫秒。这彻底是咱们没法控制的。
Redis 快照 是最简单的 Redis 持久性模式。当知足特定条件时,它将生成数据集的时间点快照,例如,若是先前的快照是在2分钟前建立的,而且如今已经至少有 100 次新写入,则将建立一个新的快照。此条件能够由用户配置 Redis 实例来控制,也能够在运行时修改而无需从新启动服务器。快照做为包含整个数据集的单个 .rdb
文件生成。
但咱们知道,Redis 是一个 单线程 的程序,这意味着,咱们不只仅要响应用户的请求,还须要进行内存快照。然后者要求 Redis 必须进行 IO 操做,这会严重拖累服务器的性能。
还有一个重要的问题是,咱们在 持久化的同时,内存数据结构 还可能在 变化,好比一个大型的 hash 字典正在持久化,结果一个请求过来把它删除了,但是这才刚持久化结束,咋办?
操做系统多进程 COW(Copy On Write) 机制 拯救了咱们。Redis 在持久化时会调用 glibc
的函数 fork
产生一个子进程,简单理解也就是基于当前进程 复制 了一个进程,主进程和子进程会共享内存里面的代码块和数据段:
这里多说一点,为何 fork 成功调用后会有两个返回值呢? 由于子进程在复制时复制了父进程的堆栈段,因此两个进程都停留在了 fork
函数中 (都在同一个地方往下继续"同时"执行),等待返回,因此 一次在父进程中返回子进程的 pid,另外一次在子进程中返回零,系统资源不够时返回负数。 (伪代码以下)
pid = os.fork() if pid > 0: handle_client_request() # 父进程继续处理客户端请求 if pid == 0: handle_snapshot_write() # 子进程处理快照写磁盘 if pid < 0: # fork error
因此 快照持久化 能够彻底交给 子进程 来处理,父进程 则继续 处理客户端请求。子进程 作数据持久化,它 不会修改现有的内存数据结构,它只是对数据结构进行遍历读取,而后序列化写到磁盘中。可是 父进程 不同,它必须持续服务客户端请求,而后对 内存数据结构进行不间断的修改。
这个时候就会使用操做系统的 COW 机制来进行 数据段页面 的分离。数据段是由不少操做系统的页面组合而成,当父进程对其中一个页面的数据进行修改时,会将被共享的页面复
制一份分离出来,而后 对这个复制的页面进行修改。这时 子进程 相应的页面是 没有变化的,仍是进程产生时那一瞬间的数据。
子进程由于数据没有变化,它能看到的内存里的数据在进程产生的一瞬间就凝固了,不再会改变,这也是为何 Redis 的持久化 叫「快照」的缘由。接下来子进程就能够很是安心的遍历数据了进行序列化写磁盘了。
快照不是很持久。若是运行 Redis 的计算机中止运行,电源线出现故障或者您 kill -9
的实例意外发生,则写入 Redis 的最新数据将丢失。尽管这对于某些应用程序可能不是什么大问题,但有些使用案例具备充分的耐用性,在这些状况下,快照并非可行的选择。
AOF(Append Only File - 仅追加文件) 它的工做方式很是简单:每次执行 修改内存 中数据集的写操做时,都会 记录 该操做。假设 AOF 日志记录了自 Redis 实例建立以来 全部的修改性指令序列,那么就能够经过对一个空的 Redis 实例 顺序执行全部的指令,也就是 「重放」,来恢复 Redis 当前实例的内存数据结构的状态。
为了展现 AOF 在实际中的工做方式,咱们来作一个简单的实验:
./redis-server --appendonly yes # 设置一个新实例为 AOF 模式
而后咱们执行一些写操做:
redis 127.0.0.1:6379> set key1 Hello OK redis 127.0.0.1:6379> append key1 " World!" (integer) 12 redis 127.0.0.1:6379> del key1 (integer) 1 redis 127.0.0.1:6379> del non_existing_key (integer) 0
前三个操做实际上修改了数据集,第四个操做没有修改,由于没有指定名称的键。这是 AOF 日志保存的文本:
$ cat appendonly.aof *2 $6 SELECT $1 0 *3 $3 set $4 key1 $5 Hello *3 $6 append $4 key1 $7 World! *2 $3 del $4 key1
如您所见,最后的那一条 DEL
指令不见了,由于它没有对数据集进行任何修改。
就是这么简单。当 Redis 收到客户端修改指令后,会先进行参数校验、逻辑处理,若是没问题,就 当即 将该指令文本 存储 到 AOF 日志中,也就是说,先执行指令再将日志存盘。这一点不一样于 MySQL
、LevelDB
、HBase
等存储引擎,若是咱们先存储日志再作逻辑处理,这样就能够保证即便宕机了,咱们仍然能够经过以前保存的日志恢复到以前的数据状态,可是 Redis 为何没有这么作呢?
Emmm... 没找到特别满意的答案,引用一条来自知乎上的回答吧:
- @缘于专一 - 我甚至以为没有什么特别的缘由。仅仅是由于,因为AOF文件会比较大,为了不写入无效指令(错误指令),必须先作指令检查?如何检查,只能先执行了。由于语法级别检查并不能保证指令的有效性,好比删除一个不存在的key。而MySQL这种是由于它自己就维护了全部的表的信息,因此能够语法检查后过滤掉大部分无效指令直接记录日志,而后再执行。
- 更多讨论参见:为何Redis先执行指令,再记录AOF日志,而不是像其它存储引擎同样反过来呢? - https://www.zhihu.com/question/342427472
Redis 在长期运行的过程当中,AOF 的日志会越变越长。若是实例宕机重启,重放整个 AOF 日志会很是耗时,致使长时间 Redis 没法对外提供服务。因此须要对 AOF 日志 "瘦身"。
Redis 提供了 bgrewriteaof
指令用于对 AOF 日志进行瘦身。其 原理 就是 开辟一个子进程 对内存进行 遍历 转换成一系列 Redis 的操做指令,序列化到一个新的 AOF 日志文件 中。序列化完毕后再将操做期间发生的 增量 AOF 日志 追加到这个新的 AOF 日志文件中,追加完毕后就当即替代旧的 AOF 日志文件了,瘦身工做就完成了。
AOF 日志是以文件的形式存在的,当程序对 AOF 日志文件进行写操做时,其实是将内容写到了内核为文件描述符分配的一个内存缓存中,而后内核会异步将脏数据刷回到磁盘的。
就像咱们 上方第四步 描述的那样,咱们须要借助 glibc
提供的 fsync(int fd)
函数来说指定的文件内容 强制从内核缓存刷到磁盘。但 "强制开车" 仍然是一个很消耗资源的一个过程,须要 "节制"!一般来讲,生产环境的服务器,Redis 每隔 1s 左右执行一次 fsync
操做就能够了。
Redis 一样也提供了另外两种策略,一个是 永不 fsync
,来让操做系统来决定合适同步磁盘,很不安全,另外一个是 来一个指令就 fsync
一次,很是慢。可是在生产环境基本不会使用,了解一下便可。
重启 Redis 时,咱们不多使用 rdb
来恢复内存状态,由于会丢失大量数据。咱们一般使用 AOF 日志重放,可是重放 AOF 日志性能相对 rdb
来讲要慢不少,这样在 Redis 实例很大的状况下,启动须要花费很长的时间。
Redis 4.0 为了解决这个问题,带来了一个新的持久化选项——混合持久化。将 rdb
文件的内容和增量的 AOF 日志文件存在一块儿。这里的 AOF 日志再也不是全量的日志,而是 自持久化开始到持久化结束 的这段时间发生的增量 AOF 日志,一般这部分 AOF 日志很小:
因而在 Redis 重启的时候,能够先加载 rdb
的内容,而后再重放增量 AOF 日志就能够彻底替代以前的 AOF 全量文件重放,重启效率所以大幅获得提高。
- 本文已收录至个人 Github 程序员成长系列 【More Than Java】,学习,不止 Code,欢迎 star:https://github.com/wmyskxz/MoreThanJava
- 我的公众号 :wmyskxz,我的独立域名博客:wmyskxz.com,坚持原创输出,下方扫码关注,2020,与您共同成长!
很是感谢各位人才能 看到这里,若是以为本篇文章写得不错,以为 「我没有三颗心脏」有点东西 的话,求点赞,求关注,求分享,求留言!
创做不易,各位的支持和承认,就是我创做的最大动力,咱们下篇文章见!