性能文章>一文读懂Redis持久化机制>

一文读懂Redis持久化机制原创

1年前
201722

 

前言

我们日常开发中,使用Redis的普遍场景就是用作缓存。也就是把后端数据库的数据存储在内存中,然后从内存读取数据,响应速度会非常的快。并且使用缓存还会降低数据库的访问压力。但是这里也有一个绝对不能忽视的问题:一旦服务器宕机,内存中的数据就会全部丢失。

为了保证数据的持久性,Redis提供了两种持久化方案:AOF日志和RDB快照。我们可以根据实际情况,在项目中灵活配置。

下面我们首先来看看AOF日志。


AOF日志

Redis默认不开启AOF持久化方式,我们可以修改redis.conf文件配置开启:

# 开启aof机制
appendonly yes

# aof文件名
appendfilename "appendonly.aof"

# 写入策略 默认 everysec
# appendfsync always
appendfsync everysec
# appendfsync no

# 自动重写配置
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb

# 保存目录
dir ./

AOF是写后日志。跟MySQL的写前日志(WAL)相反。写前日志指的是,在实际写数据前,先把修改的数据记录到日志文件,以便故障时进行恢复。写后的意思是,Redis先执行命令,把数据写入内存,然后再记录日志到磁盘。

看到这里,我们就要想了,为什么AOF要先执行命令,再记日志呢?

Redis为了避免额外的性能开销,在向AOF里面记日志的时候,并不会先去检查命令的语法正确性,而是先让系统执行命令,只有执行成功之后,这条命令才会被记录下来,否则,系统就会报错。所以写后日志好处之一就是,防止出现错误命令的问题。

此外,另一个好处就是,因为是在命令执行之后,才去记录日志,所以不会阻塞当前的写操作。

当然了写后日志也会带来一定风险。

首先第一个:数据丢失。如果执行完一个命令,还没来得及写日志系统就发生宕机了,此时就会发生数据丢失的风险。

其次,AOF日志是在主线程中执行的,如果在日志写入磁盘的时候,磁盘写压力大,就会导致写盘很慢。我们都知道,Redis是单线程的,如果主线程发生阻塞,就导致后续的操作都无法进行。

那么如何解决这两个风险呢?

聪明的同学应该发现了,这两个风险都跟AOF写回磁盘的时机相关。如果我们能找到一种合适的时机,这两个风险是不是就能避免呢?我们继续看。

AOF三种写回策略

在Redis的配置文件中,有这样几个配置:

# appendfsync always
appendfsync everysec
# appendfsync no
  • always:同步写回,每个写命令执行完,立马同步地将日志写回磁盘;

  • everysec:每秒写回,Redis默认写回策略,即每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,每隔一秒把缓冲区中的内容写入磁盘;

  • no:由操作系统控制的写回,每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘。

 

我们来总结一下这三种策略

  • always:这种策略很安全,它能基本做到不丢失数据,但是每个写命令之后都有一个落盘的操作,所以它对系统的影响也是最大的;

  • no:这种策略最不安全,因为落盘的时机不在redis手中,一旦发生宕机对应的数据就会丢失;

  • everysec:避免了always策略的性能开销,也降低了no策略的丢失风险,最多可能会丢失1s的数据,它算是在二者之间取了个折中。

 

三种我们策略应该如何选择呢

  • 想要系统的高性能选 no策略;

  • 想要高可靠性就选择 always策略;

  • 如果二者兼容的话只有 everysec策略啦。

 

注意,到这里没有结束哦。我们虽然按照系统的需求选择了写回策略,但是AOF是以文件的形式记录接收到的命令的,这时候随着写入命令的不断增加,AOF文件的体积会变得越来越大。

如果AOF太大,再往里面追加命令的时候,效率就会降低。而一旦发生宕机,用AOF恢复的速度也会非常慢。

为了避免这种问题,接下来继续AOF的重写机制。

AOF重写机制

