123
返回列表 发新帖
楼主: 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
21#
 楼主| 发表于 2012-11-9 09:06 | 只看该作者
对是否有vm线程进行再次判断,因为如果是通过save命令过来的是没有判断过vm线程的。

创建并打开临时文件

写入文件签名“REDIS”和版本号“0002”

遍历所有db中的所有key

对每个key,先判断是否设置了expireTime, 如果设置了,则保存expireTime到rdb文件中。然后判断该key对应的value是否则内存中,如果是在内存中,则取出来写入到rdb文件中保存,如果被换出到虚拟内存了,则从虚拟内存读取然后写入到rdb文件中。

不同类型有有不同的存储格式,详细见rdb文件格式

最后写入rdb文件的结束符

关闭文件并重命名临时文件名到正式文件名

更新脏数据计数server.dirty为0和最近写rdb文件的时间server.lastsave为当前时间,这个只是在通过save命令触发的情况下有用。因为如果是通过fork一个子进程来写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
22#
 楼主| 发表于 2012-11-9 09:07 | 只看该作者
如果是通过fork一个子进程来写rdb文件(即不是通过save命令触发的),在写rdb文件的过程中,可能又有一些数据被更改了,那此时的脏数据计数server.dirty怎么更新呢? redis是怎样处理的呢?
我们来看看写rdb的子进程推出时得处理

Redis.c:605
  1. if (server.bgsavechildpid != -1 || server.bgrewritechildpid != -1) {
  2.         int statloc;
  3.         pid_t pid;

  4.         if ((pid = wait3(&statloc,WNOHANG,NULL)) != 0) {
  5.             if (pid == server.bgsavechildpid) {
  6.                 backgroundSaveDoneHandler(statloc);
  7.             } else {
  8.                 backgroundRewriteDoneHandler(statloc);
  9.             }
  10.             updateDictResizePolicy();
  11.         }
  12. }
复制代码

使用道具 举报

回复
论坛徽章:
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
23#
 楼主| 发表于 2012-11-9 09:07 | 只看该作者
如果捕捉到写rdb文件的子进程退出,则调用backgroundSaveDoneHandler进行处理

接着看看backgroundSaveDoneHandler函数

Rdb.c:1062
  1. void backgroundSaveDoneHandler(int statloc) {
  2.     int exitcode = WEXITSTATUS(statloc);
  3.     int bysignal = WIFSIGNALED(statloc);

  4.     if (!bysignal && exitcode == 0) {
  5.         redisLog(REDIS_NOTICE,
  6.             "Background saving terminated with success");
  7.         server.dirty = server.dirty - server.dirty_before_bgsave;
  8.         server.lastsave = time(NULL);
  9.     } else if (!bysignal && exitcode != 0) {
  10.         redisLog(REDIS_WARNING, "Background saving error");
  11.     } else {
  12.         redisLog(REDIS_WARNING,
  13.             "Background saving terminated by signal %d", WTERMSIG(statloc));
  14.         rdbRemoveTempFile(server.bgsavechildpid);
  15.     }
  16.     server.bgsavechildpid = -1;
  17.     /* Possibly there are slaves waiting for a BGSAVE in order to be served
  18.      * (the first stage of SYNC is a bulk transfer of dump.rdb) */
  19.     updateSlavesWaitingBgsave(exitcode == 0 ? REDIS_OK : REDIS_ERR);
  20. }
复制代码

使用道具 举报

回复
论坛徽章:
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
24#
 楼主| 发表于 2012-11-9 09:08 | 只看该作者
更新脏数据计数server.dirty为0和最近写rdb文件的时间server.lastsave为当前时间

唤醒因为正在保存快照而等待的slave,关于slave的具体内容,见replication

快照导入

当redis因为停电或者某些原因挂掉了,此时重启redis时,我们就需要从rdb文件中读取快照文件,把保存到rdb文件中的数据重新导入到内存中。

先来看看启动时对快照导入的处理

