当前,我们已经深入了解了Redis中的AOF(Append-Only File)持久化方法,它的优势在于记录操作命令,不会显著增加持久化数据量。通常情况下,只要你没有选择always的持久化策略,AOF方法对性能的影响是相对较小的。
然而,由于AOF方法记录的是操作命令而不是实际的数据,所以在使用AOF进行故障恢复时,需要逐一执行所有的操作日志。当操作日志非常庞大时,这个恢复过程会变得非常缓慢,从而影响了正常的使用体验。显然,这并不是我们理想的情况。那么,是否有其他方法既可以保障数据可靠性,又能在宕机后实现快速恢复呢?
当然有,这就是我们今天要一同探讨的另一种持久化方法:内存快照。内存快照的概念很像拍照,它记录了某一时刻的内存中数据状态,就像照片一样。当你给朋友拍照时,一张照片能完美地捕捉朋友的瞬间。对于Redis,它通过将某一时刻的数据状态以文件形式写入磁盘来实现这种效果,这个文件就是快照,通常称为RDB文件,其中RDB代表Redis数据库。
RDB文件的优势在于,即使发生宕机,快照文件也不会丢失,因此可靠性得到了保证。接下来,我们将深入了解RDB持久化方法,包括它的工作原理和配置等方面的细节。这将有助于你更好地理解如何选择适当的持久化方法,以满足你的特定需求。
与AOF持久化相比,RDB持久化记录的是某一时刻的数据状态,而不是每个操作命令。这意味着在数据恢复过程中,我们可以直接将RDB文件加载到内存中,迅速完成恢复。听起来似乎非常理想,但内存快照也并不是毫无缺点的最佳选项。为什么会这样呢?
我们需要考虑两个关键问题:
- 哪些数据进行快照?这关系到快照执行的效率问题。
- 在进行快照时,数据是否能够被修改、新增或删除?这关系到Redis是否会被阻塞,以及是否能够同时处理其他请求。
这或许还不够清晰,我来用拍照的例子进行解释。当我们拍照时,通常要思考两个问题:
- 如何构图?也就是我们打算在照片中捕捉哪些人或物体。
- 在按下快门之前,需要确保拍摄对象不乱动,以避免照片模糊。
你可以看到,这两个问题非常重要,接下来,我们将详细讨论这两个问题,首先是“构图”问题,即我们应该选择哪些数据进行快照。
给哪些内存数据做快照?
Redis 的数据都在内存中,为了提供所有数据的可靠性保证,它执行的是全量快照,也就是说,把内存中的所有数据都记录到磁盘中,这就类似于给 100 个人拍合影,把每一个人都拍进照片里。这样做的好处是,一次性记录了所有数据,一个都不少。
当你给一个人拍照时,只用协调一个人就够了,但是,拍 100 人的大合影,却需要协调 100 个人的位置、状态,等等,这当然会更费时费力。同样,给内存的全量数据做快照,把它们全部写入磁盘也会花费很多时间。而且,全量数据越多,RDB 文件就越大,往磁盘上写数据的时间开销就越大。
对于 Redis 而言,它的单线程模型就决定了,我们要尽量避免所有会阻塞主线程的操作,所以,针对任何操作,我们都会提一个灵魂之问:“它会阻塞主线程吗?”RDB 文件的生成是否会阻塞主线程,这就关系到是否会降低 Redis 的性能。
Redis 提供了两个命令来生成 RDB 文件,分别是 save 和 bgsave。
save:在主线程中执行,会导致阻塞;
bgsave:创建一个子进程,专门用于写入 RDB 文件,避免了主线程的阻塞,这也是 Redis RDB 文件生成的默认配置。
好了,这个时候,我们就可以通过 bgsave 命令来执行全量快照,这既提供了数据的可靠性保证,也避免了对 Redis 的性能影响。
接下来,我们要关注的问题就是,在对内存数据做快照时,这些数据还能“动”吗? 也就是说,这些数据还能被修改吗?这个问题非常重要,这是因为,如果数据能被修改,那就意味着 Redis 还能正常处理写操作。否则,所有写操作都得等到快照完了才能执行,性能一下子就降低了。
快照时数据能修改吗?
在给别人拍照时,一旦对方动了,那么这张照片就拍糊了,我们就需要重拍,所以我们当然希望对方保持不动。对于内存快照而言,我们也不希望数据“动”。
举个例子。我们在时刻 t 给内存做快照,假设内存数据量是 4GB,磁盘的写入带宽是 0.2GB/s,简单来说,至少需要 20s(4/0.2 = 20)才能做完。如果在时刻 t+5s 时,一个还没有被写入磁盘的内存数据 A,被修改成了 A’,那么就会破坏快照的完整性,因为 A’不是时刻 t 时的状态。因此,和拍照类似,我们在做快照时也不希望数据“动”,也就是不能被修改。
但是,如果快照执行期间数据不能被修改,是会有潜在问题的。对于刚刚的例子来说,在做快照的 20s 时间里,如果这 4GB 的数据都不能被修改,Redis 就不能处理对这些数据的写操作,那无疑就会给业务服务造成巨大的影响。
你可能会想到,可以用 bgsave 避免阻塞啊。这里我就要说到一个常见的误区了,避免阻塞和正常处理写操作并不是一回事。此时,主线程的确没有阻塞,可以正常接收请求,但是,为了保证快照完整性,它只能处理读操作,因为不能修改正在执行快照的数据。
为了快照而暂停写操作,肯定是不能接受的。所以这个时候,Redis 就会借助操作系统提供的写时复制技术(Copy-On-Write, COW),在执行快照的同时,正常处理写操作。
简单来说,bgsave 子进程是由主线程 fork 生成的,可以共享主线程的所有内存数据。bgsave 子进程运行后,开始读取主线程的内存数据,并把它们写入 RDB 文件。
此时,如果主线程对这些数据也都是读操作(例如图中的键值对 A),那么,主线程和 bgsave 子进程相互不影响。但是,如果主线程要修改一块数据(例如图中的键值对 C),那么,这块数据就会被复制一份,生成该数据的副本。然后,bgsave 子进程会把这个副本数据写入 RDB 文件,而在这个过程中,主线程仍然可以直接修改原来的数据。
写时复制机制保证快照期间数据可修改
这既保证了快照的完整性,也允许主线程同时对数据进行修改,避免了对正常业务的影响。
到这里,我们就解决了对“哪些数据做快照”以及“做快照时数据能否修改”这两大问题:Redis 会使用 bgsave 对当前内存中的所有数据做快照,这个操作是子进程在后台完成的,这就允许主线程同时可以修改数据。
现在,我们再来看另一个问题:多久做一次快照?我们在拍照的时候,还有项技术叫“连拍”,可以记录人或物连续多个瞬间的状态。那么,快照也适合“连拍”吗?
可以每秒做一次快照吗?
对于内存快照,所谓的“连拍”即连续执行多次快照。这将大大减小快照之间的时间间隔,即使在某一刻发生宕机,由于上一刻的快照刚刚执行,所丢失的数据也会降至最低。然而,快照间隔时间的选择成为关键。
如下图所示,我们在T0时刻首次执行了一次快照,然后在T0+t时刻再次执行了快照。在这段时间内,数据块5和9发生了修改。如果在t时间内出现宕机,只能按照T0时刻的快照进行恢复。这时,由于数据块5和9的修改没有被记录在快照中,它们的值将无法完全恢复。
快照机制下的数据丢失
所以,要想尽可能恢复数据,t 值就要尽可能小,t 越小,就越像“连拍”。那么,t 值可以小到什么程度呢,比如说是不是可以每秒做一次快照?毕竟,每次快照都是由 bgsave 子进程在后台执行,也不会阻塞主线程。
这种想法其实是错误的。虽然 bgsave 执行时不阻塞主线程,但是,如果频繁地执行全量快照,也会带来两方面的开销。
首先,频繁将完整的数据写入磁盘会对磁盘造成巨大的压力。多个快照争相使用有限的磁盘带宽,这可能导致前一个快照尚未完成,后一个快照已经开始,从而产生恶性循环。
另一方面,bgsave子进程需要通过fork操作从主线程中创建。虽然子进程在创建后不会再次阻塞主线程,但是fork这个创建过程本身会阻塞主线程。而且主线程的内存越大,阻塞时间越长。如果频繁进行fork以创建bgsave子进程,这将频繁地阻塞主线程。那么,有没有更好的方法呢?
在这种情况下,可以考虑使用增量快照。增量快照指的是在生成完整快照后,后续的快照只记录已更改的数据,从而避免每次生成完整快照的开销。
在第一次做完全量快照后,T1 和 T2 时刻如果再做快照,我们只需要将被修改的数据写入快照文件就行。但是,这么做的前提是,我们需要记住哪些数据被修改了。你可不要小瞧这个“记住”功能,它需要我们使用额外的元数据信息去记录哪些数据被修改了,这会带来额外的空间开销问题。如下图所示:
增量快照示意图
如果我们为每个键值对的修改都记录一条记录,那么当有1万个键值对被修改时,我们就需要额外记录1万条记录。有时,键值对可能非常小,例如只有32字节,而为了记录它们的修改,我们可能需要8字节的元数据信息。在某些情况下,为了记录修改所引入的额外空间开销会相当大。对于内存资源宝贵的Redis来说,这可能不是一个划算的选择。
从这里可以看出,虽然与AOF相比,快照的恢复速度更快,但快照的频率很难确定。如果频率太低,一旦在两次快照之间发生宕机,可能会有大量数据丢失。如果频率太高,将导致额外的开销。那么,有没有一种方法既能利用RDB的快速恢复,又能以较小的开销尽量减少数据丢失呢?
Redis 4.0 中提出了一个混合使用 AOF 日志和内存快照的方法。简单来说,内存快照以一定的频率执行,在两次快照之间,使用 AOF 日志记录这期间的所有命令操作。
这样一来,快照不用很频繁地执行,这就避免了频繁 fork 对主线程的影响。而且,AOF 日志也只用记录两次快照间的操作,也就是说,不需要记录所有操作了,因此,就不会出现文件过大的情况了,也可以避免重写开销。
如下图所示,T1 和 T2 时刻的修改,用 AOF 日志记录,等到第二次做全量快照时,就可以清空 AOF 日志,因为此时的修改都已经记录到快照中了,恢复时就不再用日志了。
内存快照和AOF混合使用
这个方法既能享受到 RDB 文件快速恢复的好处,又能享受到 AOF 只记录操作命令的简单优势,颇有点“鱼和熊掌可以兼得”的感觉,建议你在实践中用起来。
小结
在本篇文章中,我们深入学习了Redis用于避免数据丢失的内存快照方法。这种持久化方式具有快速恢复数据库的显著优势,只需将RDB文件直接加载到内存中,避免了AOF方式需要逐一重新执行操作命令所带来的性能低效问题。
然而,内存快照方法也存在一些限制。它类似于拍摄内存的“大合影”,这不可避免地会占用较多的时间和计算资源。虽然Redis采用了bgsave和写时复制等方式来最小化内存快照对正常读写操作的影响,但频繁的快照仍然可能对性能产生不可接受的压力。因此,将RDB和AOF方式混合使用,可以充分利用它们各自的优势,规避它们的弱点,以较小的性能开销来同时保证数据的可靠性和性能。
最后,关于选择AOF和RDB的问题,我愿意提供三点建议:
- 当数据绝不能丢失时,混合使用内存快照和AOF方式是一个明智的选择。
- 如果可以容忍分钟级别的数据丢失,可以只使用RDB方式。
- 如果决定仅采用AOF方式,首选使用everysec的配置选项,因为它在可靠性和性能之间取得了较好的平衡。