2024-04-06
原文作者:Brand 原文地址: https://www.cnblogs.com/wzh2010/p/18030902

1 介绍

从上一篇的 《深刻理解高性能Redis的本质》 中可以知道, 我们经常在数据库层上加一层缓存(如Redis),来保证数据的访问效率。
这样性能确实也有了大幅度的提升,因为从内存中取数远比从磁盘中快的多,但是本身Redis也是一层服务,也存在宕机、故障的可能性。
一旦服务挂起,可能生产的后果包括如下几方面:
1. Redis的数据是存在内存中的,所以一旦挂起,内存中的数据会全部丢失。
2. I/O从内存层级迁移到磁盘层级,性能极速下降。
3. 原本访问缓存的请求会透过缓存层直接投向数据库,给数据库带来极大的压力,甚至导致雪崩。

所以,缓存层崩溃产生的后果是灾难的。为了避免宕机和宕机后的数据丢失, 为了保证数据的快速恢复,Redis提供了两个持久化数据的能力,RDB Snapshot 和 AOF(Append Only FIle)日志。本章我们先来看看RDB快照的使用。

2 什么是RDB内存快照

大规模高并发的分布式场景,经常会遇到问题就是Redis挂起,导致访问失败,而所有的请求透过缓存层投向数据库,给数据库造成极大的压力,甚至雪崩。

而Redis的数据是存储在高速缓存中,即使我们重启并且恢复使用,缓存池依旧是空的,因为内存被释放了。
重新建立缓存的过程,对数据库也是一个暴击的过程,很可能会导致整个系统调用链的雪崩。参考我的这篇《架构与思维:一次缓存雪崩的灾难复盘
所以更为稳妥的办法是持久化到磁盘中,这样哪怕重启数据也不会消失。但是如果每次数据的变化(增、删、改)都要写内存并同时写磁盘,这样成本太高,内存+磁盘的持续数据同步,会让 Redis 性能大大降低。而且还要保证原子性操作,避免内存和磁盘的数据不一致。

2.1 使用内存快照

为了避免实时写入高频操作磁盘带来的负面效应。Redis提供了内存快照策略。
工作原理是,Redis在指定的时间间隔内,将内存中的数据集快照定格下来,写入磁盘,并存储在副本文件中。当Redis重启时,这些快照文件会被自动读取并恢复到内存中。打游戏的同学可以想象存盘,下一次恢复游戏,可以从存盘的地方读取游戏直接开始。

202404062053012971.png
如上图,将指定时间的Redis缓存数据进行快照。当发生故障的时候,直接从最接近的时间点进行数据恢复(即21:10的故障按照21点的RDB快照进行恢复),直接将 RDB 文件读入内存完成恢复。

2.2 生成RDB策略

在Redis的RDB持久化方案中,提供了两种模式来生成RDS文件,分别是 SAVE 和 BGSAVE。虽然都是用于创建内存快照并保存到磁盘的命令,但两者在执行方式和影响上有明显的区别。

SAVE命令会阻塞当前Redis服务器进程,直到RDB文件创建完毕。
在执行SAVE命令期间,Redis不能处理其他命令,阻塞主进程,这会导致服务器无法响应其他请求,直到RDB过程完成为止。因此,当数据量较大时,使用SAVE命令可能会对Redis的性能产生较大影响。

BGSAVE命令则会在后台异步进行快照操作,同时Redis还可以继续处理客户端的请求。
BGSAVE命令通过fork一个子进程来完成持久化任务,这样主进程就不会被阻塞,从而保证了Redis的高可用性。但是,由于需要fork一个子进程,BGSAVE命令可能会消耗更多的内存资源。

2.2.1 SAVE模式

save模式是主进程执行,非常不建议使用主进程执行的方式,在笔者的 《深刻理解高性能Redis的本质》 一文中,
我们介绍了它的主操作都是在单线程模型上完成的。所以 RDB 文件生成会影响主线程的网络I/O和键值对读写,导致客户端正常操作被阻塞,所以应该尽量避免。

2.2.2 BGSAVE模式

bgsave是后台异步执行,通过调用glibc函数创建一个子进程专门用于写入RDB文件,从而避免了主线程的阻塞。当执行BGSAVE命令时,Redis会继续处理其他客户端请求(比如Get、Set等),而子进程会在后台完成RDB文件的生成。这是Redis RDB文件生成的默认配置,也是推荐的方式。

202404062053016162.png
上图执行流程如下:

  1. 执行bgsave命令,Redis主进程判断当前是否存在正在执行的RDB/AOF子进程,若果存在则bgsave命令直接返回。
  2. 主进程执行fork操作创建子进程,fork操作过程中父进程会阻塞(创建子进程),通过info stats命令查看latest_fork_usec选项,可以获取最近一个fork操作的耗时,单位为微秒
  3. 父进程fork完成后,bgsave命令返回Background saving started信息,之后的操作都是异步的了,不再阻塞主进程,Client的Get、Set等操作依然可以执行。
  4. fork子进程的做法是通过调用glibc函数进行创建的,这步骤跟第2点对齐,都是会有短暂的阻塞。
  5. 子进程创建RDB文件,在主进程内存中生成临时快照文件,完成后对原有文件进行原子替换。执行lastsave命令可以获取最后一次生成RDB的时间,对应rdb_last_save_time选项。
  6. 子进程发送信号给主进程表示完成,主进程接受到信息并更新统计记录。

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

2.2.3 避免过频的全量Snapshot

虽然说Redis 使用 bgsave 函数 fork 子进程在后台完成内存中的数据做快照,并不阻塞父进程继续处理客户端的操作。
但过频执行全量数据快照,依然会导致严重的性能开销,主要如下:

  1. 频繁生成 RDB 文件写入磁盘,磁盘空间占用大,IO压力大,也会降低效率。
  2. fork 出来的 bgsave 子进程因为共享主线程的资源,一定程度上会影响主线程的运行性能。

2.3 总结

快照的恢复速度快,但是生成 RDB 文件的频率需要把握一个度,频率过低快照间隔数据较大,丢失的数据就会比较多;频率太快,又会消耗额外开销,降低Redis性能。
RDB内存快照优缺点如下:
优点:

  • RDB以一种二进制格式+数据压缩的方式写磁盘,文件轻量。
  • 数据恢复速度快,用于灾难恢复的场景,加载 RDB 恢复数据远快于 AOF 方式。

缺点:

  • 无法做到实时持久化,每次都要创建子进程, 频繁操作成本过高
  • 保存后的二进制文件, 存在老版本不兼容新版本 rdb 文件的问题
  • 数据恢复不完全,快照时间点和故障时间点之间必然有时间差、数据差
阅读全文