Redis AOF

经常会被问到一个问题,假如Redis宕机,内存中的数据全部丢失,怎么恢复数据?

Redis 分别提供了 RDB 和 AOF 两种持久化机制:
RDB将数据库的快照(snapshot)以二进制的方式保存到磁盘中。类似于MySQL全量备份。
AOF则以协议文本的方式,将所有对数据库进行过写入的命令(及其参数)记录到AOF文件,以此达到记录数据库状态的目的。 类似于MySQL binlog 增量更新。

(1) AOF日志是什么

Redis AOF(Append-Only File)是一种持久化机制,它记录Redis接收到的每个写操作。它通过将每个写操作追加到磁盘上的文件中来实现。这个文件称为AOF文件。

AOF文件以Redis特定的格式编写,以实现最佳性能。它也是只追加的,这意味着一旦数据被写入文件,就不能被修改或删除。这使得它成为一种可靠和安全的数据持久化方式。

和我们常见的WAL日志不同,WAL(Write Ahead Log)是写前日志,在实际写数据前,先把修改的数据记到日志文件中,再去执行命令,这个就要求数据库需要额外的检查命令是否正确。

AOF(Append Only File)日志是一种写后日志,在Redis先执行命令,把数据写入内存后,然后才记录日志,日志会追加到文件末尾,所以叫AOF日志。


(2) AOF有哪些作用?

AOF日志的作用主要有2个:

  1. 持久化,把内存中的数据保存到到磁盘上。
  2. 用来在redis宕机后恢复数据;
  3. 可以用来主从数据同步。

(3) AOF使用

(3.1) 开启AOF

# redis.conf 1254行  
# 设置成yes 开启AOF
appendonly yes


# 设置AOF文件名
appendfilename "appendonly.aof"


# 保存模式  
# 默认是 everysec
# appendfsync always
appendfsync everysec
# appendfsync no


# 关闭AOF重写配置 
# 如果有延迟问题  可以把AOF后台重写关闭(把no改成yes)
# 详细内容看 redis.conf 英文注释
no-appendfsync-on-rewrite no


# 自动AOF重写触发条件 
# AOF重写比例
auto-aof-rewrite-percentage 100
# AOF重写最小的文件大小 默认64mb 
auto-aof-rewrite-min-size 64mb


# 
aof-load-truncated yes


# 
aof-use-rdb-preamble yes

(3.2) AOF保存模式

Redis 目前支持三种 AOF 保存模式,它们分别是:

保存模式 解释 速度及安全性
AOF_FSYNC_ALWAYS 在每次写操作后追加日志同步(fsync)。 慢 最安全
AOF_FSYNC_EVERYSEC 每秒只同步(fsync)一次。 妥协
AOF_FSYNC_NO 不同步(fsync),只让操作系统在需要时刷新数据。 很快
# Redis supports three different modes:
#
# no: don't fsync, just let the OS flush the data when it wants. Faster.
# always: fsync after every write to the append only log. Slow, Safest.
# everysec: fsync only one time every second. Compromise.

注意: fsync()函数是把数据同步文件。
fsync函数只对由文件描述符filedes指定的单一文件起作用,并且等待写磁盘操作结束,然后返回。可用于数据库这样的应用程序,这种应用程序需要确保将修改过的块立即写到磁盘上。

(3.3) AOF保存模式对性能和安全性的影响

对于三种 AOF 保存模式, 它们对服务器主进程的阻塞情况如下:

保存模式 解释 对服务器主进程的阻塞情况
AOF_FSYNC_NO 不保存 写入和保存都由主进程执行,两个操作都会阻塞主进程。
AOF_FSYNC_EVERYSEC 每一秒钟保存一次 写入操作由主进程执行,阻塞主进程。保存操作由子线程执行,不直接阻塞主进程,但保存操作完成的快慢会影响写入操作的阻塞时长。
AOF_FSYNC_ALWAYS 每执行一个命令保存一次 和模式 1 一样。

(4) AOF原理

(4.1) AOF命令是怎么同步的?

Redis将所有对数据库进行过写入的命令(及其参数)记录到 AOF 文件, 以此达到记录数据库状态的目的。

redis> RPUSH list 1 2 3 4
(integer) 4

redis> LRANGE list 0 -1
1) "1"
2) "2"
3) "3"
4) "4"

redis> KEYS *
1) "list"

