头图

00 前言

很多小伙伴都用 Redis 做缓存,那如果 Redis 服务器宕机,内存中数据全部丢失,应该如何做数据恢复呢?有人说很简单呀,直接从 MySQL 数据库再读回来就得了

这种方式存在两个问题:一是频繁访问 MySQL 数据库,有一定的风险;二是,从界面上来看,从 MySQL 读就不如从 Redis 快。

远哥远哥,那咋办呀?教教我吧。

我用中指抵着小胖的下吧,说到:傻瓜,我们可以做持久化呀。Redis 的持久化分两种,一种是 AOF,另一种是 RDB。来,坐哥哥腿上,我给你好好说道说道。

老规矩先上张脑图:

Redis 持久化

0.1 什么是持久化?

持久化(Persistence),即把数据(如内存中的对象)保存到可永久保存的存储设备中(如磁盘)。持久化的主要应用是将内存中的对象存储在数据库中,或者存储在磁盘文件中、XML 数据文件中等等。持久化是将程序数据在持久状态和瞬时状态间转换的机制。

0.2 往期精彩

1、小胖问我:select 语句是怎么执行的?

2、女朋友问我:MySQL 索引的原理是怎样的?

3、小胖问我:MySQL 日志到底有啥用?

4、老王问我:MySQL 事务与 MVCC 原理是怎样的?

5、女朋友问我:MySQL 的锁机制是怎样的?

6、万字长文,38 图爆肝 Redis 基础!

01 怎么理解 Redis 的单线程?

必须声明一点:Redis 的单线程,是指 Redis 的网络 IO 和键值对读写是由一个线程(主线程)完成的,这也是 Redis 对外提供键值存储服务的主要流程。但 Redis 的其他功能,比如持久化、异步删除、集群数据同步等,其实是由额外的线程执行的。

1.0 Redis 快的原因?

基于内存

  • 数据都存储在内存里,减少了一些不必要的 I/O 操作,操作速率很快。

高效的数据结构

  • 底层多种数据结构支持不同的数据类型,支持 Redis 存储不同的数据;
  • 不同数据结构的设计,使得数据存储时间复杂度降到最低。

合理的线程模型

  • I/O 多路复用模型同时监听多个客户端连接;
  • 单线程在执行过程中不需要进行上下文切换,减少了耗时。

02 AOF 持久化

AOF(Append Only File) 持久化是通过保存 Redis 服务器所执行的写命令来记录数据库状态,也就是每当 Redis 执行一个改变数据集的命令时(比如 SET), 这个命令就会被追加到 AOF 文件的末尾。

修改 redis.conf 配置文件,默认是 appendonly no(关闭状态),将 no 改为 yes 即可开启 AOF 持久化:

appendonly yes

在客户端输入如下命令也可,但是 Redis 服务器重启后会失效。

192.168.17.101:6379> config set appendonly yes
OK

AOF 持久化功能的实现可以分为命令追加(append)、文件写回磁盘两个步骤。

2.0 命令追加

AOF 持久化功能开启时,Redis 在执行完一个写命令之后,会将被执行的写命令追加到服务器状态的 aof_buf 缓冲区的末尾,此时缓冲区的记录还没有写入到 appendonly.aof 文件中

2.0.1 AOF 的格式

AOF 保存的是 Redis 的写命令,比如:执行命令set testkey testvalue,它存储的内容如下图所示:

AOF 格式

其中,“*3” 表示当前命令有三个部分,每部分都是由 $+数字**开头,后面紧跟着具体的命令、键或值。这里,**数字**表示这部分中的命令、键或值一共有多少字节。例如,**$3 set 表示这部分有 3 个字节,也就是set命令。

2.0.2 写后日志有啥优缺点?

AOF 记录日志的方式被称为写后日志,也就是先执行命令再记录,而 MySQL 中的 redo log、binlog 等都是写前日志。它的写入流程是下图这样的:

写 AOF 流程

写后有什么优点?

  • 记录 AOF 时不会对命令进行语法检查 ,写后就只记录了执行成功的命令。(避免保存的错误的命令,恢复的时候就完犊子了)
  • 执行完之后再记录,不会阻塞当前的写操作

写后有什么缺陷?

  • 如果执行完一个命令还没来得及写日志就宕机了会造成响应数据丢失。
  • AOF 的写入由主线程处理,如果写入时出现较长耗时,那就会影响主线程处理后续的请求。

你发现没有?写后的两个缺陷都是 AOF 的写入磁盘时相发生的,我们来看看它是怎么写入的呢?

2.1 AOF 写入磁盘

