十四:Redis持久化之rdb

十四:Redis持久化之rdb

Redis提供了两种持久化的方式:AOF与RDB: RDB偏重在保存某个时间redis的快照;AOF
偏重在实时保存的效率上。两者方案侧重点不同。

RDB

RDB:redis database 意思时在指定时间间隔内将内存中的数据写入到磁盘也就是说一段时间对redis进行一次快照;rdb写入的是二进制数据所以文件
大小也更紧凑些。

RDB: 一般应用在对数据冷备、复制传输,重启redis加载rdb的数据也更快一些。

配置项

可以在redis.conf中配置是否开启RDB(默认已开启)、多久保存一下快照以及快照文件名、存储快照地址。如果不想使用快照,你可以注释掉
下面的快照。

下面是在redis.cong配置rdb的配置项。


################################ SNAPSHOTTING  ################################

# 表示多久保存一次 save  <seconds> <changes>  表示在多少秒内直到有多少key改变才触发rdb持久化
#900秒(15分钟)内至少一个key改变触发持久化
save 900 1
#300(5分钟)内至少10个Key改变触发持久化
save 300 10
#60(1分钟)内至少10000个key改变触发出持久化
save 60 10000

# 当设置为yes表示:当进行快照的时候出现了失败,redis将拒绝接收后面客户端的写入命令操作
# 当设置为no表示:当进行快照的时候出现了失败,redis将接收接收后面客户端的写入命令操作
stop-writes-on-bgsave-error yes


# 是否开启快照文件压缩(使用LZF算法)
# 默认为yes,会压缩rdb的内容,节省空间
# 当设置为no,表示不开启压缩节省cpu,实际上不建议这中做法,因为rdb使用场景是冷备、复制传输,
# 希望文件大小越小越好
rdbcompression yes

# 是否对rdb文件使用CRC64算法进行检查,如果开启在保存和加载rdb文件市需要消耗10%性能,
# 如果禁用则不进行校验
rdbchecksum yes

#  配置rdb保存的文件名
dbfilename "dump.rdb"


# rbm保存在磁盘的目录(仅仅是目录,这里不要带文件名),aof的保存路径也共用这个目录
dir "/homepa1/redis"


运行流程

这里只说明一下bgsave的流程(save的流程是同步的)

bgsave运行流程

流程解释

  1. 客户端执行bgsave命令,服务端接收到命令
  2. 父进程判断当前是否有正在执行的子进程,如果有则直接返回Background save already in progress,否则fork一个子进程出来
  3. fork(fork这段时间不响应)出了子进程,父进程继续响应客户端的请求
  4. 子进程保存当前所有key以及其value到rdb文件
  5. 子进程保存完毕,信号量通知父进程

我们可以看到bgsave这里处理fork子进程的时候会阻塞,其他都是可以响应客户端请求的;
如果是save,会在接收到save命令后不fork子进程,而是阻塞式的写入rdb,此时是不能响应客户端的请求,
对于键多并且value非常大的情况下对redis灾难性。会导致整个缓存层对外停止服务。

相关命令

save

/**
**  redis.c
**/

struct redisCommand readonlyCommandTable[] = {
  {"save",saveCommand,1,0,NULL,0,0,0}

}


1. 如果有子进程正在处理,则直接返回
2. 同步保存rdb,如果成功返回 ok
3. 保存失败返回err

/**
** db.c
**/

void saveCommand(redisClient *c) {
    //1.如果有子进程正在处理,则直接返回
    if (server.bgsavechildpid != -1) {
        addReplyError(c,"Background save already in progress");
        return;
    }
    //2. 同步保存rdb,如果成功返回 ok
    if (rdbSave(server.dbfilename) == REDIS_OK) {
        addReply(c,shared.ok);
    } else {
    //3. 保存失败返回err
        addReply(c,shared.err);
    }
}


1. 构造临时保存rdb文件名
2. 打开文件,准备写操作
3. 打开失败,直接返回错误信息 
4. 写入rdb文件开始字符失败,跳转到关闭文件,以及输出错误
5. 循环保存redis每个数据库中的键值对
6. 如果当前循环数据库键个数为0,则继续下一个数据库
7. 构建当前循环数据库键多迭代器
8. 如果没有生成迭代器,则关闭文件,返回错误
9. 写入当前保存键值对的数据库
10. 迭代保存单个键值对 
11. 如果过期时间保存过期时间
12. 保存键值对
13. 写入rdb文件结尾标识符         
14. 刷新文件,同步写入,关闭文件
15. 使用redis配置文件配置的rdb文件名重命名上面的写入的临时文件 
16. 保存成功,输出日志
17. 上面的发生错误跳转到这里,关闭文件释放临时文件,输出日志,关闭迭代器


/**
** rdb.c
**
**/

