楼主: jieforest

[转载] 深入剖析Redis RDB持久化机制

[复制链接]
论坛徽章:
277
马上加薪
日期:2014-02-19 11:55:14马上有对象
日期:2014-02-19 11:55:14马上有钱
日期:2014-02-19 11:55:14马上有房
日期:2014-02-19 11:55:14马上有车
日期:2014-02-19 11:55:14马上有车
日期:2014-02-18 16:41:112014年新春福章
日期:2014-02-18 16:41:11版主9段
日期:2012-11-25 02:21:03ITPUB年度最佳版主
日期:2014-02-19 10:05:27现任管理团队成员
日期:2011-05-07 01:45:08
11#
 楼主| 发表于 2012-11-5 14:11 | 只看该作者
我们可以看到,在读取rdb文件时,当发现长度类型是REDIS_RDB_ENCVAL,把编码类型返回。

我们来看看知道编码类型后的处理

Rdb.c:633
  1. robj *rdbGenericLoadStringObject(FILE*fp, int encode) {
  2.     int isencoded;
  3.     uint32_t len;
  4.     sds val;

  5.     len = rdbLoadLen(fp,&isencoded);
  6.     if (isencoded) {
  7.         switch(len) {
  8.         case REDIS_RDB_ENC_INT8:
  9.         case REDIS_RDB_ENC_INT16:
  10.         case REDIS_RDB_ENC_INT32:
  11.             return rdbLoadIntegerObject(fp,len,encode);
  12.         case REDIS_RDB_ENC_LZF:
  13.             return rdbLoadLzfStringObject(fp);
  14.         default:
  15.             redisPanic("Unknown RDB encoding type");
  16.         }
  17.     }

  18.     if (len == REDIS_RDB_LENERR) return NULL;
  19.     val = sdsnewlen(NULL,len);
  20.     if (len && fread(val,len,1,fp) == 0) {
  21.         sdsfree(val);
  22.         return NULL;
  23.     }
  24.     return createObject(REDIS_STRING,val);
  25. }
复制代码

使用道具 举报

回复
论坛徽章:
277
马上加薪
日期:2014-02-19 11:55:14马上有对象
日期:2014-02-19 11:55:14马上有钱
日期:2014-02-19 11:55:14马上有房
日期:2014-02-19 11:55:14马上有车
日期:2014-02-19 11:55:14马上有车
日期:2014-02-18 16:41:112014年新春福章
日期:2014-02-18 16:41:11版主9段
日期:2012-11-25 02:21:03ITPUB年度最佳版主
日期:2014-02-19 10:05:27现任管理团队成员
日期:2011-05-07 01:45:08
12#
 楼主| 发表于 2012-11-6 15:11 | 只看该作者
1. 读取长度

2. 如果长度类型是有编码信息的,则根据编码类型进行读取

3. 如果长度类型是有效长度,则根据长度信息读取字符串

REDIS_EXPIRETIME类型

1. 如果一个key被expire设置过,那么在该key与value的前面会有一个REDIS_EXPIRETIME类型与其对应的值。

2. REDIS_EXPIRETIME类型对应的值是过期时间点的timestamp

3. REDIS_EXPIRETIME类型与其值是可选的,不是必须的,只有被expire设置过的key才有这个值

使用道具 举报

回复
论坛徽章:
277
马上加薪
日期:2014-02-19 11:55:14马上有对象
日期:2014-02-19 11:55:14马上有钱
日期:2014-02-19 11:55:14马上有房
日期:2014-02-19 11:55:14马上有车
日期:2014-02-19 11:55:14马上有车
日期:2014-02-18 16:41:112014年新春福章
日期:2014-02-18 16:41:11版主9段
日期:2012-11-25 02:21:03ITPUB年度最佳版主
日期:2014-02-19 10:05:27现任管理团队成员
日期:2011-05-07 01:45:08
13#
 楼主| 发表于 2012-11-6 15:11 | 只看该作者
假设有一个key被expire命令设置过,把这REDIS_EXPIRETIME类型代入到上边的rdb文件的格式中,那么rdb文件的整体格式变成为:

