试想一下,若是 Redis 每执行一条写操做命令,就把该命令以追加的方式写入到一个文件里,而后重启 Redis 的时候,先去读取这个文件里的命令,而且执行它,这不就至关于恢复了缓存数据了吗?redis
这种保存写操做命令到日志的持久化方式,就是 Redis 里的 AOF(Append Only File) 持久化功能,注意只会记录写操做命令,读操做命令是不会被记录的,由于没意义。数据库
在 Redis 中 AOF 持久化功能默认是不开启的,须要咱们修改 redis.conf
配置文件中的如下参数:缓存
AOF 日志文件其实就是普通的文本,咱们能够经过 cat
命令查看里面的内容,不过里面的内容若是不知道必定的规则的话,可能会看不懂。安全
我这里以「set name xiaolin」命令做为例子,Redis 执行了这条命令后,记录在 AOF 日志里的内容以下图:服务器
我这里给你们解释下。数据结构
「*3
」表示当前命令有三个部分,每部分都是以「$+数字
」开头,后面紧跟着具体的命令、键或值。而后,这里的「数字
」表示这部分中的命令、键或值一共有多少字节。例如,「$3 set
」表示这部分有 3 个字节,也就是「set
」命令这个字符串的长度。多线程
不知道你们注意到没有,Redis 是先执行写操做命令后,才将该命令记录到 AOF 日志里的,这么作其实有两个好处。app
第一个好处,避免额外的检查开销。异步
由于若是先将写操做命令记录到 AOF 日志里,再执行该命令的话,若是当前的命令语法有问题,那么若是不进行命令语法检查,该错误的命令记录到 AOF 日志里后,Redis 在使用日志恢复数据时,就可能会出错。函数
而若是先执行写操做命令再记录日志的话,只有在该命令执行成功后,才将命令记录到 AOF 日志里,这样就不用额外的检查开销,保证记录在 AOF 日志里的命令都是可执行而且正确的。
第二个好处,不会阻塞当前写操做命令的执行,由于当写操做命令执行成功后,才会将命令记录到 AOF 日志。
固然,AOF 持久化功能也不是没有潜在风险。
第一个风险,执行写操做命令和记录日志是两个过程,那当 Redis 在还没来得及将命令写入到硬盘时,服务器发生宕机了,这个数据就会有丢失的风险。
第二个风险,前面说道,因为写操做命令执行成功后才记录到 AOF 日志,因此不会阻塞当前写操做命令的执行,可是可能会给「下一个」命令带来阻塞风险。
由于将命令写入到日志的这个操做也是在主进程完成的(执行命令也是在主进程),也就是说这两个操做是同步的。
若是在将日志内容写入到硬盘时,服务器的硬盘的 I/O 压力太大,就会致使写硬盘的速度很慢,进而阻塞住了,也就会致使后续的命令没法执行。
认真分析一下,其实这两个风险都有一个共性,都跟「 AOF 日志写回硬盘的时机」有关。
Redis 写入 AOF 日志的过程,以下图:
我先来具体说说:
server.aof_buf
缓冲区;Redis 提供了 3 种写回硬盘的策略,控制的就是上面说的第三步的过程。
在 redis.conf
配置文件中的 appendfsync
配置项能够有如下 3 种参数可填:
这 3 种写回策略都没法能完美解决「主进程阻塞」和「减小数据丢失」的问题,由于两个问题是对立的,偏向于一边的话,就会要牺牲另一边,缘由以下:
你们根据本身的业务场景进行选择:
我也把这 3 个写回策略的优缺点总结成了一张表格:
你们知道这三种策略是怎么实现的吗?
深刻到源码后,你就会发现这三种策略只是在控制 fsync()
函数的调用时机。
当应用程序向文件写入数据时,内核一般先将数据复制到内核缓冲区中,而后排入队列,而后由内核决定什么时候写入硬盘。
若是想要应用程序向文件写入数据后,能立马将数据同步到硬盘,就能够调用 fsync()
函数,这样内核就会将内核缓冲区的数据直接写入到硬盘,等到硬盘写操做完成后,该函数才会返回。
AOF 日志是一个文件,随着执行的写操做命令愈来愈多,文件的大小会愈来愈大。
若是当 AOF 日志文件过大就会带来性能问题,好比重启 Redis 后,须要读 AOF 文件的内容以恢复数据,若是文件过大,整个恢复的过程就会很慢。
因此,Redis 为了不 AOF 文件越写越大,提供了 AOF 重写机制,当 AOF 文件的大小超过所设定的阈值后,Redis 就会启用 AOF 重写机制,来压缩 AOF 文件。
AOF 重写机制是在重写时,读取当前数据库中的全部键值对,而后将每个键值对用一条命令记录到「新的 AOF 文件」,等到所有记录完后,就将新的 AOF 文件替换掉现有的 AOF 文件。
举个例子,在没有使用重写机制前,假设先后执行了「set name xiaolin」和「set name xiaolincoding」这两个命令的话,就会将这两个命令记录到 AOF 文件。
可是在使用重写机制后,就会读取 name 最新的 value(键值对) ,而后用一条 「set name xiaolincoding」命令记录到新的 AOF 文件,以前的第一个命令就没有必要记录了,由于它属于「历史」命令,没有做用了。这样一来,一个键值对在重写日志中只用一条命令就好了。
重写工做完成后,就会将新的 AOF 文件覆盖现有的 AOF 文件,这就至关于压缩了 AOF 文件,使得 AOF 文件体积变小了。
而后,在经过 AOF 日志恢复数据时,只用执行这条命令,就能够直接完成这个键值对的写入了。
因此,重写机制的妙处在于,尽管某个键值对被多条写命令反复修改,最终也只须要根据这个「键值对」当前的最新状态,而后用一条命令去记录键值对,代替以前记录这个键值对的多条命令,这样就减小了 AOF 文件中的命令数量。最后在重写工做完成后,将新的 AOF 文件覆盖现有的 AOF 文件。
这里说一下为何重写 AOF 的时候,不直接复用现有的 AOF 文件,而是先写到新的 AOF 文件再覆盖过去。
由于若是 AOF 重写过程当中失败了,现有的 AOF 文件就会形成污染,可能没法用于恢复使用。
因此 AOF 重写过程,先重写到新的 AOF 文件,重写失败的话,就直接删除这个文件就好,不会对现有的 AOF 文件形成影响。
写入 AOF 日志的操做虽然是在主进程完成的,由于它写入的内容很少,因此通常不太影响命令的操做。
可是在触发 AOF 重写时,好比当 AOF 文件大于 64M 时,就会对 AOF 文件进行重写,这时是须要读取全部缓存的键值对数据,并为每一个键值对生成一条命令,而后将其写入到新的 AOF 文件,重写完后,就把如今的 AOF 文件替换掉。
这个过程实际上是很耗时的,因此重写的操做不能放在主进程里。
因此,Redis 的重写 AOF 过程是由后台子进程 bgrewriteaof 来完成的,这么作能够达到两个好处:
子进程是怎么拥有主进程同样的数据副本的呢?
主进程在经过 fork
系统调用生成 bgrewriteaof 子进程时,操做系统会把主进程的「页表」复制一份给子进程,这个页表记录着虚拟地址和物理地址映射关系,而不会复制物理内存,也就是说,二者的虚拟空间不一样,但其对应的物理空间是同一个。
这样一来,子进程就共享了父进程的物理内存数据了,这样可以节约物理内存资源,页表对应的页表项的属性会标记该物理内存的权限为只读。
不过,当父进程或者子进程在向这个内存发起写操做时,CPU 就会触发缺页中断,这个缺页中断是因为违反权限致使的,而后操做系统会在「缺页异常处理函数」里进行物理内存的复制,并从新设置其内存映射关系,将父子进程的内存读写权限设置为可读写,最后才会对内存进行写操做,这个过程被称为「写时复制(Copy On Write)」。
写时复制顾名思义,在发生写操做的时候,操做系统才会去复制物理内存,这样是为了防止 fork 建立子进程时,因为物理内存数据的复制时间过长而致使父进程长时间阻塞的问题。
固然,操做系统复制父进程页表的时候,父进程也是阻塞中的,不过页表的大小相比实际的物理内存小不少,因此一般复制页表的过程是比较快的。
不过,若是父进程的内存数据很是大,那天然页表也会很大,这时父进程在经过 fork 建立子进程的时候,阻塞的时间也越久。
因此,有两个阶段会致使阻塞父进程:
触发重写机制后,主进程就会建立重写 AOF 的子进程,此时父子进程共享物理内存,重写子进程只会对这个内存进行只读,重写 AOF 子进程会读取数据库里的全部数据,并逐一把内存数据的键值对转换成一条命令,再将命令记录到重写日志(新的 AOF 文件)。
可是子进程重写过程当中,主进程依然能够正常处理命令。
若是此时主进程修改了已经存在 key-value,就会发生写时复制,注意这里只会复制主进程修改的物理内存数据,没修改物理内存仍是与子进程共享的。
因此若是这个阶段修改的是一个 bigkey,也就是数据量比较大的 key-value 的时候,这时复制的物理内存数据的过程就会比较耗时,有阻塞主进程的风险。
还有个问题,重写 AOF 日志过程当中,若是主进程修改了已经存在 key-value,此时这个 key-value 数据在子进程的内存数据就跟主进程的内存数据不一致了,这时要怎么办呢?
为了解决这种数据不一致问题,Redis 设置了一个 AOF 重写缓冲区,这个缓冲区在建立 bgrewriteaof 子进程以后开始使用。
在重写 AOF 期间,当 Redis 执行完一个写命令以后,它会同时将这个写命令写入到 「AOF 缓冲区」和 「AOF 重写缓冲区」。
也就是说,在 bgrewriteaof 子进程执行 AOF 重写期间,主进程须要执行如下三个工做:
当子进程完成 AOF 重写工做(扫描数据库中全部数据,逐一把内存数据的键值对转换成一条命令,再将命令记录到重写日志)后,会向主进程发送一条信号,信号是进程间通信的一种方式,且是异步的。
主进程收到该信号后,会调用一个信号处理函数,该函数主要作如下工做:
信号函数执行完后,主进程就能够继续像往常同样处理命令了。
在整个 AOF 后台重写过程当中,除了发生写时复制会对主进程形成阻塞,还有信号处理函数执行时也会对主进程形成阻塞,在其余时候,AOF 后台重写都不会阻塞主进程。
此次小林给你们介绍了 Redis 持久化技术中的 AOF 方法,这个方法是每执行一条写操做命令,就将该命令以追加的方式写入到 AOF 文件,而后在恢复时,以逐一执行命令的方式来进行数据恢复。
Redis 提供了三种将 AOF 日志写回硬盘的策略,分别是 Always、Everysec 和 No,这三种策略在可靠性上是从高到低,而在性能上则是从低到高。
随着执行的命令越多,AOF 文件的体积天然也会愈来愈大,为了不日志文件过大, Redis 提供了 AOF 重写机制,它会直接扫描数据中全部的键值对数据,而后为每个键值对生成一条写操做命令,接着将该命令写入到新的 AOF 文件,重写完成后,就替换掉现有的 AOF 日志。重写的过程是由后台子进程完成的,这样可使得主进程能够继续正常处理命令。
用 AOF 日志的方式来恢复数据实际上是很慢的,由于 Redis 执行命令由单线程负责的,而 AOF 日志恢复数据的方式是顺序执行日志里的每一条命令,若是 AOF 日志很大,这个「重放」的过程就会很慢了。