虽然缓存功能已经实现,但是作为对外提供服务的软件开发者,不能只关注是否提供了正确的服务,稳定和快速恢复等等指标是同样非常非常重要的。
考虑这样一个问题,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会停止服务端几毫秒甚至一秒。
可为什么偏偏是多进程?这样的事情交给一个线程去做不好吗?单机的redis影响的确不大,但是涉及到集群时,就会产生误会,这之后再说。不过,即便如此,多线程来缓存数据,有两个事情必须二选一:放弃SNAPSHOT这个特点,遇到修改操作时加锁阻塞主进程直到备份完毕。使用子进程则不存在这两个问题,最多有一些数据不是最新。不过考虑redis的特点我们就会知道,完全保证持久化所有数据是很困难的,都会损失一些数据。所以纠结这一点并没有特别大的意义。