上一次我们介绍了Redis的数据类型,应用场景,单线程模型以及与Memcached的比较.今天继续介绍它的持久化.

持久化

Redis的数据是存在内存里的,如果不采取特殊措施,一旦redis重启/退出/故障,里面的数据将全部丢失.

即便它很快重启也只是个空的redis,这时大量的请求将会直接打到DB去.

为了避免这种情况,Redis提供了RDB和AOF两种持久化方式.

RDB


RDB也就是快照,持久化时是把当前内存中的数据集快照写入磁盘(名为dump.rdb的二进制文件),恢复时则把快照文件直接读到内存中.

RDB文件的创建

两条命令:

  • SAVE阻塞Redis服务器进程,服务器不能接收任何请求,直到RDB文件创建完毕为止。
  • BGSAVE创建出一个子进程,由子进程来负责创建RDB文件,服务器进程可以继续接收请求。bgsave期间服务器会拒绝任何save命令(防止竞态).

这两条命令既可以由客户端来请求执行,也可以在满足下列条件时自动触发:

  • 用户设置了save配置选项,比如save 60 10000,那么从最近一次创建快照之后开始算起,当"60秒内有10000次写入"这个条件满足时,redis就会自动触发bgsave命令.如果设置了多条,满足其中一条就会触发.如果用户没有主动设置,redis也有自己的默认配置
  • 收到shutdown(关闭服务器)或者标准term信号(linux下终止进程)后,redis会执行save命令,阻塞所有客户端,不再执行客户端发送的任何命令,并在save命令执行完毕后关闭服务器.
  • 主从复制时,主服务器向从服务器发送一条sync命令,如果此时主服务器没在执行bgsave,也不是刚刚执行完bgsave,就会执行bgsave命令.

save命令的操作比较简单,但是它在执行期间会阻塞线上的业务,所以一般我们想手动备份的时候都是用的bgsave.要了解bgsave,必须先清楚什么是COW,什么是fork.

cow也就是copy on write,写时复制.这是一种读写分离的设计思想.

假设有一块内存空间,我们要对它进行读写,那么当只有读的需求时,不需要额外操作.如果有写的需求时,另外开一块新的内存空间,把需要写的那页复制到这里,然后就在新的空间进行修改,而写的同时仍旧可以读旧的空间.写完之后把指针指向新的空间,旧的空间抛弃.

这样设计的好处是读写分离,不需要加锁来造成阻塞,写的同时也可以读,提高了读的效率.(联想到CopyOnWriteList没有?)

而fork是linux系统的一个进程机制,应用到了cow.当父进程fork出一个子进程时,两者共享内存里面的代码段和数据段.

RDB的具体执行过程如下:

  • redis调用forks创建子进程来进行rdb操作.
  • 子进程将数据集写入到一个临时rdb文件中,父进程继续提供读写服务.此时父进程和子进程共享数据段,可以用到copy-on-write机制.
  • 不必担心父进程的读写会改变子进程持久化的数据,因为此时子进程已经把内存的数据固定下来了.父进程读的时候,依然如旧.父进程写的时候,子进程会把脏页(即将被写入的那页)复制一份出来让父进程去写.子进程写入硬盘的还是旧的那页.
  • 当子进程完成对rdb文件的写入时,redis用新的rdb文件替换原来的rdb文件.而fork时产生的那些脏页也会替换原来未修改的那些页.旧页被抛弃.

需要注意的是,在bgsave这个过程中,服务器执行的写命令并没有一起同步到rdb文件中,也就是说,RDB文件在bgsave之后并不与当前的数据库状态一致.

RDB文件的载入

rdb文件是在服务器启动的时候自动载入的,redis没有提供任何主动载入rdb文件的命令.

只要redis在启动的时候检测到rdb文件的存在,就会主动载入它.

但是AOF文件的载入优先级高于RDB.只有AOF功能关闭时,redis才会用rdb文件来恢复数据.

aof or rdb

AOF


aof(append only file)与rdb文件存储数据集不同,aof文件存储的是redis所有执行过的命令,类似于mysql的binlog.

AOF的实现

分为三步:

  • 命令追加
  • 文件写入
  • 文件同步

这里要先介绍现代操作系统的文件写入机制:

为了提高效率,在现代操作系统中,当用户试图写入文件时,这些数据会先写入到操作系统的内存缓冲区里,这就是上面说的文件写入.

等到内存缓冲区被填满了,或者超过一定时限后,操作系统才真正把内存缓冲区里的数据写入到硬盘的文件中去.这就是文件同步.

