Redis 持久化机制

Redis 的数据全部在内存里,如果突然宕机,数据就会全部丢失,因此必须有一种机制来保证 Redis 的数据不会因为故障而丢失,这种机制就是 Redis 的持久化机制。

Redis 持久化机制 有两种,第一种是RDB (快照),第二种是AOF日志。

快照是一次全量备份,AOF 日志是连续的增量备份。快照是内存数据的二进制序列化形式,在存储上非常紧凑,而 AOF 日志记录的是内存数据修改的指令记录文本。AOF 日志在长期的运行过程中会变的无比庞大,数据库重启时需要加载 AOF 日志进行指令重放,这个时间就会无比漫长。所以需要定期进行 AOF 重写,给 AOF 日志进行瘦身

A:RDB
我们知道 Redis 是单线程程序,这个线程要同时负责多个客户端套接字的并发读写操作和内存数据结构的逻辑读写。

在服务线上请求的同时,Redis 还需要进行内存快照,内存快照要求 Redis 必须进行文件 IO 操作,可文件 IO 操作是不能使用多路复用 API

Redis 使用操作系统的多进程COW(Copy On Write)机制来实现快照持久化。redis持久化的时候会fork一个子进程,RDB持久化完全交给子进程来处理,父进程继续处理客户端请求。子进程刚刚产生时,它和父进程共享内存里面的代码段和数据段。这是linux操作系统的机制,为了节约内存资源,所以尽可能的让它们共享起来。

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

这个时候就会使用操作系统的 COW 机制来进行数据段页面的分离。数据段是由很多操作系统的页面组合而成,当父进程对其中一个页面的数据进行修改时,会将被共享的页面复制一份分离出来,然后对这个复制的页面进行修改。这时子进程相应的页面是没有变化的,还是进程产生时那一瞬间的数据。

随着父进程修改操作的持续进行,越来越多的共享页面被分离出来,内存就会持续增长。但是也不会超过原有数据内存的 2 倍大小。另外一个 Redis 实例里冷数据占的比例往往是比较高的,所以很少会出现所有的页面都会被分离,被分离的往往只有其中一部分页面。每个页面的大小只有 4K,一个 Redis 实例里面一般都会有成千上万的页面。

子进程因为数据没有变化,它能看到的内存里的数据在进程产生的一瞬间就凝固了,再也不会改变,这也是为什么 Redis 的持久化叫「快照」的原因。接下来子进程就可以非常安心的遍历数据了进行序列化写磁盘了。

RDB 持久化过程:
执行bgsave 指令时,Redis会先触发bgsaveCommand进行当前状态检查:看是否在进行RDB持久化,如果是 直接返回。(AOF持久化时候也会影响,在AOF处再讨论这一情况)。如果没有,执行fork指令,记录下fork的时间和一些其他信息之后主进程就去响应其他命令了,子进程开始进行RDB 持久化。为什么 Redis 使用子进程而不是线程来进行后台 RDB 持久化呢?主要是出于Redis性能的考虑,我们知道Redis对客户端响应请求的工作模型是单进程和单线程的,如果在主进程内启动一个线程,这样会造成对数据的竞争条件。所以为了避免使用锁降低性能,Redis选择启动新的子进程,独立拥有一份父进程的内存拷贝,以此为基础执行RDB持久化。
但是需要注意的是,fork 会消耗一定时间,并且父子进程所占据的内存是相同的,当 Redis 键值较大时,fork 的时间会很长,这段时间内 Redis 是无法响应其他命令的。除此之外,Redis 占据的内存空间会翻倍。大致流程:
首先打开一个临时文件,
调用 rdbSaveRio函数(会遍历Redis 当前所有数据库),将当前 Redis 的内存信息写入到这个临时文件中,
接着调用 fflush(从应用层刷到内核)、fsync(将内核缓存刷到物理设备) 和 fclose 接口将文件写入磁盘中,
使用 rename 将临时文件改名为 正式的 RDB 文件,
最后记录 dirty 和 lastsave等状态信息。这些状态信息在 serverCron时会使用到(dirty 记录着有多少键值发生变化,lastsave记录着上次 RDB 持久化的时间。)

相关参数配置:redis.conf
注意这里是要同时满足 save 后面的2个参数的条件。
save 900 1 (15min 内 至少1条key改变)就产生快照
save 300 10 (5分钟内 至少10条key被改变)就产生快照
save 60 10000 (1分钟内 至少10000条被改变)就产生快照

dbfilename dump.rdb 文件名称
dir /home/work/app/redis/data/ rdb文件存储的路径
rdbcomporess yes //开启rdb文件压缩

手动触发 bgsave 会产生一个dump.rdb 文件,打开来看,有redis的版本信息,还有些乱码:

自动检查保存条件是否满足:
Redis 的服务器周期性操作函数 serverCron 默认每隔 100 毫秒就会执行一次,该函数用于对正在运行的服务器进行维护,它的其中一项工作就是检查 save 选项所设置的保存条件是否已经满足,如果满足的话,就执行 BGSAVE 命令。

B: AOF 原理

AOF 日志存储的是Redis 服务器的顺序指令序列,AOF日志只记录对内存进行修改的指令记录。