AOF 提供了三个选择,也就是 AOF 配置项 appendfsync 的三个可选值。

  • Always,同步写回:每个写命令执行完,立马同步地将日志写回磁盘;
  • Everysec(默认),每秒写回:每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,每隔一秒把缓冲区中的内容写入磁盘;
  • No,操作系统控制的写回:每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘。

2.1.0 三种策略的优缺点

针对避免主线程阻塞和减少数据丢失问题,这三种写回策略都无法做到两全其美。主要原因是:

  • Always(同步写回) 基本不丢数据,但是它在每一个写命令后都有一个慢速的落盘操作,影响主线程性能;
  • No(操作系统控制的写回)在写完缓冲区后,继续执行后续的命令,但是落盘的时机已经不在 Redis 手中了,只要 AOF 记录没有写回磁盘,一旦宕机对应的数据就丢失了;
  • Everysec(每秒写回)采用一秒写回一次的频率,避免了 Always 的性能开销,虽然减少了对系统性能的影响,但是如果发生宕机,上一秒内未落盘的命令操作仍然会丢失。

三种策略的优缺点

总结一下就是:想高性能,选择 No 策略;想高可靠性,选择 Always 策略;允许数据有一点丢失,又希望性能别受太大影响,选择 Everysec 策略。

2.2 AOF 恢复数据

不说了,看图:

AOF恢复数据

2.3 AOF 重写

我不知道你发现没有?AOF 文件是不断地将写命令追加到文件的末尾来记录数据库状态的。写命令不断增加,AOF 体积也越来越大。

有些命令是执行多次更新同一条数据,但其实它是可以合并成同一条命令的。比如:LPUSH 对列表数据做了 6 次更改,但 AOF 只需要记录最后一次更改。因为日志恢复时,只需要执行最后一次更改的命令即可

为了处理这种情况,Redis 提供了 AOF 的重写机制。它的多变一功能,把 6 条写命令合并成一条。如下所示:

LPUSH

如果你的某些键有成百上千次的修改,重写机制节约的空间就很可观了。

2.3.1 触发重写

有两种触发的方法,一个是调用命令 BGREWRITEAOF;一个是修改配置文件参数。

# 方式一
192.168.17.101:6379> BGREWRITEAOF
Background append only file rewriting started

# 方式二
auto-aof-rewrite-percentage 100 #当前AOF文件大小和上一次重写时AOF文件大小的比值
auto-aof-rewrite-min-size 64mb  #文件的最小体积

2.3.2 重写步骤

  1. 创建子进程进行 AOF 重写
  2. 将客户端的写命令追加到 AOF 重写缓冲区
  3. 子进程完成 AOF 重写工作后,会向父进程发送一个信号
  4. 父进程接收到信号后,将 AOF 重写缓冲区的所有内容写入到新 AOF 文件中
  5. 对新的 AOF 文件进行改名,覆盖现有的 AOF 文件

重写步骤

2.4 相关配置

# 是否开启AOF功能
appendonly no

# AOF文件件名称
appendfilename "appendonly.aof"

# 写入AOF文件的三种方式
appendfsync always
appendfsync everysec
appendfsync no

# 重写AOF时,是否继续写AOF文件
no-appendfsync-on-rewrite no

# 自动重写AOF文件的条件
auto-aof-rewrite-percentage 100 #百分比
auto-aof-rewrite-min-size 64mb #大小

# 是否忽略最后一条可能存在问题的指令
aof-load-truncated yes

2.5 优缺点

优点

  1. AOF 文件可读性高,分析容易
  2. AOF 文件过大时,自动进行重写
  3. 追加形式,写入时不需要再次读取文件,直接加到末尾

缺点

  1. 相同数据量下,AOF 一般比 RDB 大
  2. AOF 恢复时需要重放命令,恢复速度慢
  3. 根据 fsync 策略,AOF 的速度可能慢于 RDB

03 RDB 持久化

RDB 持久化是指在客户端输入 save、bgsave 或者达到配置文件自动保存快照条件时,将 Redis 在内存中的数据生成快照保存在名字为 dump.rdb(文件名可修改)的二进制文件中。

3.1 save 命令

save 命令会阻塞 Redis 服务器进程,直到 RDB 文件创建完毕为止,在 Redis 服务器阻塞期间,服务器不能处理任何命令请求。 在客户端输入 save

127.0.0.1:6379> save
OK

快照生成完毕,会弹出 DB saved ondisk 的提示。

1349:M 25 Apr 13:16:48.935 * DB saved on disk

3.2 bgsave 命令