redis> RPOP list
"4"

redis> LPOP list
"1"

redis> LPUSH list 1
(integer) 3

redis> LRANGE list 0 -1
1) "1"
2) "2"
3) "3"

那么其中四条对数据库有修改的写入命令就会被同步到 AOF 文件中:

RPUSH list 1 2 3 4

RPOP list

LPOP list

LPUSH list 1

(4.1.1) MySQL binlog有3种方式保存,redis aof有几种方式?

MySQL binlog有 STATMENTROWMIXED 三种格式
redis aof只有一种方式。

(4.2) Reids AOF数据是怎么存到文件的?

为了处理的方便, AOF文件使用网络通讯协议的格式来保存这些命令。

*2      # 表示这条命令的消息体共2$6      # 下一行的数据长度为6
SELECT  # 消息体
$1      # 下一行数据长度为1
0       # 消息体
*6      # 表示这条命令的消息体共6$5      # 下一行的数据长度为5
RPUSH   # 消息体
$4      # 下一行的数据长度为4
list
$1
1
$1
2
$1
3
$1
4
*2
$4
RPOP
$4
list
*2
$4
LPOP
$4
list
*3
$5
LPUSH
$4
list
$1
1

除了 SELECT 命令是 AOF 程序自己加上去的之外, 其他命令都是之前我们在终端里执行的命令。

(4.3) redis命令同步到AOF文件的流程

// todo 流程图

同步命令到AOF文件的整个过程可以分为三个阶段:

  1. 命令传播:Redis 将执行完的命令、命令的参数、命令的参数个数等信息发送到AOF程序中。
  2. 缓存追加:AOF程序根据接收到的命令数据,将命令转换为网络通讯协议的格式,然后将协议内容追加到服务器的AOF缓存中。
  3. 文件写入和保存:AOF缓存中的内容被写入到AOF文件末尾,如果设定的AOF保存条件被满足的话, fsync函数或者 fdatasync函数会被调用,将写入的内容真正地保存到磁盘中。

(5) 思考

(5.1) AOF为什么要先执行命令再记日志呢?

MySQL在处理写操作时是先写redo log,再写bin log,最后提交事务。 可以认为是先记日志,再执行命令。
这样做可以保证cash-safe,假设写完redo log MySQL宕机了,重启后也可以根据redo log来恢复数据。

为什么Redis的AOF日志要先执行命令再记日志呢?
如果执行完命令宕机了,那从库岂不是和主库数据不一致了?

为了避免额外的检查开销,Redis 在向 AOF 里面记录日志的时候,并不会先去对这些命令进行语法检查。所以,如果先记日志再执行命令的话,日志中就有可能记录了错误的命令,Redis 在使用日志恢复数据时,就可能会出错。

(5.2) AOF文件有大小限制吗?

MySQL的redo log

(5.3) AOF文件写满了怎么办?

MySQL的redo log支持循环写,类似一个环形数组,写满了可以把以前的记录覆盖掉继续写。
Redis的AOF文件


(6) Redis AOF源码解读

一旦 AOF 功能启用后,configSetCommand 函数就会调用 startAppendOnly 函数

TODO 流程图

// 启动后台子进程,执行AOF 持久化操作。 bgrewriteaofCommand()startAppendOnly()
// serverCron() 中会调用此函数

(5.1) 开启AOF日志-startAppendOnly

/**
 * 开启AOF日志
 * 
 * 当用户在运行时使用 CONFIG 命令从 "appendonly no" 切换到 "appendonly yes" 时调用。
 */