简单点说就是根据原有的AOF文件,重新创建一个新的AOF文件,只不过这个新的AOF文件比原来的更小。

那么Redis是怎么把文件变小的呢?

原来,Redis的重写机制具有多变一的功能,也就是检查数据库的键值对,记录下键值对的最终状态,从而实现对某个键值对多次操作产生的多条命令压缩为一条的效果。

我们知道,AOF文件是以追加的方式,记录接收到的命令。当对一个键值对反复修改时,就会记录多条命令。然而在重写的时候只是记录下了当前的最新状态,这样就实现了多变一。

看到这,大家可能会问了,既然Redis是单线程的,它既要执行写入命令,同时又要同步日志到磁盘,这里又冒出来一个重写机制,但是它响应的速度依然很快,这到底是咋回事呢?

Redis为了避免阻塞主线程,导致数据库性能下降。就会创建一个子线程—bgrewriteaof由子线程完成重写过程

重写过程:

  1. 首先,主线程fork出bgrewriteaof子线程;同时也会把主线程的内存拷贝一份给子线程,这里的拷贝指的是子进程复制了父进程页表,此时子线程可以共享访问父进程的内存数据了;

  2. 然后,子线程就可以将新的内容记入重写日志了;注意,是重写日志!!!

  3. 对于新的操作命令,继续由父线程处理,Redis会把这个操作记录到正在使用的 AOF 日志的缓冲区,这样一来就不用担心宕机问题。同样,也会在重写日志的缓冲区也记录一份;

  4. 当子线程完成重写工作之后,缓冲区里的这些新的操作也会记录到新的AOF 文件。此时,我们就可以用新的 AOF 文件替代旧文件了。

 

重写触发的时机:

  1. 手动触发:手动发送bgrewriteaof指令;

  2. 自动触发:涉及到两个配置参数,只有AOF文件大小同时超出下面这两个配置项时,会触发AOF重写

    a、auto-aof-rewrite-min-size:AOF重写时文件的最小大小,默认为64MB;

    b、auto-aof-rewrite-percentage:重写百分比,当前AOF文件比上一次重写后AOF文件的增量大小,和上一次重写后AOF文件大小的比值。

 

到这里AOF日志基本就介绍完了,接下来我们继续看看另一种持久化方法:内存快照

RDB

RDB(Redis DataBase)内存快照,是Redis默认的持久化方式。具体就是将某一时刻的内存数据以文件的形式保存到磁盘上

请注意,这里是保存的是数据!!! 不是操作。所以,在数据恢复的时候,我们就可以直接把RDB文件读入内存,快速完成恢复。

RDB相关配置

# 备份的频率:900秒内至少一个键被更改则进行快照
save 900 1
save 300 10
save 60 10000

# 快照创建出错后,是否继续执行写命令
stop-writes-on-bgsave-error yes

# 是否对快照文件进行压缩
rdbcompression yes

# 文件名称
dbfilename dump.rdb

# 文件保存位置
dir ./

RDB持久化流程

首先我们要确认的是,我们在给内存数据做快照的时候,做的是全量快照,因为我们的数据都在内存中,为了确保可靠性,就必须把内存中的所有数据都记录到磁盘中。

Redis给我们提供了两个命令来创建快照:分别是 save bgsave

  • save :在主线程中执行,会导致阻塞;

  • bgsave:bgsave命令会fork一个子进程,专门用于写入RDB文件,避免了主线程的阻塞,这也是 Redis RDB 文件生成的默认配置

 

这个时候,我们就可以通过bgsave命令来执行全量快照,这样既提供了数据的可靠性保证,同时也避免了对redis的性能影响。

接下来,我们需要关注一个问题。在对内存数据做快照时,这些数据还能被修改吗?

如果能修改,意味着Redis还能正常处理写操作,否则的话,就要等所有快照写完才能执行,这会大大降低性能。