文件签名 | 版本号 | REDIS_SELECTDB类型 | db编号 | REDIS_EXPIRETIME类型 | timestamp | 类型 | 值 | … | REDIS_SELECTD 类型 | db编号 | 类型 | 值 | … | REDIS_EOF类型

数据类型

数据类型主要有以下类型:

REDIS_STRING类型
REDIS_LIST类型
REDIS_SET类型
REDIS_ZSET类型
REDIS_HASH类型
REDIS_VMPOINTER类型
REDIS_HASH_ZIPMAP类型
REDIS_LIST_ZIPLIST类型
REDIS_SET_INTSET类型
REDIS_ZSET_ZIPLIST类型

其中REDIS_HASH_ZIPMAP,REDIS_LIST_ZIPLIST,REDIS_SET_INTSET和REDIS_ZSET_ZIPLIST这四种数据类型都是只在rdb文件中才有的类型,其他的数据类型其实就是val对象中type字段存储的值。

使用道具 举报

回复
论坛徽章:
277
马上加薪
日期:2014-02-19 11:55:14马上有对象
日期:2014-02-19 11:55:14马上有钱
日期:2014-02-19 11:55:14马上有房
日期:2014-02-19 11:55:14马上有车
日期:2014-02-19 11:55:14马上有车
日期:2014-02-18 16:41:112014年新春福章
日期:2014-02-18 16:41:11版主9段
日期:2012-11-25 02:21:03ITPUB年度最佳版主
日期:2014-02-19 10:05:27现任管理团队成员
日期:2011-05-07 01:45:08
14#
 楼主| 发表于 2012-11-6 15:12 | 只看该作者
下边以REDIS_STRING类型和REDIS_LIST类型为例进行详解,其他类型都类似

REDIS_STRING类型

假设rdb文件中有一个值是REDIS_STRING类型,比如执行了一个set mykey myval命令,则在rdb文件表示为:

REDIS_STRING类型 | 值

其中值包含了key的长度,key的值,val的长度和val的值,把REDIS_STRING类型值的格式代入得:

REDIS_STRING类型 | keylen | mykey | vallen | myval

长度的存储格式见rdb中长度的存储

使用道具 举报

回复
论坛徽章:
277
马上加薪
日期:2014-02-19 11:55:14马上有对象
日期:2014-02-19 11:55:14马上有钱
日期:2014-02-19 11:55:14马上有房
日期:2014-02-19 11:55:14马上有车
日期:2014-02-19 11:55:14马上有车
日期:2014-02-18 16:41:112014年新春福章
日期:2014-02-18 16:41:11版主9段
日期:2012-11-25 02:21:03ITPUB年度最佳版主
日期:2014-02-19 10:05:27现任管理团队成员
日期:2011-05-07 01:45:08
15#
 楼主| 发表于 2012-11-8 11:17 | 只看该作者
REDIS_LIST类型

1.List

REDIS_LIST | listlen | len | value | len | value

Listlen是链表长度

Len是链表结点的值value的长度

Value是链表结点的值

2.Ziplist

REDIS_ENCODING_ZIPLIST | ziplist

Ziplist就是通过字符串来实现的,直接将其存储于rdb文件中即可

使用道具 举报

回复
论坛徽章:
277
马上加薪
日期:2014-02-19 11:55:14马上有对象
日期:2014-02-19 11:55:14马上有钱
日期:2014-02-19 11:55:14马上有房
日期:2014-02-19 11:55:14马上有车
日期:2014-02-19 11:55:14马上有车
日期:2014-02-18 16:41:112014年新春福章
日期:2014-02-18 16:41:11版主9段
日期:2012-11-25 02:21:03ITPUB年度最佳版主
日期:2014-02-19 10:05:27现任管理团队成员
日期:2011-05-07 01:45:08
16#
 楼主| 发表于 2012-11-8 11:17 | 只看该作者
快照保存

我们接下来看看具体实现细节

不管是触发条件满足后通过fork子进程来保存快照还是通过save命令来触发,其实都是调用的同一个函数rdbSave(rdb.c:394)。

先来看看触发条件满足后通过fork子进程的实现保存快照的的实现

在每100ms调用一次的serverCron函数中会对快照保存的条件进行检查,如果满足了则进行快照保存

