redis——P3:持久化

虽然缓存功能已经实现,但是作为对外提供服务的软件开发者,不能只关注是否提供了正确的服务,稳定和快速恢复等等指标是同样非常非常重要的。

考虑这样一个问题,redis确实因为不可抗力宕机了(假设我喜欢黑框框打开,然后手贱按了^C),于是瞬间redis里所有缓存全没了?这对于一个追求高性能、高可用的系统来说是不可容忍的。所以,如果有一个机制可以快速恢复数据,让这些缓存还能用,那是很好的。

下面介绍两个持久化方式,并分别指明它们的优劣。同时值得注意的是,理解持久化方式,尤其是缓冲区,对于redis集群的实现的了解有一定帮助,我会尽力写明。

RDB(Redis Database)

RDB是一种持久化的方式,思路是保存当前内存的快照到磁盘。如果发生宕机,则在再次启动时读rdb文件(安装目录下的dump.rdb),将数据再次加载到内存,以提高缓存命中率。不过,在这一部分我需要提前说明我的观点,我认为这种持久化方式更多在于要生成当前时刻内存中缓存的版本,而非所谓保存最新的数据到磁盘!最新的数据是保存不过来的。过分执着地追求所谓最新的数据,一定会带来性能上的消耗!

一起来看redis的配置文件中的文档:

################################ SNAPSHOTTING  ################################
#
# Save the DB on disk:
#
#   save <seconds> <changes>
#
#   Will save the DB if both the given number of seconds and the given
#   number of write operations against the DB occurred.
#
#   In the example below the behaviour will be to save:
#   after 900 sec (15 min) if at least 1 key changed
#   after 300 sec (5 min) if at least 10 keys changed
#   after 60 sec if at least 10000 keys changed
#
#   Note: you can disable saving completely by commenting out all "save" lines.
#
#   It is also possible to remove all the previously configured save
#   points by adding a save directive with a single empty string argument
#   like in the following example:
#
#   save ""

save 900 1
save 300 10
save 60 10000

可以发现,redis的持久化操作是以配置文件的配置为标准进行的,计算在多少秒内有多少变化(write operations),满足条件即会触发rdb。关于rdb,我认为有几点值得注意:

  • 满足上面的条件触发;
  • 手动执行rdb有两个命令,save和bgsave,save会阻塞主进程,即在主进程中做持久化操作,不会接收任何命令;bgsave和被动触发的逻辑相似,都是会fork一个子线程来执行。另外bgsave 带有一个schedule参数,当有其它子进程在执行的时候这个命令会滞后执行。两个命令之后不再解释;
  • fork出子进程、使用子进程持久化,而不是多线程,想必设计者一定有他的用意,是什么呢?
  • 保存的dump.rdb是一个内存快照,实际会丢失数据或非最新数据;我认为rdb的主要目的就是创建一份内存快照!(下面还会再提到)

从上面的特点可以容易地推断,rdb会缺失一部分数据或不是最新的数据,这一点网上很多地方归结于是redis的rdb的缺点,但是我个人不这样认为。我认为rdb的目的就是创建一份快照,并且尽可能地将最新的数据持久化到硬盘。同时,rdb快照的特性对于集群的数据同步有很大作用,这会在集群的部分再次说明。那么,既然本身目的就不是完全是保存全部数据,那么可能也不能称之缺点。(个人观点,如果从redis在历史版本中默认关闭AOF方式来看,也有可能是一开始的没有很好的持久化设计,不过个人不太敢这么想,还是认为大佬有大佬的想法较好)

被动执行的rdb的流程是什么样的?

比如当监控到如900s内有一次更新时,就会触发被动复制。主进程fork出一个子进程,由子进程去读取内存的全量数据并且写入rdb文件。

