redis笔记(1)持久化方式

本文主要是对参考资料的整理记录,加入一些自己的思考和理解。

一 持久化

主要作用是数据备份,即将数据存储在硬盘,保证数据不会因进程退出而丢失。

Redis持久化分为RDB持久化和AOF持久化:前者将当前数据保存到硬盘,后者则是将每次执行的写命令保存到硬盘(类似于MySQL的binlog);由于AOF持久化的实时性更好,即当进程意外退出时丢失的数据更少,因此AOF是目前主流的持久化方式,不过RDB持久化仍然有其用武之地。

二 RDB持久化

RDB持久化是将当前进程中的数据生成快照保存到硬盘(因此也称作快照持久化),保存的文件后缀是rdb;当Redis重新启动时,可以读取快照文件恢复数据。

2.1 触发条件

手动触发

redis笔记(1)持久化方式

自动触发

自动触发最常见的情况是在配置文件中通过save m n,指定当m秒内发生n次变化时,会触发bgsave


save m n的原理
注意时间戳是成功执行的时间,不是开始执行的时间
redis笔记(1)持久化方式

2.2 bgsave执行流程

redis笔记(1)持久化方式

  1. Redis父进程首先判断:当前是否在执行save,或bgsave/bgrewriteaof(后面会详细介绍该命令)的子进程,如果在执行则bgsave命令直接返回。bgsave/bgrewriteaof 的子进程不能同时执行,主要是基于性能方面的考虑:两个并发的子进程同时执行大量的磁盘写操作,可能引起严重的性能问题。
  2. 父进程执行fork操作创建子进程,这个过程中父进程是阻塞的(只是相对于save,阻塞时间较短),Redis不能执行来自客户端的任何命令
  3. 父进程fork后,bgsave命令返回”Background saving started”信息并不再阻塞父进程,并可以响应其他命令
  4. 子进程创建RDB文件,根据父进程内存快照生成临时快照文件,完成后对原有文件进行原子替换
  5. 子进程发送信号给父进程表示完成,父进程更新统计信息

个人理解:fork出来的进程发信号给父进程时,父进程应该需要精确地知道本次bgsave中,生成的rdb文件是截止到哪个时间戳。这样才能保证save m n的准确执行

2.3RDB文件

  • RDB文件是经过压缩的二进制文件
  • RDB文件的存储路径既可以在启动前配置,也可以通过命令动态设定
  • 压缩默认开启;可以通过命令关闭
  • RDB文件的压缩并不是针对整个文件进行的!!!,而是对数据库中的字符串进行的,且只有在字符串达到一定长度(20字节)时才会进行。

2.4 启动时加载

RDB文件的载入工作是在服务器启动时自动执行的,并没有专门的命令。但是由于AOF的优先级更高!,因此当AOF开启时,Redis会优先载入AOF文件来恢复数据;只有当AOF关闭时,才会在Redis服务器启动时检测RDB文件,并自动载入。服务器载入RDB文件期间处于阻塞状态,直到载入完成为止

2.5 RDB常用配置总结

  • save m n:bgsave自动触发的条件;如果没有save m n配置,相当于自动的RDB持久化关闭,不过此时仍可以通过其他方式触发
  • stop-writes-on-bgsave-error yes:当bgsave出现错误时,Redis是否停止执行写命令;设置为yes,则当硬盘出现问题时,可以及时发现,避免数据的大量丢失;设置为no,则Redis无视bgsave的错误继续执行写命令,当对Redis服务器的系统(尤其是硬盘)使用了监控时,该选项考虑设置为no
  • rdbcompression yes:是否开启RDB文件压缩
  • rdbchecksum yes:是否开启RDB文件的校验,在写入文件和读取文件时都起作用;关闭checksum在写入文件和启动文件时大约能带来10%的性能提升,但是数据损坏时无法发现
  • dbfilename dump.rdb:RDB文件名
  • dir ./:RDB文件和AOF文件所在目录

三 AOF持久化

AOF持久化(即Append Only File持久化),是将Redis执行的每次写命令记录到单独的日志文件中(有点像MySQL的binlog);当Redis重启时再次执行AOF文件中的命令来恢复数据

3.1 开启AOF

Redis服务器默认开启RDB,关闭AOF;要开启AOF,需要在配置文件中配置:appendonly yes

3.2 执行流程

由于需要记录Redis的每条写命令,因此AOF不需要触发,下面介绍AOF的执行流程。

AOF的执行流程包括:

  1. 命令追加(append):将Redis的写命令追加到缓冲区aof_buf
  2. 文件写入(write)和文件同步(sync):根据不同的同步策略将aof_buf中的内容同步到硬盘
  3. 文件重写(rewrite):定期重写AOF文件,达到压缩的目的。

命令追加(append)

Redis先将写命令追加到缓冲区(内存中,这样速度才快;不然每次都持久化到磁盘,IO速度太慢了),而不是直接写入文件,

文件写入(write)和文件同步(sync)

Redis提供了多种AOF缓存区的同步文件策略,策略涉及到操作系统的write函数和fsync函数

为了提高文件写入效率,在现代操作系统中,当用户调用write函数将数据写入文件时,操作系统通常会将数据暂存到一个内存缓冲区里,当缓冲区被填满或超过了指定时限后,才真正将缓冲区的数据写入到硬盘里。这样的操作虽然提高了效率,但也带来了安全问题:如果计算机停机,内存缓冲区中的数据会丢失;因此系统同时提供了fsync、fdatasync等同步函数,可以强制操作系统立刻将缓冲区中的数据写入到硬盘里,从而确保数据的安全性。(其实也只是减少数据丢失的多少,并不能保证不丢失)