bgsave 执行时,主线程会创建一个子进程,专门用于写入 RDB 文件,避免了主线程的阻塞,这也是 Redis RDB 文件生成的默认配置

127.0.0.1:6379> bgsave
Background saving started
PS:bgsave 命令执行期间 SAVE 命令会被拒绝;不能同时执行两个 BGSAVE 命令;不能同时执行 BGREWRITEAOF 和 BGSAVE 命令。

3.3 bgsave 时写数据

bgsave 执行时,Redis 主线程能正常读写数据。读操作时,主线程和 bgsave 子线程互不影响;写操作时,Redis 会利用写时复制技术(Copy-On-Write, COW),生成被修改数据的副本。然后 bgsave 子线程把副本数据写入 RDB。

比如,bgsave 期间,主线程修改键值对 C,过程如下:

写时复制技术

但是在这过程中发生宕机了咋办?比如,T0 时刻做了一次快照,T0+t 时刻又做了一次。但是 t 时间内主线程修改完数据 5 和 9,然后 Redis 宕机了,RDB 没记录到修改后的数据

Redis 重启恢复数据,就会出现数据 5 和 9 丢失的情况,没办法恢复。

丢失数据

这该咋办?我们需要记住那些数据被修改了

3.4 混合持久化

如下图所示,记录 t 时刻被修改的数据就需要占用额外的空间,而 Redis 是内存数据库,空间非常宝贵。所以,直接记录到内存这种方式不可取

增量快照

内存开销比较小的方法是把 t 时间的增量写操作记录到 AOF 日志中,这样既保留了 RDB 的快速恢复,也没占用额外的空间。

如图,T1 和 T2 时刻的修改,用 AOF 日志记录,等第二次做全量快照时,清空 AOF 日志,因为此时的修改都记录到快照中了,恢复不用 AOF 日志了。

AOF 记录增量修改

庆幸的是 Redis 4.0 就开始提供了这种RDB + AOF 的持久化方式,开启的配置项是aof-use-rdb-preamble yes,它需要配合 AOF 的重写机制实现。

# 开启混合持久化
redis> config set aof-use-rdb-preamble yes
OK
# AOF 重写
redis> BGREWRITEAOF
Background append only file rewriting started

在没有第二次做全量快照之前,它的格式是这样的:前半部分是 RDB 格式,后半部分是 AOF 增量日志。如果这个时候宕机,直接拿 appendonly.aof 恢复数据。

appendonly.aof格式

3.5 RDB 优缺点

优点

  1. 二进制数据,恢复时比 AOF 快
  2. RDB 的 bgsave 方式主线程不阻塞

缺点

  1. Redis 意外宕机 时,会丢失部分数据(混合持久化可解决)
  2. 当数据量比较大时,fork 的过程是非常耗时的,fork 子进程时是会阻塞的,在这期间 Redis 是不能响应客户端的请求的。

04 如何选择?

  1. 数据不能丢失时,选择内存快照和 AOF 混合使用;
  2. 如果允许分钟级别的数据丢失,可以只使用 RDB;
  3. 如果只用 AOF,优先使用 everysec 的配置选项,因为它在可靠性和性能之间取了一个平衡。

05 数据恢复流程

数据恢复流程

07 总结

本文主要讲解 Redis AOF 、RDB持久化的原理和两者的优缺点,对比两者后,我还给你总结了 Redis 混合持久化的流程。最后给了你一些选择持久化方案的建议,希望看完你能有所收获。

全文将近五千字,12张图,希望能帮到你。好啦,以上就是狗哥关于 Redis 持久化的总结。感谢各技术社区大佬们的付出,尤其是极客时间,真的牛逼。如果说我看得更远,那是因为我站在你们的肩膀上。希望这篇文章对你有帮助,我们下篇文章见~

08 大厂面试题 & 电子书

如果看到这里,喜欢这篇文章的话,请帮点个好看

初次见面,也不知道送你们啥。干脆就送几百本电子书2021最新面试资料吧。微信搜索JavaFish回复电子书送你 1000+ 本编程电子书;回复面试送点面试题;回复1024送你一套完整的 java 视频教程。

面试题都是有答案的,如下所示:有需要的就来拿吧,绝对免费,无套路获取

面试题

巨人的肩膀

  • 《Redis设计与实现》
  • juejin.cn/post/6844903648976175118
  • time.geekbang.org/column/article/272852
  • blog.csdn.net/wsdc0521/article/details/106765809
  • blog.csdn.net/weixin_33810006/article/details/90394921

JavaFish
310 声望58 粉丝