服务器在执行完一个写命令后,就会把这条命令追加到redis的aof缓冲区的末尾.

服务器每次在结束一个事件循环之前,都会根据配置项appendfsync来判断是否要把缓冲区的命令写入到aof文件.

appendfsync有三种选项:

  • always : 将aof缓冲区的数据写入并同步到aof文件中.
  • everysec(默认) : 写入文件,如果距离上次同步aof文件的时间超过了1秒,那就再同步.
  • no : 只写入文件,不同步.同步时间由操作系统决定.

使用always,当redis故障时数据丢失最少,但写入硬盘的操作频繁,但效率最慢.

使用no,不仅故障时丢失数据最多,而且当缓冲区等待写入硬盘的数据填满时,redis的写操作将被阻塞,所以平均效率与always相当.

使用everysec不仅安全,而且实际效率与不进行持久化时相差无几.推荐使用everysec.

AOF的载入

由于aof里包含了redis执行过的所有写命令,当服务器重启时,只需读取并执行aof文件里的所有命令即可还原数据库状态.

但是由于redis的命令只能在客户端上下文中执行,因此这个还原的过程实际上是借助于伪客户端来进行的.

load-aof

AOF重写(bgrewriteaof)

RDB文件和AOF文件随着服务器的运行,数据越来越多,文件的体积也会膨胀起来.RDB文件没有办法优化,但是AOF可以通过AOF重写来减少体积.

AOF存储的是命令集,有时候对于一条数据反复设置可以产生多条命令(过程).由于还原数据库只需要最终的数据(结果),因此可以想办法把这多条命令合并到一条命令上.比如aof记录了set a 1, set a 2,set a 3三条命令,那么我们最终只需要set a 3这条等效命令.

实际上,AOF重写并不依赖AOF文件,而是从数据库一个个读取key现在的值,然后把一条条set命令写入到新aof文件去.写完之后用新aof文件替代旧aof文件,那么重写就完成了.

AOF后台重写

aof重写需要读取全部的key,如果只通过一个线程去操作的话,无疑会造成严重阻塞,在此期间无法继续处理客户端的请求(有没有想到keys命令?)

所以Redis将重写放入到子进程去进行,让父进程继续处理客户端的读写请求.(和bgsave相似).

同样地,在重写期间,如果父进程继续修改数据库的数据,那么重写后的aof文件与当前数据库的状态并不一致.

为了解决这种数据不一致的问题,redis设置了一个aof重写缓冲区(与aof缓冲区区分开),在fork子进程之后使用.所以在重写期间,服务器进程会执行以下三个工作:

  • 执行客户端发来的命令.
  • 将执行后的写命令追加到aof缓冲区末尾.
  • 将执行后的写命令追加到aof重写缓冲区末尾.(新增)

rewriteaof

当子进程重写完成后,会把aof重写缓冲区里的命令(未优化过)写入到新的aof文件里,这样就可以保证新aof文件与数据库状态的一致性了.

所以要注意的是,即便aof刚刚重写完毕,它也是还可以再压缩的.

顺便一提,主从复制时也会用到这个机制.

RDB与AOF对比

  • RDB存储数据集,每次持久化是全量存储.AOF存储命令集,每次持久化只存储增量.
  • RDB持久化时间更长,可能丢失的数据也更多.
  • RDB文件一般会小于AOF文件(未重写),用于还原数据库状态时的速度也会快于AOF
  • 但是由于aof文件的更新频率更高,丢失数据的数量也更小,因此服务器启动时默认载入aof文件.
  • 由于RDB文件紧凑性,便于复制数据到一个远端数据中心,非常适用于灾难恢复,适合冷备.
  • AOF更新频率快,可能丢失的数据少,适合热备.

为了防止服务器突然宕机的情况,Redis在运行期间一定要开启aof进行备份.

但是如果Redis是正常退出,它会自动生成RDB文件,这个RDB文件保存的是最新的数据集.那么重启时,可以把aof文件删掉,载入这个RDB文件以快速恢复.

RDB和AOF混合搭配模式

在对redis进行恢复的时候,如果我们采用了RDB的方式,因为bgsave的策略,可能会导致我们丢失大量的数据。如果我们采用了AOF的模式,通过AOF操作日志重放恢复,重放AOF日志比RDB要长久很多。

redis4.0之后,为了解决这个问题,引入了新的持久化模式,混合持久化,将rdb的文件和局部增量的AOF文件相结合,rdb可以使用相隔较长的时间保存策略,aof不需要是全量日志,只需要保存前一次rdb存储开始到这段时间增量aof日志即可,一般来说,这个日志量是非常小的。