Redis.c:1717
  1.     if (server.appendonly) {
  2.         if (loadAppendOnlyFile(server.appendfilename) == REDIS_OK)
  3.             redisLog(REDIS_NOTICE,"DB loaded from append only file: %ld seconds",time(NULL)-start);
  4.     } else {
  5.         if (rdbLoad(server.dbfilename) == REDIS_OK) {
  6.             redisLog(REDIS_NOTICE,"DB loaded from disk: %ld seconds",
  7.                 time(NULL)-start);
  8.         } else if (errno != ENOENT) {
  9.             redisLog(REDIS_WARNING,"Fatal error loading the DB. Exiting.");
  10.             exit(1);
  11.         }
  12.     }
复制代码

使用道具 举报

回复
论坛徽章:
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
25#
 楼主| 发表于 2012-11-10 12:45 | 只看该作者
如果保存了AOF文件,则使用AOF文件来恢复数据,AOF的具体内容见AOF

如果没有AOF,则使用rdb文件恢复数据,调用rdbLoad函数

接着看看rdbLoad函数

Rdb.c:929
  1. int rdbLoad(char *filename) {
  2.     ...
  3.     fp = fopen(filename,"r");
  4.     if (!fp) {
  5.         errno = ENOENT;
  6.         return REDIS_ERR;
  7.     }
  8.     if (fread(buf,9,1,fp) == 0) goto eoferr;
  9.     buf[9] = '\0';
  10.     if (memcmp(buf,"REDIS",5) != 0) {
  11.         fclose(fp);
  12.         redisLog(REDIS_WARNING,"Wrong signature trying to load DB from file");
  13.         errno = EINVAL;
  14.         return REDIS_ERR;
  15.     }
  16.     rdbver = atoi(buf+5);
  17.     if (rdbver < 1 || rdbver > 2) {
  18.         fclose(fp);
  19.         redisLog(REDIS_WARNING,"Can't handle RDB format version %d",rdbver);
  20.         errno = EINVAL;
  21.         return REDIS_ERR;
  22.     }

  23.     startLoading(fp);
  24.     while(1) {
  25.         robj *key, *val;
  26.         int force_swapout;

  27.         expiretime = -1;

  28.         /* Serve the clients from time to time */
  29.         if (!(loops++ % 1000)) {
  30.             loadingProgress(ftello(fp));
  31.             aeProcessEvents(server.el, AE_FILE_EVENTS|AE_DONT_WAIT);
  32.         }

  33.         /* Read type. */
  34.         if ((type = rdbLoadType(fp)) == -1) goto eoferr;
  35.         if (type == REDIS_EXPIRETIME) {
  36.             if ((expiretime = rdbLoadTime(fp)) == -1) goto eoferr;
  37.             /* We read the time so we need to read the object type again */
  38.             if ((type = rdbLoadType(fp)) == -1) goto eoferr;
  39.         }
  40.         if (type == REDIS_EOF) break;
  41.         /* Handle SELECT DB opcode as a special case */
  42.         if (type == REDIS_SELECTDB) {
  43.             if ((dbid = rdbLoadLen(fp,NULL)) == REDIS_RDB_LENERR)
  44.                 goto eoferr;
  45.             if (dbid >= (unsigned)server.dbnum) {
  46.                 redisLog(REDIS_WARNING,"FATAL: Data file was created with a Redis server configured to handle more than %d databases. Exiting\n", server.dbnum);
  47.                 exit(1);
  48.             }
  49.             db = server.db+dbid;
  50.             continue;
  51.         }
  52.         /* Read key */
  53.         if ((key = rdbLoadStringObject(fp)) == NULL) goto eoferr;
  54.         /* Read value */
  55.         if ((val = rdbLoadObject(type,fp)) == NULL) goto eoferr;
  56.         /* Check if the key already expired. This function is used when loading
  57.          * an RDB file from disk, either at startup, or when an RDB was
  58.          * received from the master. In the latter case, the master is
  59.          * responsible for key expiry. If we would expire keys here, the
  60.          * snapshot taken by the master may not be reflected on the slave. */
  61.         if (server.masterhost == NULL && expiretime != -1 && expiretime < now) {
  62.             decrRefCount(key);
  63.             decrRefCount(val);
  64.             continue;
  65.         }
  66.         /* Add the new object in the hash table */
  67.         dbAdd(db,key,val);

  68.         /* Set the expire time if needed */
  69.         if (expiretime != -1) setExpire(db,key,expiretime);

  70.         /* Handle swapping while loading big datasets when VM is on */

  71.         /* If we detecter we are hopeless about fitting something in memory
  72.          * we just swap every new key on disk. Directly…
  73.          * Note that's important to check for this condition before resorting
  74.          * to random sampling, otherwise we may try to swap already
  75.          * swapped keys. */
  76.         if (swap_all_values) {
  77.             dictEntry *de = dictFind(db->dict,key->ptr);

  78.             /* de may be NULL since the key already expired */
  79.             if (de) {
  80.                 vmpointer *vp;
  81.                 val = dictGetEntryVal(de);

  82.                 if (val->refcount == 1 &&
  83.                     (vp = vmSwapObjectBlocking(val)) != NULL)
  84.                     dictGetEntryVal(de) = vp;
  85.             }
  86.             decrRefCount(key);
  87.             continue;
  88.         }
  89.         decrRefCount(key);

  90.         /* Flush data on disk once 32 MB of additional RAM are used… */
  91.         force_swapout = 0;
  92.         if ((zmalloc_used_memory() - server.vm_max_memory) > 1024*1024*32)
  93.             force_swapout = 1;

  94.         /* If we have still some hope of having some value fitting memory
  95.          * then we try random sampling. */
  96.         if (!swap_all_values && server.vm_enabled && force_swapout) {
  97.             while (zmalloc_used_memory() > server.vm_max_memory) {
  98.                 if (vmSwapOneObjectBlocking() == REDIS_ERR) break;
  99.             }
  100.             if (zmalloc_used_memory() > server.vm_max_memory)
  101.                 swap_all_values = 1; /* We are already using too much mem */
  102.         }
  103.     }
  104.     fclose(fp);
  105.     stopLoading();
  106.     return REDIS_OK;

  107. eoferr: /* unexpected end of file is handled here with a fatal exit */
  108.     redisLog(REDIS_WARNING,"Short read or OOM loading DB. Unrecoverable error, aborting now.");
  109.     exit(1);
  110.     return REDIS_ERR; /* Just to avoid warning */
  111. }