int startAppendOnly(void) {
    char cwd[MAXPATHLEN]; // 错误消息的工作目录
    int newfd;

    // 打开文件
    newfd = open(server.aof_filename,O_WRONLY|O_APPEND|O_CREAT,0644);
    // 确保AOF之前是关闭状态
    serverAssert(server.aof_state == AOF_OFF);
    
    if (newfd == -1) {
        char *cwdp = getcwd(cwd,MAXPATHLEN);

        // 记录日志 打开文件失败
        serverLog(LL_WARNING,
            "Redis needs to enable the AOF but can't open the "
            "append only file %s (in server root dir %s): %s",
            server.aof_filename,
            cwdp ? cwdp : "unknown",
            strerror(errno));
        return C_ERR;
    }

    // 如果有活跃子进程(RDB/AOF子进程) 并且 AOF子进程存在
    if (hasActiveChildProcess() && server.aof_child_pid == -1) {
        // 更新  
        server.aof_rewrite_scheduled = 1;
        // 日志记录    已经有另一个后台操作启用AOF。 AOF 后台计划在可能时启动。
        serverLog(LL_WARNING,"AOF was enabled but there is already another background operation. An AOF background was scheduled to start when possible.");
    } else {
        // 如果有待处理的 AOF 重写,我们需要将其关闭并启动一个新的:旧的不能被重用,因为它没有积累 AOF 缓冲区。
        if (server.aof_child_pid != -1) {
            // AOF已启用,但是有一个存在的AOF重写后台进程。停止AOF后台进程并且重启一个新的重写进程。
            serverLog(LL_WARNING,"AOF was enabled but there is already an AOF rewriting in background. Stopping background AOF and starting a rewrite now.");

            // 关闭AOF子进程
            killAppendOnlyChild();
        }

        // 后台重写AOF
        if (rewriteAppendOnlyFileBackground() == C_ERR) {
            // 关闭AOF文件
            close(newfd);

            // 记录日志   Redis 需要启用 AOF,但不能触发后台 AOF 重写操作。 检查上述日志以获取有关错误的更多信息。
            serverLog(LL_WARNING,"Redis needs to enable the AOF but can't trigger a background AOF rewrite operation. Check the above logs for more info about the error.");
            return C_ERR;
        }
    }

    // 我们正确地打开了 AOF,现在等待重写完成以便将数据附加到磁盘上。 
    server.aof_state = AOF_WAIT_REWRITE;

    // 更新AOF上次同步(刷盘)时间
    server.aof_last_fsync = server.unixtime;

    // 更新AOF文件描述符
    server.aof_fd = newfd;
    return C_OK;
}
/* This is how rewriting of the append only file in background works:
 *
 * 1) The user calls BGREWRITEAOF
 * 2) Redis calls this function, that forks():
 *    2a) the child rewrite the append only file in a temp file.
 *    2b) the parent accumulates differences in server.aof_rewrite_buf.
 * 3) When the child finished '2a' exists.
 * 4) The parent will trap the exit code, if it's OK, will append the
 *    data accumulated into server.aof_rewrite_buf into the temp file, and
 *    finally will rename(2) the temp file in the actual file name.
 *    The the new file is reopened as the new append only file. Profit!
 */
int rewriteAppendOnlyFileBackground(void) {
    pid_t childpid;

    if (hasActiveChildProcess()) return C_ERR;
    if (aofCreatePipes() != C_OK) return C_ERR;
    openChildInfoPipe();
    if ((childpid = redisFork(CHILD_TYPE_AOF)) == 0) {
        char tmpfile[256];

        /* Child */
        redisSetProcTitle("redis-aof-rewrite");
        redisSetCpuAffinity(server.aof_rewrite_cpulist);
        snprintf(tmpfile,256,"temp-rewriteaof-bg-%d.aof", (int) getpid());
        if (rewriteAppendOnlyFile(tmpfile) == C_OK) {
            sendChildCOWInfo(CHILD_TYPE_AOF, "AOF rewrite");
            exitFromChild(0);
        } else {
            exitFromChild(1);
        }
    } else {
        /* Parent */
        if (childpid == -1) {
            closeChildInfoPipe();
            serverLog(LL_WARNING,
                "Can't rewrite append only file in background: fork: %s",
                strerror(errno));
            aofClosePipes();
            return C_ERR;
        }
        serverLog(LL_NOTICE,
            "Background append only file rewriting started by pid %d",childpid);
        server.aof_rewrite_scheduled = 0;
        server.aof_rewrite_time_start = time(NULL);
        server.aof_child_pid = childpid;
        updateDictResizePolicy();
        /* We set appendseldb to -1 in order to force the next call to the
         * feedAppendOnlyFile() to issue a SELECT command, so the differences
         * accumulated by the parent into server.aof_rewrite_buf will start
         * with a SELECT statement and it will be safe to merge. */
        server.aof_selected_db = -1;
        replicationScriptCacheFlush();
        return C_OK;
    }
    return C_OK; /* unreached */
}

参考资料

[1] 04 | AOF日志:宕机了,Redis如何避免数据丢失?
[2] AOF-Redis设计与实现
[3] Redis AOF 持久化- Redis源码分析