Redis.c:604
  1.     /* Check if a background saving or AOF rewrite in progress terminated */
  2.     if (server.bgsavechildpid != -1 || server.bgrewritechildpid != -1) {
  3.         int statloc;
  4.         pid_t pid;

  5.         if ((pid = wait3(&statloc,WNOHANG,NULL)) != 0) {
  6.             if (pid == server.bgsavechildpid) {
  7.                 backgroundSaveDoneHandler(statloc);
  8.             }

  9.             updateDictResizePolicy();
  10.         }
  11.     } else {
  12.          time_t now = time(NULL);

  13.         /* If there is not a background saving in progress check if
  14.          * we have to save now */
  15.          for (j = 0; j < server.saveparamslen; j++) {
  16.             struct saveparam *sp = server.saveparams+j;

  17.             if (server.dirty >= sp->changes &&
  18.                 now-server.lastsave > sp->seconds) {
  19.                 redisLog(REDIS_NOTICE,"%d changes in %d seconds. Saving…",
  20.                     sp->changes, sp->seconds);
  21.                 rdbSaveBackground(server.dbfilename);
  22.                 break;
  23.             }
  24.          }
  25.         …
  26. }
复制代码

使用道具 举报

回复
论坛徽章:
277
马上加薪
日期:2014-02-19 11:55:14马上有对象
日期:2014-02-19 11:55:14马上有钱
日期:2014-02-19 11:55:14马上有房
日期:2014-02-19 11:55:14马上有车
日期:2014-02-19 11:55:14马上有车
日期:2014-02-18 16:41:112014年新春福章
日期:2014-02-18 16:41:11版主9段
日期:2012-11-25 02:21:03ITPUB年度最佳版主
日期:2014-02-19 10:05:27现任管理团队成员
日期:2011-05-07 01:45:08
17#
 楼主| 发表于 2012-11-8 11:18 | 只看该作者
如果后端有写rdb的子进程或者写aof的子进程,则检查rdb子进程是否退出了,如果退出了则进行一些收尾处理,比如更新脏数据计数server.dirty和最近快照保存时间server.lastsave。

如果后端没有写rdb的子进程且没有写aof的子进程,则判断下是否有触发写rdb的条件满足了,如果有条件满足,则通过调用rdbSaveBackground函数进行快照保存。

跟着进rdbSaveBackground函数里边看看

Rdb.c:499
  1. int rdbSaveBackground(char *filename) {
  2.     pid_t childpid;
  3.     long long start;

  4.     if (server.bgsavechildpid != -1) return REDIS_ERR;
  5.     if (server.vm_enabled) waitEmptyIOJobsQueue();
  6.     server.dirty_before_bgsave = server.dirty;
  7.     start = ustime();
  8.     if ((childpid = fork()) == 0) {
  9.         /* Child */
  10.         if (server.vm_enabled) vmReopenSwapFile();
  11.         if (server.ipfd > 0) close(server.ipfd);
  12.         if (server.sofd > 0) close(server.sofd);
  13.         if (rdbSave(filename) == REDIS_OK) {
  14.             _exit(0);
  15.         } else {
  16.             _exit(1);
  17.         }
  18.     } else {
  19.         /* Parent */
  20.         server.stat_fork_time = ustime()-start;
  21.         if (childpid == -1) {
  22.             redisLog(REDIS_WARNING,"Can't save in background: fork: %s",
  23.                 strerror(errno));
  24.             return REDIS_ERR;
  25.         }
  26.         redisLog(REDIS_NOTICE,"Background saving started by pid %d",childpid);
  27.         server.bgsavechildpid = childpid;
  28.         updateDictResizePolicy();
  29.         return REDIS_OK;
  30.     }
  31.     return REDIS_OK; /* unreached */
  32. }
复制代码

使用道具 举报

回复
论坛徽章:
277
马上加薪
日期:2014-02-19 11:55:14马上有对象
日期:2014-02-19 11:55:14马上有钱
日期:2014-02-19 11:55:14马上有房
日期:2014-02-19 11:55:14马上有车
日期:2014-02-19 11:55:14马上有车
日期:2014-02-18 16:41:112014年新春福章
日期:2014-02-18 16:41:11版主9段
日期:2012-11-25 02:21:03ITPUB年度最佳版主
日期:2014-02-19 10:05:27现任管理团队成员
日期:2011-05-07 01:45:08
18#
 楼主| 发表于 2012-11-8 11:18 | 只看该作者
