Redis源码剖析和注释(十八)--- Redis AOF持久化机制

Redis AOF持久化机制

1. AOF持久化介绍

Redis中支持RDBAOF这两种持久化机制,目的都是避免因进程退出,造成的数据丢失问题。

  • RDB持久化:把当前进程数据生成时间点快照(point-in-time snapshot)保存到硬盘的过程,避免数据意外丢失。
  • AOF持久化:以独立日志的方式记录每次写命令,重启时在重新执行AOF文件中的命令达到恢复数据的目的。

Redis RDB持久化机制源码剖析和注释

AOF的使用:在redis.conf配置文件中,将appendonly设置为yes,默认的为no

2. AOF持久化的实现

AOF持久化所有注释:Redis AOF持久化机制源码注释

2.1 命令写入磁盘

2.1.1 命令写入缓冲区
  • 命令问什么先写入缓冲区

由于Redis是单线程响应命令,所以每次写AOF文件都直接追加到硬盘中,那么写入的性能完全取决于硬盘的负载,所以Redis会将命令写入到缓冲区中,然后执行文件同步操作,再将缓冲区内容同步到磁盘中,这样就很好的保持了高性能。

那么缓冲区定义如下,它是一个简单动态字符串(sds),因此很好的和C语言的字符串想兼容。

struct redisServer {
    // AOF缓冲区,在进入事件loop之前写入
    sds aof_buf;      /* AOF buffer, written before entering the event loop */
};
  • 命令的写入格式

Redis命令写入的内容直接就是文本协议格式,例如:

*2\r\n$6\r\nSELECT\r\n$1\r\n0\r\n*5\r\n$4\r\nSADD\r\n$3\r\nkey\r\n$2\r\nm3\r\n$2\r\nm2\r\n$2\r\nm1\r\n

根据协议内容,大致可以得出:这是第0号数据库,执行了一个SADD key m1 m2 m3命令。这就是Redis采用文件协议格式的原因之一,文本协议具有很高的可读性,可以直接进行修改。而且,文本协议还具有很好的兼容性,而且协议采用了\r\n换行符,所以每次写入命令只需执行追加操作。

既然是追加操作,因此,源码中的函数名字也是如此,catAppendOnlyGenericCommand()函数实现了追加命令到缓冲区中,从这个函数中,可以清楚的看到协议是如何生成的。

// 根据传入的命令和命令参数,将他们还原成协议格式
sds catAppendOnlyGenericCommand(sds dst, int argc, robj **argv) {
    char buf[32];
    int len, j;
    robj *o;

    // 格式:"*<argc>\r\n"
    buf[0] = '*';
    len = 1+ll2string(buf+1,sizeof(buf)-1,argc);
    buf[len++] = '\r';
    buf[len++] = '\n';
    // 拼接到dst的后面
    dst = sdscatlen(dst,buf,len);

    // 遍历所有的参数,建立命令的格式:$<command_len>\r\n<command>\r\n
    for (j = 0; j < argc; j++) {
        o = getDecodedObject(argv[j]);  //解码成字符串对象
        buf[0] = '$';
        len = 1+ll2string(buf+1,sizeof(buf)-1,sdslen(o->ptr));
        buf[len++] = '\r';
        buf[len++] = '\n';
        dst = sdscatlen(dst,buf,len);
        dst = sdscatlen(dst,o->ptr,sdslen(o->ptr));
        dst = sdscatlen(dst,"\r\n",2);
        decrRefCount(o);
    }
    return dst; //返回还原后的协议内容
}

这个函数只是追加一个普通的键,然而一个过期命令的键,需要全部转换为PEXPIREAT,因为必须将相对时间设置为绝对时间,否则还原数据库时,就无法得知该键是否过期,Redis的catAppendOnlyExpireAtCommand()函数实现了这个功能。

