Redis: 一文带你了解AOF日志
- AOF日志
- AOF的内容
- AOF持久化的实现
- AOF数据恢复
- 为什么AOF先执行命令后写日志?
- 写回策略
- AOF重写机制
- 参考文献
Redis用作缓存,直接从内存中读取数据,相比于传统的将数据保存在磁盘中响应速度要快很多。但是服务器宕机,那么内存中的数据将全部丢失,该如何恢复这些数据呢。
一个简单的方案是,从后端数据库中恢复这些数据。但是频繁的访问数据库会给数据库带来压力的同时,也会降低Redis的性能。
对于Redis来说,如何避免从后端数据库中恢复数据来实现数据的持久化是至关重要的。Redis 提供了 RDB 和 AOF 两种持久化方案,将内存中的数据保存到磁盘中,避免数据丢失。
本文将介绍 AOF 相关的原理;
AOF( append only file )持久化以独立日志的方式记录每次写命令,并在 Redis 重启时在重新执行 AOF 文件中的命令以达到恢复数据的目的。AOF 的主要作用是解决数据持久化的实时性。
AOF日志
Redis是先执行命令,再把数据写入内存,然后记录日志,如下图所示:
AOF的内容
MySQL中redo log,记录的是修改后的数据,AOF中记录的是以文本的形式来记录收到的每一条命令。
执行以下的命令,
set testkey testvalue
看一下AOF日志的内容,*3
代表当前的命令有三个部分,分别是set
,testkey
,testvalue
,每个部分的开头都是由$+数字
开头,后面跟着对应的命令、键或者值,数字代表的就是对应的命令、键或者值有多少个字节。
$3 set
表示的set
命令有3个字节,如下图所示:
AOF持久化的实现
AOF持久化的实现可以分为:命令追加( append )、文件写入( write )、文件同步( sync )、文件重写(rewrite)和重启加载(load)。
- 所有的写命令会追加至AOF缓存中;
- AOF缓存区根据对应的策略向硬盘进行同步操作;
- AOF日志会越来越大,需要定期对AOF文件进行重写来实现压缩;
- Redis重启时,可以加载AOF文件进行数据恢复;
命令追加:当 AOF 持久化功能处于打开状态时,Redis 在执行完一个写命令之后,会以协议格式(也就是RESP,即 Redis 客户端和服务器交互的通信协议 )将被执行的写命令追加到 Redis 服务端维护的 AOF 缓冲区末尾。
文件写入:Redis 每次结束一个事件循环之前,它都会调用 flushAppendOnlyFile
函数,判断是否需要将 AOF 缓存区中的内容写入和同步到 AOF 文件中。
AOF数据恢复
AOF文件中包含了重建 Redis 数据所需的所有写命令,所以 Redis 只要读入并重新执行一遍 AOF 文件里边保存的写命令,就可以还原 Redis 关闭之前的状态,其对应的过程如下:
具体步骤如下:
伪客户端(fake client): Redis 的命令只能在客户端上下文中执行,而载入AOF文件时所使用的命令直接来源于AOF文件而不是网络连接,所以服务器使用了一个没有网络连接的伪客户端来执行AOF文件保存的写命令,伪客户端执行命令 的效果和带网络连接的客户端执行命令的效果完全一样。
- 创建一个不带网络连接的伪客户端;
- 从AOF文件中分析并读取第一条命令;
- 使用伪客户端执行被读取的写命令;
- 重复执行步骤2和步骤3,直到AOF中的所有命令被执行完毕即可;
为什么AOF先执行命令后写日志?
了解完AOF的文本内容之后,可以猜到Redis在向AOF里面记录日志的时候,并不会先去对这些命令进行语法检查。所以,如果先记日志再执行命令的话,日志中就有可能记录了错误的命令,Redis在使用日志恢复数据时,就可能会出错。
先让系统执行命令,只有记录成功的执行的命令到日志中就可以避免出现记录错误命令的情况,在命令执行完才记录日志是不会阻塞当前的写操作;
AOF存在的潜在风险就是,如果刚执行完一个命令,还没来得及记录日志就宕机,那么命令和对应的数据就有丢失的风险;其次,AOF日志虽然避免了对当前命令的阻塞,但是其在主线程中将日志写入到磁盘中就会使得增加磁盘的写压力,来阻塞后续的命令操作。
仔细分析的话,你就会发现,这两个风险都是和AOF写回磁盘的时机相关的。这也就意味着,如果我们能够控制一个写命令执行完后AOF日志写回磁盘的时机,这两个风险就解除了。
写回策略
Redis在结束每个事件之前,都会调用flushAppendOnlyFile
函数,判断是否需要将 AOF 缓存区中的内容写入和同步到 AOF 文件中。flushAppendOnlyFile
函数的行为由 redis.conf
配置中的 appendfsync
选项的值来决定。该选项有三个可选值,分别是 always
、everysec
和 no
:
- Always,同步写回:每个写命令执行完,立马同步地将日志写回磁盘;
- Everysec,每秒写回:每个写命令执行完,只是先把日志写到AOF文件的内存缓冲区,每隔一秒把缓冲区中的内容写入磁盘;
- No,操作系统控制的写回:每个写命令执行完,只是先把日志写到AOF文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘。
但是三种写回策略都无法实现两全其美,原因如下:
- 同步写回,每次执行命令之后同步入瓷盘中会影响主线程的性能;
- 每秒写回,采用一秒写回一次的频率,避免了“同步写回”的性能开销,虽然减少了对系统性能的影响,但是如果发生宕机,上一秒内未落盘的命令操作仍然会丢失。
- 操作系统控制的写回:存入磁盘的操作由操作系统控制,只要AOF中没有写回对应的命令,一旦服务器宕机就会丢失数据;
三种写回策略对应的优缺点如下:
想要获取高性能,就选择No策略;
想要获取高可靠,就选择Always策略;
运行数据的丢失,也不影响性能的话就选择Everysec策略;
AOF文件是记录所有的命令,随着写命令的越来越多,AOF文件会越来越大就会带来性能问题,如下:
- 文件系统本身对于文件大小的限制,无法保存过大的文件;
- 文件太大,追加命令会导致效率变低;
- 如果宕机,日志过大,故障恢复的过程很慢,影响Redis的正常使用;
所以如何避免AOF文件带来的性能问题,就需要AOF重写机制
AOF重写机制
AOF重写机制就是在重写时,Redis根据数据库的现状创建一个新的AOF文件,也就是说,读取数据库中的所有键值对,然后对每一个键值对用一条命令记录它的写入。比如说,当读取了键值对“testkey”: “testvalue”之后,重写机制会记录set testkey testvalue这条命令。这样,当需要恢复时,可以重新执行该命令,实现“testkey”: “testvalue”的写入。
重写机制就是具有多变一的功能,旧日志中的多条命令,在重写后的新日志中变成一条命令,示例如下图所示:
在对一个列表之后6次修改操作之后,最终的列表的状态是[“D”, “C”, “N”],只用LPUSH u:list “N”, “C”, "D"这一条命令就能实现该数据的恢复,这就节省了五条命令的空间。
虽然AOF重写后,日志文件会缩小,但是,要把整个数据库的最新数据的操作日志都写回磁盘,仍然是一个非常耗时的过程。这时,我们就要继续关注另一个问题了:重写会不会阻塞主线程?
AOF后台重写
和AOF日志由主线程写回不同,重写过程是由后台线程bgrewriteaof来完成的,这也是为了避免阻塞主线程,导致数据库性能下降。
- 子进程在AOF重写期间,Redis可以继续处理客户端的命令请求;
- 进程带有父进程的内存数据拷贝副本,在不适用锁的情况下,也可以保证数据的安全性。
但是,在子进程进行 AOF 重启期间,Redis接收客户端命令,会对现有数据库状态进行修改,从而导致数据当前状态和 重写后的 AOF 文件所保存的数据库状态不一致。
为此,Redis 设置了一个 AOF 重写缓冲区,这个缓冲区在服务器创建子进程之后开始使用,当 Redis 执行完一个写命令之后,它会同时将这个写命令发送给 AOF 缓冲区和 AOF 重写缓冲区。
当子进程完成AOF重写之后,会向父进程发送一个信号,父进程在接收到该信号之后会调用一个信号处理函数来执行后续的工作:
- 将 AOF 重写缓冲区中的所有内容写入到新的 AOF 文件中,保证新 AOF 文件- 保存的数据库状态和服务器当前状态一致。
- 对新的 AOF 文件进行改名,原子地覆盖现有 AOF 文件,完成新旧文件的替换
- 继续处理客户端请求命令。
AOF重写的过程可以总结为一个拷贝,两处日志;
“一个拷贝”指,每次执行重写时,主线程fork出后台的bgrewriteaof子进程。此时,fork会把主线程的内存拷贝一份给bgrewriteaof子进程,这里面就包含了数据库的最新数据。然后,bgrewriteaof子进程就可以在不影响主线程的情况下,逐一把拷贝的数据写成操作,记入重写日志。
”两处日志“指,主线程未阻塞,仍然可以处理新来的操作。此时,如果有写操作,第一处日志就是指正在使用的AOF日志,Redis会把这个操作写到它的缓冲区。这样一来,即使宕机了,这个AOF日志的操作仍然是齐全的,可以用于恢复。第二处日志,就是指新的AOF重写日志。这个操作也会被写到重写日志的缓冲区。这样,重写日志也不会丢失最新的操作。等到拷贝数据的所有操作记录重写完成后,重写日志记录的这些最新操作也会写入新的AOF文件,以保证数据库最新状态的记录。此时,我们就可以用新的AOF文件替代旧文件了,整个过程如下图所示:
参考文献
1、极客时间-Redis核心技术与实战
2、Redis AOF 持久化详解
3、Redis设计与实现