复制代码

使用道具 举报

回复
论坛徽章:
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
26#
 楼主| 发表于 2012-11-10 12:46 | 只看该作者
打开rdb文件

读取rdb文件的签名和版本号

开始进入 类型 | 值 | 类型 | 值 的循环读取,可参考rdb文件格式

作者还做了导入的进度条,是有人反馈说rdb文件很大时导入时要很久,但又不知道进度,所以作者就加了导入的进度条,改善用户体验

读取类型

如果类型是过期时间类型REDIS_EXPIRETIME,则读取过期时间

如果类型是文件结束类型REDIS_EOF,则跳出 类型 | 值 | 类型 | 值 的循环读取

如果类型是选择db类型REDIS_SELECTDB,则读取db索引并把当前db转成该db,然后继续 类型 | 值 | 类型 | 值 的循环读取。

如果不是以上类型,则表明该类型是数据类型,读取作为key的字符串,即读取字符串类型的值,然后接着读取作为value的字符串。不同类型的编码不一样,根据写入时得规则解释读取到的值即可

读取到key和value后,判断下该key是否过期,如果过期则丢弃,不再导入,然后继续 类型 | 值 | 类型 | 值 的循环读取。

如果读取成功,则导入到内存,如果有过期时间则设置过期时间

如果配置了虚拟内存并且内存的使用比虚拟内存配置的大32M时,开始随机的取一些数据换出到虚拟内存中。

从上边我们也可以看到,如果没有配置虚拟内存,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
27#
 楼主| 发表于 2012-11-10 12:46 | 只看该作者
总结

落地存储是数据设计的一大重点也是难点。原理很简单,定义某种协议,然后按照某种协议写入读出。Redis为了节省空间和读写时的I/O操作,做了很多很细致的工作来压缩数据。另外redis的丰富的数据类型也加大了落地的实现难度。作者也曾经在他的博客说过,redis的丰富的数据类型导致了很多经典的优化办法无法在redis上实现。

使用道具 举报

回复
论坛徽章:
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
28#
 楼主| 发表于 2012-11-10 12:46 | 只看该作者
over.

使用道具 举报

回复

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

本版积分规则 发表回复

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