/* Save the DB on disk. Return REDIS_ERR on error, REDIS_OK on success */
int rdbSave(char *filename) {
    dictIterator *di = NULL;
    dictEntry *de;
    FILE *fp;
    char tmpfile[256];
    int j;
    time_t now = time(NULL);

    
    if (server.vm_enabled)
        waitEmptyIOJobsQueue();
    // 1. 构造临时保存rdb文件名
    snprintf(tmpfile,256,"temp-%d.rdb", (int) getpid());
    // 2. 打开文件,准备写操作
    fp = fopen(tmpfile,"w");
    //3. 打开失败,直接返回错误信息 
    if (!fp) {
        redisLog(REDIS_WARNING, "Failed saving the DB: %s", strerror(errno));
        return REDIS_ERR;
    }
    // 4. 写入rdb文件开始字符失败,跳转到关闭文件,以及输出错误
    if (fwrite("REDIS0001",9,1,fp) == 0) goto werr;
    // 5. 循环保存redis每个数据库中的键值对
    for (j = 0; j < server.dbnum; j++) {
        redisDb *db = server.db+j;
        dict *d = db->dict;
        //6. 如果当前循环数据库键个数为0,则继续下一个数据库
        if (dictSize(d) == 0) continue;
        //7. 构建当前循环数据库键多迭代器
        di = dictGetSafeIterator(d);
        //8. 如果没有生成迭代器,则关闭文件,返回错误
        if (!di) {
            fclose(fp);
            return REDIS_ERR;
        }

        //9. 写入当前保存键值对的数据库
        if (rdbSaveType(fp,REDIS_SELECTDB) == -1) goto werr;
        if (rdbSaveLen(fp,j) == -1) goto werr;

        //10. 迭代保存单个键值对 
        while((de = dictNext(di)) != NULL) {
            sds keystr = dictGetEntryKey(de);
            robj key, *o = dictGetEntryVal(de);
            time_t expiretime;
            
            initStaticStringObject(key,keystr);
            expiretime = getExpire(db,&key);

            //11. 如果过期时间保存过期时间
            if (expiretime != -1) {
                /* If this key is already expired skip it */
                if (expiretime < now) continue;
                if (rdbSaveType(fp,REDIS_EXPIRETIME) == -1) goto werr;
                if (rdbSaveTime(fp,expiretime) == -1) goto werr;
            }
           //12. 保存键值对
            if (!server.vm_enabled || o->storage == REDIS_VM_MEMORY ||
                                      o->storage == REDIS_VM_SWAPPING) {
                /* Save type, key, value */
                if (rdbSaveType(fp,o->type) == -1) goto werr;
                if (rdbSaveStringObject(fp,&key) == -1) goto werr;
                if (rdbSaveObject(fp,o) == -1) goto werr;
            } else {
                /* REDIS_VM_SWAPPED or REDIS_VM_LOADING */
                robj *po;
                /* Get a preview of the object in memory */
                po = vmPreviewObject(o);
                /* Save type, key, value */
                if (rdbSaveType(fp,po->type) == -1) goto werr;
                if (rdbSaveStringObject(fp,&key) == -1) goto werr;
                if (rdbSaveObject(fp,po) == -1) goto werr;
                /* Remove the loaded object from memory */
                decrRefCount(po);
            }
        }
        dictReleaseIterator(di);
    }
    //13. 写入rdb文件结尾标识符
    if (rdbSaveType(fp,REDIS_EOF) == -1) goto werr;

   //14. 刷新文件,同步写入,关闭文件
    fflush(fp);
    fsync(fileno(fp));
    fclose(fp);

    //15. 使用redis配置文件配置的rdb文件名重命名上面的写入的临时文件 
    if (rename(tmpfile,filename) == -1) {
        redisLog(REDIS_WARNING,"Error moving temp DB file on the final destination: %s", strerror(errno));
        unlink(tmpfile);
        return REDIS_ERR;
    }
    //16. 保存成功,输出日志
    redisLog(REDIS_NOTICE,"DB saved on disk");
    server.dirty = 0;
    server.lastsave = time(NULL);
    return REDIS_OK;
    //17. 上面的发生错误跳转到这里,关闭文件释放临时文件,输出日志,关闭迭代器
werr:
    fclose(fp);
    unlink(tmpfile);
    redisLog(REDIS_WARNING,"Write error saving DB on disk: %s", strerror(errno));
    if (di) dictReleaseIterator(di);
    return REDIS_ERR;
}


/**
** 上面的方法保存时最终会调用下面这个方法
**  这个方法也时调用fwrite这个C的标准库函数
**/
static int rdbWriteRaw(FILE *fp, void *p, size_t len) {
    if (fp != NULL && fwrite(p,len,1,fp) == 0) return -1;
    return len;
}