对是否已经有写rdb的子进程进行了判断,如果已经有保存快照的子进程,则返回错误。

如果启动了虚拟内存,则等待所有处理换出换入的任务线程退出,如果还有vm任务在处理就会一直循环等待。一直到所有换入换出任务都完成且所有vm线程退出。

保存当前的脏数据计数,当快照保存完后用于更新当前的脏数据计数(见函数backgroundSaveDoneHandler,rdb.c:1062)

记下当前时间,用于统计fork一个进程需要的时间

Fork一个字进程,子进程调用rdbSave进行快照保存

父进程统计fork一个子进程消耗的时间: server.stat_fork_time = ustime()-start,这个统计可以通过info命令获得。

保存子进程ID和更新增量重哈希的策略,即此时不应该再进行增量重哈希,不然大量key的改变可能导致fork的copy-on-write进行大量的写。

使用道具 举报

回复
论坛徽章:
277
马上加薪
日期:2014-02-19 11:55:14马上有对象
日期:2014-02-19 11:55:14马上有钱
日期:2014-02-19 11:55:14马上有房
日期:2014-02-19 11:55:14马上有车
日期:2014-02-19 11:55:14马上有车
日期:2014-02-18 16:41:112014年新春福章
日期:2014-02-18 16:41:11版主9段
日期:2012-11-25 02:21:03ITPUB年度最佳版主
日期:2014-02-19 10:05:27现任管理团队成员
日期:2011-05-07 01:45:08
19#
 楼主| 发表于 2012-11-8 11:18 | 只看该作者
到了这里我们知道,rdb的快照保存是通过函数rdbSave函数(rdb.c:394)来实现的。其实save命令也是通过调用这个函数来实现的。我们来简单看看