AOF缓存区的同步文件策略由参数appendfsync控制,各个值的含义如下:

  • always:命令写入aof_buf后立即调用系统fsync操作同步到AOF文件,fsync完成后线程返回。这种情况下,每次有写命令都要同步到AOF文件,硬盘IO成为性能瓶颈,Redis只能支持大约几百TPS写入,严重降低了Redis的性能;即便是使用固态硬盘(SSD),每秒大约也只能处理几万个命令,而且会大大降低SSD的寿命。参考:AOF 持久化的实现可得:即使是最安全得方式,也是可能会丢失数据。
  • no:命令写入aof_buf后调用系统write操作,不对AOF文件做fsync同步;同步由操作系统负责,通常同步周期为30秒。这种情况下,文件同步的时间不可控,且缓冲区中堆积的数据会很多,数据安全性无法保证。
  • everysec:命令写入aof_buf后调用系统write操作,write完成后线程返回;fsync同步文件操作由专门的线程每秒调用一次。everysec是前述两种策略的折中,是性能和数据安全性的平衡,因此是Redis的默认配置,也是我们推荐的配置。

文件重写(rewrite)

文件重写是指定期重写AOF文件,减小AOF文件的体积。需要注意的是,AOF重写是把Redis进程内的数据转化为写命令,同步到新的AOF文件;不会对旧的AOF文件进行任何读取、写入操作!(这种方式保证了旧数据都安全性)

这个配置不是必须得。在一些实现中,会关闭自动的文件重写,然后通过定时任务在每天的某一时刻定时执行

文件重写的触发

文件重写的触发,分为手动触发和自动触发

  • 手动触发:直接调用bgrewriteaof命令,该命令的执行与bgsave有些类似:都是fork子进程(经常用到这个技术,需要多了解一下。这样才能明白,fork能实现哪些效果!)进行具体的工作,且都只有在fork时阻塞。
  • 自动触发:根据auto-aof-rewrite-min-size和auto-aof-rewrite-percentage参数,以及aof_current_size和aof_base_size状态确定触发时机

3.3 启动时加载

当AOF开启时,Redis启动时会优先载入AOF文件来恢复数据;只有当AOF关闭时,才会载入RDB文件恢复数据

文件校验

与载入RDB文件类似,Redis载入AOF文件时,会对AOF文件进行校验,如果文件损坏,则日志中会打印错误,Redis启动失败。但如果是AOF文件结尾不完整(机器突然宕机等容易导致文件尾部不完整),且aof-load-truncated参数开启,则日志中会输出警告,Redis忽略掉AOF文件的尾部,启动成功。aof-load-truncated参数默认是开启的:

伪客户端

因为Redis的命令只能在客户端上下文中执行,而载入AOF文件时命令是直接从文件中读取的,并不是由客户端发送;因此Redis服务器在载入AOF文件之前,会创建一个没有网络连接的客户端,之后用它来执行AOF文件中的命令,命令执行的效果与带网络连接的客户端完全一样。

四 常见问题

4.1 fork

父进程通过fork操作可以创建子进程;子进程创建后,父子进程共享代码段,不共享进程的数据空间),但是子进程会获得父进程的数据空间的副本(也就是子进程获取到得是某一个时刻得快照数据)。
在操作系统fork的实际实现中,基本都采用了写时复制技术,即在父/子进程试图修改数据空间之前,父子进程实际上共享数据空间;但是当父/子进程的任何一个试图修改数据空间时,操作系统会为修改的那一部分(内存的一页)制作一个副本。(这样做可以节约空间,而且fork速度快)

虽然fork时,子进程不会复制父进程的数据空间**,但是会复制内存页表**(页表相当于内存的索引、目录);父进程的数据空间越大,内存页表越大,fork时复制耗时也会越多。(也就是说:fork虽然用了写时复制优化,但有些消耗还是不可避免得)

4.2 AOF追加阻塞:硬盘的阻塞

在AOF中,如果AOF缓冲区的文件同步策略为everysec,则:在主线程中注意是主线程!),命令写入aof_buf后调用系统write操作,write完成后主线程返回;fsync同步文件操作由专门的文件同步线程每秒调用一次。(当everysec策略时,write和fsync不是一个线程做的)

这种做法的问题在于,如果硬盘负载过高,那么fsync操作可能会超过1s;如果Redis主线程持续高速向aof_buf写入命令,硬盘的负载可能会越来越大,IO资源消耗更快;如果此时Redis进程异常退出,丢失的数据也会越来越多,可能远超过1s。

为此,Redis的处理策略是这样的:主线程每次进行AOF会对比上次fsync成功的时间;如果距上次不到2s,主线程直接返回;如果超过2s,则主线程阻塞直到fsync同步完成。因此,如果系统硬盘负载过大导致fsync速度太慢,会导致Redis主线程的阻塞;此外,使用everysec配置,AOF最多可能丢失2s的数据超超过2s后,主线程就不对外提供服务了),而不是1s。

AOF追加阻塞问题定位的方法:
(1)监控info Persistence中的aof_delayed_fsync:当AOF追加阻塞发生时(即主线程等待fsync而阻塞),该指标累加。
(2)AOF阻塞时的Redis日志:
Asynchronous AOF fsync is taking too long (disk is busy?). Writing the AOF buffer without waiting for fsync to complete, this may slow down Redis.
(3)如果AOF追加阻塞频繁发生,说明系统的硬盘负载太大;可以考虑更换IO速度更快的硬盘,或者通过IO监控分析工具对系统的IO负载进行分析,如iostat(系统级io)、iotop(io版的top)、pidstat等。

参考