size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream)

- prt  指针指向要写入的元素
- size 写入元素的大小
- nmemb 写入的元素个数
- stream 打开的文件流

返回值:成功写入的元素个数,如果不等于nmemb则返回错误


bgsave

关于fork函数需要理解,每当调用一次fork函数时,会返回两个两次。
一次是在调用进程中(父进程)返回一次,返回值是新派生的进程的进程ID。
一次是在子进程中返回,返回值是0,代表当前进程为子进程。
如果返回-1,那么则代表在创建子进程的过程中出现了错误。


/**
**  redis.c
**/

struct redisCommand readonlyCommandTable[] = {
   {"bgsave",bgsaveCommand,1,0,NULL,0,0,0},
}

1. 如果有子进程在处理则返回
2. fork子进程来处理rdb
3. 失败返回错误

/**
** db.c
**/
void bgsaveCommand(redisClient *c) {
    //1. 如果有子进程在处理则返回
    if (server.bgsavechildpid != -1) {
        addReplyError(c,"Background save already in progress");
        return;
    }
    //2. fork子进程来处理rdb
    if (rdbSaveBackground(server.dbfilename) == REDIS_OK) {
        addReplyStatus(c,"Background saving started");
    } else {
    //3. 失败返回错误
        addReply(c,shared.err);
    }
}



1. 如果有子进程在处理则返回
2. fork出子进程,父子进程执行各自的逻辑 fork = 0:表示现在时子进程,fork > 0表示父进程, fork = -1表示fork失败
3.调用save命令保存rdb的函数,不过与这里相比,bgsave是在子进程中执行,save是在父进程中执行
4. 父进程中执行下面逻辑,如果子进程等于-1,表示子进程创建失败,输出失败日志,返回错误
5. 成功,子进程在执行rdb生成快照,此时主进程可以继续处理响应

/**
**fork子进程来生成快照
** rdb.c
**/

int rdbSaveBackground(char *filename) {
    pid_t childpid;
     //1. 如果有子进程在处理则返回
    if (server.bgsavechildpid != -1) return REDIS_ERR;
    if (server.vm_enabled) waitEmptyIOJobsQueue();
    server.dirty_before_bgsave = server.dirty;
    //2. fork出子进程,父子进程执行各自的逻辑 fork = 0:表示现在时子进程,fork > 0表示父进程, fork = -1表示fork失败
    if ((childpid = fork()) == 0) {
        /* Child */
        if (server.vm_enabled) vmReopenSwapFile();
        if (server.ipfd > 0) close(server.ipfd);
        if (server.sofd > 0) close(server.sofd);
        //3.调用save命令保存rdb的函数,不过与这里相比,bgsave是在子进程中执行,save是在父进程中执行
        if (rdbSave(filename) == REDIS_OK) {
            _exit(0);
        } else {
            _exit(1);
        }
    } else {
        /* Parent */
        //4. 父进程中执行下面逻辑,如果子进程等于-1,表示子进程创建失败,输出失败日志,返回错误
        if (childpid == -1) {
            redisLog(REDIS_WARNING,"Can't save in background: fork: %s",
                strerror(errno));
            return REDIS_ERR;
        }
        //5. 成功,子进程在执行rdb生成快照,此时主进程可以继续处理响应
        redisLog(REDIS_NOTICE,"Background saving started by pid %d",childpid);
        server.bgsavechildpid = childpid;
        updateDictResizePolicy();
        return REDIS_OK;
    }
    return REDIS_OK; /* unreached */
}

copy-on-write写时复制技术

在上面我们fork进程的时候,有没有想过一个问题呢,如果父进程的键值对很大, 占用的内存很多,rdb的时候,fork一个进程岂不是需要两倍的内存,
以及更多时间呢?

事实上,fork的时候时不需要两倍内存的,因为这里使用了linux的cow技术

写时复制技术(cow): fork创建出来的子进程,与父进程共享物理内存空间,父子进程通过不同的页表指向想用的物理地址。

虽然fork创建的子进程不需要拷贝父进程的物理内存空间,但是会复制父进程的空间内存页
表。例如对于10GB的Redis进程,需要复制大约20MB的内存页表,因此fork
操作耗时跟进程总内存量息息相关(引自:《redis开发与运维》作者:付磊 张益军)

父子进程,内存页

fork函数,每当调用一次fork函数时,会返回两次值,一次是在调用进程中(父进程)返回一次,
返回值时新生成的进程也即子进程id;一次是在子进程中返回,返回值是0,代表当前进程为子进程;
如果返回-1,则表示创建子进程失败。(两次返回可以看到上面rdbSaveBackground方法)

参考

Linux进程概念

C library function - fwrite()

写时复制(Copy On Write)

《Redis开发与运维》作者:付磊、张益军

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值