// 用sds表示一个 PEXPIREAT 命令,seconds为生存时间,cmd为指定转换的指令
// 这个函数用来转换 EXPIRE and PEXPIRE 命令成 PEXPIREAT ,以便在AOF时,时间总是一个绝对值
sds catAppendOnlyExpireAtCommand(sds buf, struct redisCommand *cmd, robj *key, robj *seconds) {
    long long when;
    robj *argv[3];

    /* Make sure we can use strtoll */
    // 解码成字符串对象,以便使用strtoll函数
    seconds = getDecodedObject(seconds);
    // 取出过期值,long long类型
    when = strtoll(seconds->ptr,NULL,10);
    /* Convert argument into milliseconds for EXPIRE, SETEX, EXPIREAT */
    // 将 EXPIRE, SETEX, EXPIREAT 参数的秒转换成毫秒
    if (cmd->proc == expireCommand || cmd->proc == setexCommand ||
        cmd->proc == expireatCommand)
    {
        when *= 1000;
    }
    /* Convert into absolute time for EXPIRE, PEXPIRE, SETEX, PSETEX */
    // 将 EXPIRE, PEXPIRE, SETEX, PSETEX 命令的参数,从相对时间设置为绝对时间
    if (cmd->proc == expireCommand || cmd->proc == pexpireCommand ||
        cmd->proc == setexCommand || cmd->proc == psetexCommand)
    {
        when += mstime();
    }
    decrRefCount(seconds);

    // 创建一个 PEXPIREAT 命令对象
    argv[0] = createStringObject("PEXPIREAT",9);
    argv[1] = key;
    argv[2] = createStringObjectFromLongLong(when);
    // 将命令还原成协议格式,追加到buf
    buf = catAppendOnlyGenericCommand(buf, 3, argv);
    decrRefCount(argv[0]);
    decrRefCount(argv[2]);
    // 返回buf
    return buf;
}

那么,这两个函数都是实现的底层功能,因此他们都被feedAppendOnlyFile()函数最终调用。

这个函数,创建一个空的简单动态字符串(sds),将当前所有追加命令操作都追加到这个sds中,最终将这个sds追加到server.aof_buf。。还有就是,这个函数在写入键之前,需要显式的写入一个SELECT命令,以正确的将所有键还原到正确的数据库中。

// 将命令追加到AOF文件中
void feedAppendOnlyFile(struct redisCommand *cmd, int dictid, robj **argv, int argc) {
    sds buf = sdsempty();   //设置一个空sds
    robj *tmpargv[3];

    // 使用SELECT命令,显式的设置当前数据库
    if (dictid != server.aof_selected_db) {
        char seldb[64];

        snprintf(seldb,sizeof(seldb),"%d",dictid);
        // 构造SELECT命令的协议格式
        buf = sdscatprintf(buf,"*2\r\n$6\r\nSELECT\r\n$%lu\r\n%s\r\n",
            (unsigned long)strlen(seldb),seldb);
        // 执行AOF时,当前的数据库ID
        server.aof_selected_db = dictid;
    }

    // 如果是 EXPIRE/PEXPIRE/EXPIREAT 三个命令,则要转换成 PEXPIREAT 命令
    if (cmd->proc == expireCommand || cmd->proc == pexpireCommand ||
        cmd->proc == expireatCommand) {
        /* Translate EXPIRE/PEXPIRE/EXPIREAT into PEXPIREAT */
        buf = catAppendOnlyExpireAtCommand(buf,cmd,argv[1],argv[2]);

    // 如果是 SETEX/PSETEX 命令,则转换成 SET and PEXPIREAT
    } else if (cmd->proc == setexCommand || cmd->proc == psetexCommand) {
        /* Translate SETEX/PSETEX to SET and PEXPIREAT */
        // SETEX key seconds value
        // 构建SET命令对象
        tmpargv[0] = createStringObject("SET",3);
        tmpargv[1] = argv[1];
        tmpargv[2] = argv[3];
        // 将SET命令按协议格式追加到buf中
        buf = catAppendOnlyGenericCommand(buf,3,tmpargv);
        decrRefCount(tmpargv[0]);
        // 将SETEX/PSETEX命令和键对象按协议格式追加到buf中
        buf = catAppendOnlyExpireAtCommand(buf,cmd,argv[1],argv[2]);

    // 其他命令直接按协议格式转换,然后追加到buf中
    } else {
        buf = catAppendOnlyGenericCommand(buf,argc,argv);
    }

    // 如果正在进行AOF,则将命令追加到AOF的缓存中,在重新进入事件循环之前,这些命令会被冲洗到磁盘上,并向client回复
    if (server.aof_state == AOF_ON)
        server.aof_buf = sdscatlen(server.aof_buf,buf,sdslen(buf));

    // 如果后台正在进行重写,那么将命令追加到重写缓存区中,以便我们记录重写的AOF文件于当前数据库的差异
    if (server.aof_child_pid != -1)
        aofRewriteBufferAppend((unsigned char*)buf,sdslen(buf));

    sdsfree(buf);
}
2.1.2 缓冲区同步到文件

