美文网首页
Redis持久化

Redis持久化

作者: 逍遥白亦 | 来源:发表于2020-11-24 21:11 被阅读0次

    由于Redis的数据都放在内存里,如果Redis服务重启的话,数据就会丢失,所以Redis提供了RDB和AOF持久化的功能,将内存中的数据保存到磁盘里。

    1. RDB持久化

    RDB持久化功能所生成的RDB文件是一个经过压缩的二进制文件,通过该文件可以还原生成RDB文件时的数据库状态,因为RDB文件是保存在磁盘里的,所以即使Redis服务器进程退出,甚至运行Redis服务器的计算机停机,但只要RDB文件还在,Redis服务器就可以用它来还原数据。

    1.1 RDB文件的创建于载入

    1.1.1 RDB文件创建

    有两个Redis命令可以用于生成RDB文件,一个是SAVE,另一个是BGSAVE。

    1.1.1.1 SAVE命令

    SAVE命令会阻塞Redis服务器进程,直到RDB文件创建完毕为止,在服务器进程阻塞期间,服务器不能处理任何命令请求。

    相关源码:

    void saveCommand(redisClient *c) {
    
        // BGSAVE 已经在执行中,不能再执行 SAVE
        // 否则将产生竞争条件
        if (server.rdb_child_pid != -1) {
            addReplyError(c,"Background save already in progress");
            return;
        }
    
        // 执行 
        if (rdbSave(server.rdb_filename) == REDIS_OK) {
            addReply(c,shared.ok);
        } else {
            addReply(c,shared.err);
        }
    }
    

    1.1.1.2 BGSAVE命令

    BGSAVE命令会派生出一个子进程,然后由子进程负责创建RDB文件,服务器进程继续处理命令请求。

    相关源码:

    void bgsaveCommand(redisClient *c) {
    
        // 不能重复执行 BGSAVE
        if (server.rdb_child_pid != -1) {
            addReplyError(c,"Background save already in progress");
    
        // 不能在 BGREWRITEAOF 正在运行时执行
        } else if (server.aof_child_pid != -1) {
            addReplyError(c,"Can't BGSAVE while AOF log rewriting is in progress");
    
        // 执行 BGSAVE
        } else if (rdbSaveBackground(server.rdb_filename) == REDIS_OK) {
            addReplyStatus(c,"Background saving started");
    
        } else {
            addReply(c,shared.err);
        }
    }
    

    1.1.2 RDB文件载入

    RDB文件的载入工作是在服务器启动时自动执行的,所以Redis并没有专门用于载入RDB文件的命令,只要Redis服务器在启动时检测到RDB文件存在,它就会自动载入RDB文件。

    但是对于整个Redis服务来说,因为AOF文件的更新频率通常比RDB文件的更新频率高,所以如果服务器开启了AOF持久化功能,那么服务器启动时会优先使用AOF文件来还原数据。

    服务器在载入RDB文件期间,会一直处于阻塞状态,直到载入工作完成为止。

    服务器载入文件时的判断流程:


    image

    1.2 自动间隔性保存

    因为BGSAVE命令可以在不阻塞服务器进程的情况下执行,所以Redis允许用户通过设置服务器配置的save选项,让服务器每隔一段时间自动执行一次BGSAVE命令。

    用户可以通过save选项设置多个保存条件,但只要其中任意一个条件被满足,服务器就会执行BGSAVE命令。

    举个例子,如果我们向服务器提供以下配置:

    save 900 1
    save 300 30
    save 60 10000
    

    那么只要满足以下三个条件中的任意一个,BGSAVE命令就会被执行:

    • 服务器在900秒之内,对数据库进行了至少1次修改。
    • 服务器在300秒之内,对数据库进行了至少30次修改。
    • 服务器在60秒之内,对数据库进行了至少10000次修改。

    1.2.1 设置保存条件

    Redis的配置文件redis.conf里有RDB保存条件的默认设置

    #
    # Save the DB on disk:
    #
    #   save <seconds> <changes>
    #
    #   Will save the DB if both the given number of seconds and the given
    #   number of write operations against the DB occurred.
    #
    #   In the example below the behavior will be to save:
    #   after 900 sec (15 min) if at least 1 key changed
    #   after 300 sec (5 min) if at least 10 keys changed
    #   after 60 sec if at least 10000 keys changed
    #
    #   Note: you can disable saving completely by commenting out all "save" lines.
    #
    #   It is also possible to remove all the previously configured save
    #   points by adding a save directive with a single empty string argument
    #   like in the following example:
    #
    #   save ""
    
    save 900 1
    save 300 10
    save 60 10000
    

    接着,服务器程序会根据sava选项所设置的保存条件,设置服务器状态redisServer结构的saveparams属性:

    struct redisServer {
        
        // ...
        
        //记录了保存条件的数组
        struct saveparam *saveparams;
        
        //...
        
    }
    

    saveparams属性是一个数组,数组中的每个元素都是一个saveparam结构,每个saveparams结构都保存了一个save选项设置的保存条件:

    struct saveparam {
        
        // 秒数
        time_t seconds;
        
        // 修改数
        int changes;
        
    }
    

    比如说,按照默认save设置,服务器状态中的saveparams数组将会是下图这样:


    image

    1.2.2 dirty计数器和lastsave属性

    除了saveparams数组之外,服务器状态还维持着一个dirty计数器,以及一个lastsave属性:

    • dirty计数器记录距离上一次成功执行SAVE命令或者BGSAVE命令之后,服务器对数据库状态进行了多少次修改。
    • lastsave属性是一个UNIX时间戳,记录了服务器上一次成功执行SAVA命令或者BGSAVE命令的时间。

    1.2.3 检查保存条件是否满足

    Redis 的服务器周期性操作函数 serverCron 默认每隔100 毫秒就会执行一次,该函数用于对正在运行的服务器进行维护,它的其中一项工作就是检查 save 选项所设置的保存条件是否已经满足,如果满足的话,就执行 BGSAVE 命令。

    2. AOF持久化

    除了RDB 持久化功能之外,Redis 还提供了AOF(Append Only File)持久化功能。与RDB持久化通过保存数据库中的键值对来记录数据库状态不同,AOF持久化是通过保存Redis服务器所执行的写命令来记录数据库状态的。

    在配置文件中开启AOF的功能:

     appendonly yes
    

    2.1 AOF持久化的实现

    AOF持久化功能的实现可以分为命令追加(append)、文件写入、文件同步(sync)三个步骤

    2.1.1 命令追加

    当AOF持久化功能开启时,服务器在执行完一个写命令之后,会以协议格式将被执行的写命令追加到服务器状态的aof_buf缓冲区的末尾。

    2.1.2 AOF文件的写入与同步

    Redis 的服务器进程就是一个事件循环(loop),这个循环中的文件事件负责接收客户端的命令请求,以及向客户端发送命令回复,而时间事件则负责执行像 serverCron 函数这样需要定时运行的函数。

    服务器每次结束一个事件循环之前,它都会调用flushAppendOnlyFile函数。

    2.2 AOF文件的载入与数据还原

    因为AOF文件里面包含了重建数据库状态所需的所有写命令,所以服务器只要读入并重新执行一遍AOF文件里面保存的写命令,就可以还原服务器关闭之前的数据库状态。

    AOF文件载入过程如下:

    image

    2.3 AOF重写

    因为 AOF 持久化是通过保存被执行的写命令来记录数据库状态的,所以随着服务器运行时间的流逝,AOF文件中的内容会越来越多,文件的体积也会越来越大,如果不加以控制的话,体积过大的AOF文件很可能对 Redis 服务器、甚至整个宿主计算机造成影响,并且AOF文件的体积越大,使用AOF 文件来进行数据还原所需的时间就越多。

    举个例子,如果客户端执行了以下命令:

    那么光是为了记录这个readcount键的状态,AOF文件就需要保存六条命令。为了解决AOF文件过大的问题,Redis提供了AOF文件重写功能。通过该功能,Redis服务器可以创建一个新的AOF文件来替代现有的AOF文件。

    手动重写命令:

    BGREWRITEAOF
    

    2.3.1 AOF文件重写的实现

    虽然 Redis 将生成新AOF文件替换旧AOF文件的功能命名为"AOF文件重写",但实际上,AOF文件重写并不需要对现有的AOF文件进行任何读取、分析或者写入操作,这个功能是通过读取服务器当前的数据库状态来实现的。

    重写原理就是首先从数据库中读取键现在的值,然后用一条命令去记录键值对,代替之前记录这个键值对的多条命令,这就是AOF重写功能的实现原理。

    相关源码:

    /* Write a sequence of commands able to fully rebuild the dataset into
     * "filename". Used both by REWRITEAOF and BGREWRITEAOF.
     *
     * 将一集足以还原当前数据集的命令写入到 filename 指定的文件中。
     *
     * 这个函数被 REWRITEAOF 和 BGREWRITEAOF 两个命令调用。
     * (REWRITEAOF 似乎已经是一个废弃的命令)
     *
     * In order to minimize the number of commands needed in the rewritten
     * log Redis uses variadic commands when possible, such as RPUSH, SADD
     * and ZADD. However at max REDIS_AOF_REWRITE_ITEMS_PER_CMD items per time
     * are inserted using a single command. 
     *
     * 为了最小化重建数据集所需执行的命令数量,
     * Redis 会尽可能地使用接受可变参数数量的命令,比如 RPUSH 、SADD 和 ZADD 等。
     *
     * 不过单个命令每次处理的元素数量不能超过 REDIS_AOF_REWRITE_ITEMS_PER_CMD 。
     */
    int rewriteAppendOnlyFile(char *filename) {
        dictIterator *di = NULL;
        dictEntry *de;
        rio aof;
        FILE *fp;
        char tmpfile[256];
        int j;
        long long now = mstime();
    
        /* Note that we have to use a different temp name here compared to the
         * one used by rewriteAppendOnlyFileBackground() function. 
         *
         * 创建临时文件
         *
         * 注意这里创建的文件名和 rewriteAppendOnlyFileBackground() 创建的文件名稍有不同
         */
        snprintf(tmpfile,256,"temp-rewriteaof-%d.aof", (int) getpid());
        fp = fopen(tmpfile,"w");
        if (!fp) {
            redisLog(REDIS_WARNING, "Opening the temp file for AOF rewrite in rewriteAppendOnlyFile(): %s", strerror(errno));
            return REDIS_ERR;
        }
    
        // 初始化文件 io
        rioInitWithFile(&aof,fp);
    
        // 设置每写入 REDIS_AOF_AUTOSYNC_BYTES 字节
        // 就执行一次 FSYNC 
        // 防止缓存中积累太多命令内容,造成 I/O 阻塞时间过长
        if (server.aof_rewrite_incremental_fsync)
            rioSetAutoSync(&aof,REDIS_AOF_AUTOSYNC_BYTES);
    
        // 遍历所有数据库
        for (j = 0; j < server.dbnum; j++) {
    
            char selectcmd[] = "*2\r\n$6\r\nSELECT\r\n";
    
            redisDb *db = server.db+j;
    
            // 指向键空间
            dict *d = db->dict;
            if (dictSize(d) == 0) continue;
    
            // 创建键空间迭代器
            di = dictGetSafeIterator(d);
            if (!di) {
                fclose(fp);
                return REDIS_ERR;
            }
    
            /* SELECT the new DB 
             *
             * 首先写入 SELECT 命令,确保之后的数据会被插入到正确的数据库上
             */
            if (rioWrite(&aof,selectcmd,sizeof(selectcmd)-1) == 0) goto werr;
            if (rioWriteBulkLongLong(&aof,j) == 0) goto werr;
    
            /* Iterate this DB writing every entry 
             *
             * 遍历数据库所有键,并通过命令将它们的当前状态(值)记录到新 AOF 文件中
             */
            while((de = dictNext(di)) != NULL) {
                sds keystr;
                robj key, *o;
                long long expiretime;
    
                // 取出键
                keystr = dictGetKey(de);
    
                // 取出值
                o = dictGetVal(de);
                initStaticStringObject(key,keystr);
    
                // 取出过期时间
                expiretime = getExpire(db,&key);
    
                /* If this key is already expired skip it 
                 *
                 * 如果键已经过期,那么跳过它,不保存
                 */
                if (expiretime != -1 && expiretime < now) continue;
    
                /* Save the key and associated value 
                 *
                 * 根据值的类型,选择适当的命令来保存值
                 */
                if (o->type == REDIS_STRING) {
                    /* Emit a SET command */
                    char cmd[]="*3\r\n$3\r\nSET\r\n";
                    if (rioWrite(&aof,cmd,sizeof(cmd)-1) == 0) goto werr;
                    /* Key and value */
                    if (rioWriteBulkObject(&aof,&key) == 0) goto werr;
                    if (rioWriteBulkObject(&aof,o) == 0) goto werr;
                } else if (o->type == REDIS_LIST) {
                    if (rewriteListObject(&aof,&key,o) == 0) goto werr;
                } else if (o->type == REDIS_SET) {
                    if (rewriteSetObject(&aof,&key,o) == 0) goto werr;
                } else if (o->type == REDIS_ZSET) {
                    if (rewriteSortedSetObject(&aof,&key,o) == 0) goto werr;
                } else if (o->type == REDIS_HASH) {
                    if (rewriteHashObject(&aof,&key,o) == 0) goto werr;
                } else {
                    redisPanic("Unknown object type");
                }
    
                /* Save the expire time 
                 *
                 * 保存键的过期时间
                 */
                if (expiretime != -1) {
                    char cmd[]="*3\r\n$9\r\nPEXPIREAT\r\n";
    
                    // 写入 PEXPIREAT expiretime 命令
                    if (rioWrite(&aof,cmd,sizeof(cmd)-1) == 0) goto werr;
                    if (rioWriteBulkObject(&aof,&key) == 0) goto werr;
                    if (rioWriteBulkLongLong(&aof,expiretime) == 0) goto werr;
                }
            }
    
            // 释放迭代器
            dictReleaseIterator(di);
        }
    
        /* Make sure data will not remain on the OS's output buffers */
        // 冲洗并关闭新 AOF 文件
        if (fflush(fp) == EOF) goto werr;
        if (aof_fsync(fileno(fp)) == -1) goto werr;
        if (fclose(fp) == EOF) goto werr;
    
        /* Use RENAME to make sure the DB file is changed atomically only
         * if the generate DB file is ok. 
         *
         * 原子地改名,用重写后的新 AOF 文件覆盖旧 AOF 文件
         */
        if (rename(tmpfile,filename) == -1) {
            redisLog(REDIS_WARNING,"Error moving temp append only file on the final destination: %s", strerror(errno));
            unlink(tmpfile);
            return REDIS_ERR;
        }
    
        redisLog(REDIS_NOTICE,"SYNC append only file rewrite performed");
    
        return REDIS_OK;
    
    werr:
        fclose(fp);
        unlink(tmpfile);
        redisLog(REDIS_WARNING,"Write error writing append only file on disk: %s", strerror(errno));
        if (di) dictReleaseIterator(di);
        return REDIS_ERR;
    }
    

    2.3.2 AOF后台重写

    前面介绍的AOF重写程序aof_rewrite函数可以很好地完成创建一个新AOF文件的任务,但是,因为这个函数会进行大量的写入操作,所以调用这个函数的线程将被长时间阻塞,因为Redis服务器使用单个线程来处理请求,所以如果由服务器直接调用aof_rewrite函数的话,那么重写AOF文件期间,服务器将无法处理客户端发来的命令请求。所以Redis决定将AOF重写程序放到子进程里执行。

    但是使用子进程会引发一个新的问题,因为子进程在进行AOF重写期间,服务器进程还需要继续处理命令请求,而新的命令可能会对现有的数据库状态进行修改,从而使得服务器当前的数据库状态和重写后的AOF文件所保存的数据库状态不一致。

    为了解决这种数据不一致问题,Redis服务器设置了一个AOF重写缓冲区,这个缓冲区在服务器创建子进程之后开始使用,当Redis服务器执行完一个写命令之后,它会同时将这个写命令发送给AOF缓冲区和AOF重写缓冲区。

    这也就是说,在子进程执行AOF重写期间,服务器进程需要执行以下三个工作:

    1. 执行客户端发来的命令
    2. 将执行后的写命令追加到AOF缓冲区
    3. 将执行后的写命令追加到AOF重写缓冲区

    3. 混合持久化

    重启 Redis 时,我们很少使用rdb来恢复内存状态,因为会丢失大量数据。我们通常使用AOF日志重放,但是重放AOF日志性能相对rdb来说要慢很多,这样在Redis实
    例很大的情况下,启动需要花费很长的时间。

    Redis 4.0 为了解决这个问题,带来了一个新的持久化选项——混合持久化。将rdb 文件的内容和增量的AOF日志文件存在一起。这里的AOF 日志不再是全量的日志,而是自持久化开始到持久化结束的这段时间发生的增量 AOF日志,通常这部分AOF日志很小。

    通过如下配置可以开启混合持久化(必须先开启AOF)

    aof‐use‐rdb‐preamble yes
    

    如果开启了混合持久化,AOF在重写时,不再是单纯将内存数据转换为RESP命令写入AOF文件,而是将 重写这一刻之前的内存做RDB快照处理,并且将RDB快照内容和增量的AOF修改内存数据的命令存在一 起,都写入新的AOF文件,新的文件一开始不叫appendonly.aof,等到重写完新的AOF文件才会进行改 名,覆盖原有的AOF文件,完成新旧两个AOF文件的替换。

    于是在 Redis 重启的时候,可以先加载 RDB 的内容,然后再重放增量 AOF 日志就可以完全替代之前的 AOF 全量文件重放,因此重启效率大幅得到提升。

    参考资料

    1. 《Redis设计与实现》
    2. 《Redis深度历险》
    3. Redis源码注释版 https://github.com/huangz1990/redis-3.0-annotated

    相关文章

      网友评论

          本文标题:Redis持久化

          本文链接:https://www.haomeiwen.com/subject/ermwiktx.html