Db.c:323
  1. void saveCommand(redisClient *c) {
  2.     if (server.bgsavechildpid != -1) {
  3.         addReplyError(c,"Background save already in progress");
  4.         return;
  5.     }
  6.     if (rdbSave(server.dbfilename) == REDIS_OK) {
  7.         addReply(c,shared.ok);
  8.     } else {
  9.         addReply(c,shared.err);
  10.     }
复制代码

使用道具 举报

回复
论坛徽章:
277
马上加薪
日期:2014-02-19 11:55:14马上有对象
日期:2014-02-19 11:55:14马上有钱
日期:2014-02-19 11:55:14马上有房
日期:2014-02-19 11:55:14马上有车
日期:2014-02-19 11:55:14马上有车
日期:2014-02-18 16:41:112014年新春福章
日期:2014-02-18 16:41:11版主9段
日期:2012-11-25 02:21:03ITPUB年度最佳版主
日期:2014-02-19 10:05:27现任管理团队成员
日期:2011-05-07 01:45:08
20#
 楼主| 发表于 2012-11-9 09:06 | 只看该作者
最后我们进rdbSave函数看看

rdb.c:394
  1. int rdbSave(char *filename) {
  2.     ...
  3.     /* Wait for I/O therads to terminate, just in case this is a
  4.      * foreground-saving, to avoid seeking the swap file descriptor at the
  5.      * same time. */
  6.     if (server.vm_enabled)
  7.         waitEmptyIOJobsQueue();

  8.     snprintf(tmpfile,256,"temp-%d.rdb", (int) getpid());
  9.     fp = fopen(tmpfile,"w");
  10.     if (!fp) {
  11.         redisLog(REDIS_WARNING, "Failed saving the DB: %s", strerror(errno));
  12.         return REDIS_ERR;
  13.     }
  14.     if (fwrite("REDIS0002",9,1,fp) == 0) goto werr;
  15.     for (j = 0; j < server.dbnum; j++) {
  16.         redisDb *db = server.db+j;
  17.         dict *d = db->dict;
  18.         if (dictSize(d) == 0) continue;
  19.         di = dictGetSafeIterator(d);
  20.         if (!di) {
  21.             fclose(fp);
  22.             return REDIS_ERR;
  23.         }

  24.         /* Write the SELECT DB opcode */
  25.         if (rdbSaveType(fp,REDIS_SELECTDB) == -1) goto werr;
  26.         if (rdbSaveLen(fp,j) == -1) goto werr;

  27.         /* Iterate this DB writing every entry */
  28.         while((de = dictNext(di)) != NULL) {
  29.             sds keystr = dictGetEntryKey(de);
  30.             robj key, *o = dictGetEntryVal(de);
  31.             time_t expiretime;

  32.             initStaticStringObject(key,keystr);
  33.             expiretime = getExpire(db,&key);

  34.             /* Save the expire time */
  35.             if (expiretime != -1) {
  36.                 /* If this key is already expired skip it */
  37.                 if (expiretime < now) continue;
  38.                 if (rdbSaveType(fp,REDIS_EXPIRETIME) == -1) goto werr;
  39.                 if (rdbSaveTime(fp,expiretime) == -1) goto werr;
  40.             }
  41.             /* Save the key and associated value. This requires special
  42.              * handling if the value is swapped out. */
  43.             if (!server.vm_enabled || o->storage == REDIS_VM_MEMORY ||
  44.                                       o->storage == REDIS_VM_SWAPPING) {
  45.                 int otype = getObjectSaveType(o);

  46.                 /* Save type, key, value */
  47.                 if (rdbSaveType(fp,otype) == -1) goto werr;
  48.                 if (rdbSaveStringObject(fp,&key) == -1) goto werr;
  49.                 if (rdbSaveObject(fp,o) == -1) goto werr;
  50.             } else {
  51.                 /* REDIS_VM_SWAPPED or REDIS_VM_LOADING */
  52.                 robj *po;
  53.                 /* Get a preview of the object in memory */
  54.                 po = vmPreviewObject(o);
  55.                 /* Save type, key, value */
  56.                 if (rdbSaveType(fp,getObjectSaveType(po)) == -1)
  57.                     goto werr;
  58.                 if (rdbSaveStringObject(fp,&key) == -1) goto werr;
  59.                 if (rdbSaveObject(fp,po) == -1) goto werr;
  60.                 /* Remove the loaded object from memory */
  61.                 decrRefCount(po);
  62.             }
  63.         }
  64.         dictReleaseIterator(di);
  65.     }
  66.     /* EOF opcode */
  67.     if (rdbSaveType(fp,REDIS_EOF) == -1) goto werr;

  68.     /* Make sure data will not remain on the OS's output buffers */
  69.     fflush(fp);
  70.     fsync(fileno(fp));
  71.     fclose(fp);

  72.     /* Use RENAME to make sure the DB file is changed atomically only
  73.      * if the generate DB file is ok. */
  74.     if (rename(tmpfile,filename) == -1) {
  75.         redisLog(REDIS_WARNING,"Error moving temp DB file on the final destination: %s", strerror(errno));
  76.         unlink(tmpfile);
  77.         return REDIS_ERR;
  78.     }
  79.     redisLog(REDIS_NOTICE,"DB saved on disk");
  80.     server.dirty = 0;
  81.     server.lastsave = time(NULL);
  82.     return REDIS_OK;

  83. werr:
  84.     fclose(fp);
  85.     unlink(tmpfile);
  86.     redisLog(REDIS_WARNING,"Write error saving DB on disk: %s", strerror(errno));
  87.     if (di) dictReleaseIterator(di);
  88.     return REDIS_ERR;
  89. }
复制代码

使用道具 举报

回复

您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

TOP技术积分榜 社区积分榜 徽章 团队 统计 知识索引树 积分竞拍 文本模式 帮助
  ITPUB首页 | ITPUB论坛 | 数据库技术 | 企业信息化 | 开发技术 | 微软技术 | 软件工程与项目管理 | IBM技术园地 | 行业纵向讨论 | IT招聘 | IT文档
  ChinaUnix | ChinaUnix博客 | ChinaUnix论坛
CopyRight 1999-2011 itpub.net All Right Reserved. 北京盛拓优讯信息技术有限公司版权所有 联系我们 未成年人举报专区 
京ICP备16024965号-8  北京市公安局海淀分局网监中心备案编号:11010802021510 广播电视节目制作经营许可证:编号(京)字第1149号
  
快速回复 返回顶部 返回列表