既然缓冲区提供了高性能的保障,那么缓冲区中的数据安全问题如何解决呢?只要数据存在于缓冲区,那么就有丢失的危险。那么,如果控制同步的频率呢?Redis中给出了3中缓冲区同步文件的策略。

可配置值 说明
AOF_FSYNC_ALWAYS 命令写入aof_buf后调用系统fsync和操作同步到AOF文件,fsync完成后进程程返回
AOF_FSYNC_EVERYSEC 命令写入aof_buf后调用系统write操作,write完成后线程返回。fsync同步文件操作由进程每秒调用一次
AOF_FSYNC_NO 命令写入aof_buf后调用系统write操作,不对AOF文件做fsync同步,同步硬盘由操作由操作系统负责

我们来了解一下,write和fsync操作,在系统中都做了哪些事:

  • write操作:会触发延迟写(delayed write)机制。Linux在内核提供页缓冲区用来提高IO性能,因此,write操作在将数据写入操作系统的缓冲区后就直接返回,而不一定触发同步到磁盘的操作。只有在页空间写满,或者达到特定的时间周期,才会同步到磁盘。因此单纯的write操作也是有数据丢失的风险。
  • fsync操作:针对单个文件操作,做强制硬盘同步,fsync将阻塞直到写入硬盘完成后返回。

虽然Redis提供了三种同步策略,兼顾安全和性能的同步策略是:AOF_FSYNC_EVERYSEC。但是仍有丢失数据的风险,而且不是一秒而是两秒的数据,接下来就看同步的源码实现:

// 将AOF缓存写到磁盘中
// 因为我们需要在回复client之前对AOF执行写操作,唯一的机会是在事件loop中,因此累计所有的AOF到缓存中,在下一次重新进入事件loop之前将缓存写到AOF文件中

// 关于force参数
// 当fsync被设置为每秒执行一次,如果后台仍有线程正在执行fsync操作,我们可能会延迟flush操作,因为write操作可能会被阻塞,当发生这种情况时,说明需要尽快的执行flush操作,会调用 serverCron() 函数。
// 然而如果force被设置为1,我们会无视后台的fsync,直接进行写入操作

#define AOF_WRITE_LOG_ERROR_RATE 30 /* Seconds between errors logging. */
// 将AOF缓存冲洗到磁盘中
void flushAppendOnlyFile(int force) {
    ssize_t nwritten;
    int sync_in_progress = 0;
    mstime_t latency;

    // 如果缓冲区中没有数据,直接返回
    if (sdslen(server.aof_buf) == 0) return;

    // 同步策略是每秒同步一次
    if (server.aof_fsync == AOF_FSYNC_EVERYSEC)
        // AOF同步操作是否在后台正在运行
        sync_in_progress = bioPendingJobsOfType(BIO_AOF_FSYNC) != 0;

    // 同步策略是每秒同步一次,且不是强制同步的
    if (server.aof_fsync == AOF_FSYNC_EVERYSEC && !
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值