AOF的持久化流程:
所有的写命令会追加到AOF缓冲区中,AOF缓冲区根据对应的策略向硬盘进行同步操作。随着AOF文件越来越大,需要定期对AOF文件进行重写,达到压缩目的。当Reids重启时,可以加载AOF文件进行数据恢复。

Redis提供了bgrewriteaof指令来对AOF日志重写,防止AOF日志过于臃肿导致重放的时候耗时过长。

AOF 文件重写并不需要对现有的 AOF 文件进行任何读取、分析或者写入操作,而是通过读取服务器当前的数据库状态来实现的。

Redis重写AOF流程,redis 执行 fork() ,现在同时拥有父进程和子进程。
子进程开始将新 AOF 文件的内容写入到临时文件。
对于所有新执行的写入命令,父进程一边将它们累积到一个内存缓存(重写缓冲区aof_rewrite_buffer)中,一边将这些改动追加到现有 AOF 文件的末尾,这样即使在重写的中途发生停机,现有的 AOF 文件也还是安全的。
当子进程完成重写工作时,它给父进程发送一个信号,父进程在接收到信号之后,将内存缓存中的所有数据追加到新 AOF 文件的末尾。
搞定!现在 Redis 原子地用新文件替换旧文件,之后所有命令都会直接追加到新 AOF 文件的末尾。

AOF日志以文件的形式存在,当程序对AOF日志文件进行写操作时,实际上是将内容写到了内核为文件描述符分配的一个内存缓存中,然后内核会异步将数脏数据刷回到磁盘中。linux提供了一个fsync函数,可以把数据刷回磁盘。
相关参数配置:
appendonly no:是否开启AOF

appendfilename “appendonly.aof”:AOF文件名
no-appendfsync-on-rewrite no:AOF重写期间是否禁止fsync;如果开启(默认是关闭)该选项,可以减轻文件重写时CPU和硬盘的负载(尤其是硬盘),但是可能会丢失AOF重写期间的数据;需要在负载和安全性之间进行平衡。

auto-aof-rewrite-percentage 100:代表当前AOF文件空间和上一次重写后AOF文件空间的百分比值。默认100 代表 当前aof的大小超过上次rewrite大小的一倍时自动触发aof。
auto-aof-rewrite-min-size 64mb:表示运行AOF重写时文件最小体积,默认为64mb。
aof-load-truncated yes:如果AOF文件结尾损坏,Redis启动时是否仍载入AOF文件

fsync持久化策略

appendfsync always   每次有数据修改发生时都会写入AOF文件。
appendfsync no 这个永不 是根据系统刷新输出缓冲区的时间来决定的,一般来说是30s
appendfsync everysec  默认选项 折中方案,兼顾速度和安全

手动触发AOF 重写 在redis-cli 输入 bgrewriteaof

然后再查看appendonlyaof
明显有很多重复的set指令减少了。

Redis AOF 文件还原数据库状态的详细步骤如下:
创建一个不带网络连接的的伪客户端( fake client),因为 Redis 的命令只能在客户端上下文中执行,而载入 AOF 文件时所使用的的命令直接来源于 AOF 文件而不是网络连接,所以服务器使用了一个没有网络连接的伪客户端来执行 AOF 文件保存的写命令,伪客户端执行命令的效果和带网络连接的客户端执行命令的效果完全一样的。
2 从 AOF 文件中分析并取出一条写命令。
3 使用伪客户端执行被读出的写命令。
一直执行步骤 2 和步骤3,直到 AOF 文件中的所有写命令都被处理完毕为止。

RDB的优点
a> 文件紧凑,体积小,网络传输快,适合全量复制;
恢复速度比AOF快很多。
当然与AOF想比,RDB最重要的优点之一是对性能的影响相对较小。

RDB缺点:
1)做不到实时持久化。
2)数据可能大量丢失。
3)RDB文件需要满足特定格式,兼容性差。
4)RDB fork的时候 非常耗时 这时候不能响应客户端请求。

AOF的优点
优点在于支持秒级持久化,兼容性好,
可以重写文件。
缺点
1)文件大
2)恢复速度慢、对性能影响大。

tips: 你也可以同时开启两种持久化方式, 在这种情况下, 当redis重启的时候会优先载入AOF文件来恢复原始的数据,因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集要完整.

总结:

RDB持久化是由redis fork出一个子进程(redis-rdb-bgsave),子进程对数据结构进行遍历读取,然后序列化写到磁盘中。在子进程序列化期间,父进程仍然处理客户端的命令,如果这时候redis宕机可能会丢失一部分数据。
相关参数 dbfilename,dir,rdbcompress

AOF持久化方式:redis 需要记录redis 每条写命令,因此AOF不需要触发,redis 写命令追加到aof_buffer(feedAppendOnlyFile,如果在重写 还需要再追加到重写缓冲区里面),然后由write函数写进系统缓存,fsync写磁盘。兼顾安全和性能的同步策略是AOF_FSYNC_EVERYSEC 可能会丢失2s的数据,因为每两次刷新磁盘的频率一定要大于等于2s。
相关参数: appendfsync no
appendfsync always
appendfsync everysec

AOF里面有个优秀的设计 缓存重用 小于4K的缓存被重用否则释放AOF缓存。

參考链接:
https://gsmtoday.github.io/2018/07/30/redis-01/
https://blog.csdn.net/carlosfu/article/details/84746430

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注