这里我们先给出答案:在对内存做快照时,这些数据肯定还是可以被修改的。
RDB采用写时复制(COW,copy on write)策略。在执行快照的同时,正常处理写操作。

简单来说,Redis在持久化时会调用glibc的函数fork,产生一个子进程,此时快照持久化就交给子进程来处理,父进程则继续处理客户端请求。

子进程在做持久化的时候,不会对现有的内存数据结构进行修改,它只是进行遍历读取,然后序列化写到磁盘中。但是父进程不一样,它必须持续接受客户端请求,然后对内存数据结构进行修改。

如下图所示:如果主线程对数据是读操作,那么,主线程和子进程相互不影响。如果主线程要修改一块数据,那么这块数据就会被复制一份,生成该数据的副本。然后主线程对这个副本进行修改。

这既保证了快照的完整性,也允许主线程同时对数据进行修改,避免了对正常业务的影响。

快照的频率

为了提高系统的可靠性,防止宕机导致的数据丢失,我们肯定希望快照的时间越短越好。我们可能会想,通过bgsave子线程来执行快照,这样既不会阻塞主线程,同时也尽可能地少丢失数据。但是这样真的是完美的吗?

答案是否定的。

虽然 bgsave 执行时不阻塞主线程,但是如果频繁的执行全量快照也会有两方面的开销

  • 频繁将全量数据写入磁盘,会给磁盘带来很大压力;

  • bgsave 子进程需要通过 fork 操作从主线程创建出来,虽然子进程在创建之后不会阻塞主线程,但是在fork的时候本身就会阻塞主线程,如果频繁的调用fork创建子进程,就会频繁的阻塞主线程了。

那我们需要如何处理呢?

此时,我们可以做增量快照,也就是,在做了一次全量快照后,后续的快照只对修改的数据进行快照记录,这样可以避免每次全量快照的开销。但是,这么做的前提是,我们需要记住哪些数据被修改了。这会带来额外的空间开销问题。

两种持久化方式对比

AOF每次记录的是操作命令,一般需要持久化的数据量不大。只要不是设置的always方式,对性能不会造成太大影响。但是在数据恢复时,需要把所有的命令都执行一遍。如果操作日志很多,Redis恢复的速度就会很慢,可能会影响到正常使用。

而RDB快照的方式就弥补了这一点,它每次记录的是数据,Redis在故障恢复的时候速度就会很快。但是,RDB的问题是,它执行快照的频率不好控制,如果频率太快会对系统带来性能影响,如果频率太慢就会造成更多的数据丢失。

那么,有没有方法既能利用 RDB 的快速恢复,又能以较小的开销做到尽量少丢数据呢?

当然有,下面继续Redis 4.0 混合持久化

合持久化

混合持久化,就是将 RDB 文件的内容和增量的 AOF 日志文件存在一起。

简单来说,内存快照是以一定频率执行的,那么在两次快照之间,使用AOF 日志记录发生的增量操作。如下图所示:

 

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

这个方法既能享受到 RDB 文件快速恢复的好处,又能享受到 AOF 只记录操作命令的简单优势,提高了数据恢复效率,以及数据的可靠性。

点赞收藏
分类:标签:
ShawnBlog

我是 Shawn 一 Java 后端开发。欢迎关注我的公众号「ShawnBlog」。

请先登录,查看2条精彩评论吧
快去登录吧,你将获得
  • 浏览更多精彩评论
  • 和开发者讨论交流,共同进步

为你推荐

解锁 ElasticJob 云原生实践的难题

解锁 ElasticJob 云原生实践的难题

【云原生•监控】Micrometer打造SpringBoot服务的可观测能力

【云原生•监控】Micrometer打造SpringBoot服务的可观测能力

一文讲透消息队列RocketMQ实现消费幂等

一文讲透消息队列RocketMQ实现消费幂等

一次 Rancher go 应用内存占用过高问题排查

一次 Rancher go 应用内存占用过高问题排查

2
2