但是,我们知道,父子进程之间是会有一些共享的数据的,子进程创建伊始,父子进程内存中指向的都是同一个页帧。当备份时,有请求过来修改缓存的内容时会发生什么呢?这涉及到操作系统虚拟内存的知识,简单介绍一下操作系统的COW(Copy on Write):当创建子进程时,操作系统会让父进程和子进程的内存指向同一片区域,并将这片区域标记为只读,这样不管是谁尝试修改这片内存时都会触发一个异常,那么操作系统的异常处理程序就将复制一个新的页框,修改那个数据,然后将新的页复制给尝试修改的程序,而另外一个进程指向的地址依旧不变。也即当有写操作过来时,主进程即业务进程是会陷入中断的。不过,redis利用这个特性,可以保证最后生成的快照文件一定是开始备份时的数据,并且尽可能地减少开销。其大部分操作为读操作,所以一般需要复制新页的操作较少,不过如果当时的写操作太多,明显地,也会拖垮主线程的性能。如果数据集很大的话,fork()比较耗时,结果就是,当数据集非常大并且CPU性能不够强大的话,redis会停止服务端几毫秒甚至一秒。

AOF(Append Only File)

AOF顾名思义,只会增加的文件。前面提到,RDB只是一个快照,两个快照之间的数据就有丢失的可能。但是其实,任何不立即持久化的缓存都会造成数据的丢失,所以,默认情况下AOF也并不能保证全部是最新的数据,即并不100%安全。但是,AOF提供了安全的方式,只是会牺牲性能。

升级一个策略,就是针对该策略的痛点作出优化。RDB的痛点在于:当进行RDB操作时,过程中执行的命令不会被记录。所以概括来说,AOF所做的就是,每次来了“写”命令和“删”命令时,都放在一个aof缓冲区内,根据服务器配置的aof刷新策略,仅仅(only)以追加(append)的方式刷新磁盘写入到aof文件(file)中。当然,执行这个策略也一定会带来一些其他问题,再针对性地进行解决,以上方案+解决方案构成了完整的aof策略。

其他

解释了基本的概念之后,说说对它的理解。首先不难看出,这是一个类似于mysql的binlog一样的日志文件。从redis开始提供服务开始,就积累全部的增删改命令,按照一定的时间间隔(即配置文件中提供的flush配置)调用操作系统提供的强制刷盘接口将日志写入磁盘。且每次都是以追加的形式写入文件,照比rdb保存全量数据的方式,aof刷盘显然更快,无所谓在主进程还是子进程。

很多人应该听说过aof重写这个概念,这很有必要:既然是对所有数据所有操作的日志文件,那针对同一个数据来说,只有最后一条数据操作的结果是有效的,而中间状态的日志只会白白地浪费磁盘空间。同时,由于利用aof文件恢复数据需要完整的执行全部的日志,多余的命令也是浪费时间。那它是如何做的呢?

AOF重写

主要的流程是:开辟一个子进程,创建一个临时文件,并非去读旧的aof文件一条条解析命令,而是直接去读内存中的数据,并将它构造成一个命令写入临时文件。而过程中,主进程依然会接收到命令(所以AOF重写同样地,COW问题理应依然存在)。这些命令除了写到刚刚提到的aof_buf中以外,还会写入aof_rewrite_buf_blocks缓冲区。

很快,子进程就完成了重写,这时会用进程通信的方式向主进程发出信号,然后将这一部分新的命令写入文件,替换掉原来的文件,结束。请注意,这一部分操作会造成主进程的阻塞,需要停止接收命令,否则可能造成这个新的文件永远也写不完。

AOF和RDB混合

4.0后开启了一种新的机制,即这一部分的标题。在重写aof的过程中,我们发现它其实也是加载内存中的全量数据,与rdb无二,所以,redis采取了一种更简单的方式,将rdb文件写入到新的aof临时文件的头部(二进制形式),然后后半部分则以增量的形式将新的命令还按照aof文件格式协议写入。这也是为什么有了aof以后,aof和rdb都打开,且系统恢复的时候只使用aof但是rdb方式依然存在的一个原因。

这种方式形成的aof文件,一部分能看懂一部分看不懂 ,并且由于格式不同,新旧版本的aof文件并不能兼容,如果升级,则需要将集群内所有的redis实例全部升级,不然跟不上潮流的实例可能完全看不懂别人发来的aof文件是什么意思。

使用场景

挖坑待填,这一部分比较考验对于场景的理解,以及在什么时候选择什么技术,暂时不具备这种能力,无法对写出的文字负责,因此暂时只做自己待学的内容。感谢阅读,期待批评。

posted @ 2024-06-18 19:21  pidanhub  阅读(19)  评论(0)    收藏  举报