(一)问题

在使用MySQL数据库binlog日志基于时间点恢复数据库时,我们必须要指定binlog的开始位置和结束位置,而在MongoDB里面,如果使用oplog进行恢复,只有oplogLimit参数,该参数信息如下

  1. --oplogLimit=<seconds>[:ordinal] only include oplog entries before the provided Timestamp

oplogLimit参数定义了数据库恢复到该时间点。也就是说,MongoDB只是设置了oplog的结束位置,没有指定oplog的开始位置。那么就存在问题了,以下图为例,我在T3时刻执行了全备份,在T4时刻数据库发生了误操作,当我执行恢复的时候,分为2个步骤:

  • 阶段1:使用之前的完全备份,将数据库恢复到T3时刻;
  • 阶段2:使用oplog日志,将数据库恢复到T4故障之前。T4故障之前的时间点由参数oplogLimit控制,但是:oplog的开始时间不是从T3时刻开始的,而是T2时刻开始的,这里T2是oplog记录的最早时间,该时间并不受我们控制

补充:这里的“不受我们控制”是指在使用mongorestore重做oplog的时候,我们没办法指定开始时间。但是如果想要把oplog的开始时间控制在T3时刻,还是有办法的:使用bsondump分析全备的最后一笔数据,在备份oplog的时候,用query选项过滤掉之前的数据即可。然而,这并不是我们关心的,我所关心的,是为什么mongorestore不指定开始时间。

说了那么多,把问题明确一下:

mongorestore在恢复oplog的时候,只限定了日志的结束位置,而没有开始位置,这样就会造成oplog恢复的开始位置不是T3,而是在T2,那么就会存在T2~T3这段时间数据重复操作的问题,理论上会造成数据变化,为什么mongorestore不设定一个开始时间参数去避免重复操作的问题呢?

(二)问题探索

(2.1)oplog日志格式解析

既然该问题可能会发生在重做oplog时,那么我们不妨先看一下oplog到底存储了什么信息。为了查看oplog日志保存了什么信息,向test集合中插入1条数据:

  1. db.test.insert({"empno":1,"ename":"lijiaman","age":22,"address":"yunnan,kungming"});

查看test集合的数据信息

  1. db.test.find()
  2. /* 1 */
  3. {
  4. "_id" : ObjectId("5f30eb58bcefe5270574cd54"),
  5. "empno" : 1.0,
  6. "ename" : "lijiaman",
  7. "age" : 22.0,
  8. "address" : "yunnan,kungming"
  9. }

使用下面查询语句查看oplog日志信息:

  1. use local db.oplog.rs.find( { $and : [ {"ns" : "testdb.test"} ] } ).sort({ts:1})

结果如下:

  1. /* 1 */
  2. {
  3. "ts" : Timestamp(1597070283, 1),
  4. "op" : "i",
  5. "ns" : "lijiamandb.test",
  6. "o" : {
  7. "_id" : ObjectId("5f30eb58bcefe5270574cd54"),
  8. "empno" : 1.0,
  9. "ename" : "lijiaman",
  10. "age" : 22.0,
  11. "address" : "yunnan,kungming"
  12. }
  13. }

oplog中各个字段的含义:

  • ts:数据写的时间,括号里面第1位数据代表时间戳,是自unix纪元以来的秒值,第2位代表在1s内订购时间戳的序列数
  • op:操作类型,可选参数有:

-- "i": insert

--"u": update

--"d": delete

--"c": db cmd

--"db":声明当前数据库 (其中ns 被设置成为=>数据库名称+ '.')

--"n": no op,即空操作,其会定期执行以确保时效性

  • ns:命名空间,通常是具体的集合
  • o:具体的写入信息
  • o2: 在执行更新操作时的where条件,仅限于update时才有该属性

(2.2)文档中的“_id”字段

在上面的插入文档中,我们发现每插入一个文档,都会伴随着产生一个“_id”字段,该字段是一个object类型,对于“_id”,需要知道:

  • "_id"是集合文档的主键,每个文档(即每行记录)都有一个唯一的"_id"值
  • "_id"会自动生成,也可以手动指定,但是必须唯一且非空

经过测试,发现在执行文档的DML操作时,会根据ID进行,我们不妨来看看DML操作的文档变化。

(1)插入文档,查看文档信息与oplog信息

  1. use testdb
  2.  
  3. //插入文档
  4. db.mycol.insert({id:1,name:"a"})
  5. db.mycol.insert({id:2,name:"b"})
  6. db.mycol.insert({id:3,name:"c"})
  7. db.mycol.insert({id:4,name:"d"})
  8. db.mycol.insert({id:5,name:"e"})
  9. db.mycol.insert({id:6,name:"f"})
  10.  
  11. rstest:PRIMARY> db.mycol.find()
  12. { "_id" : ObjectId("5f3b471a6530eb8aa5bf88a0"), "id" : 1, "name" : "a" }
  13. { "_id" : ObjectId("5f3b471a6530eb8aa5bf88a1"), "id" : 2, "name" : "b" }
  14. { "_id" : ObjectId("5f3b471a6530eb8aa5bf88a2"), "id" : 3, "name" : "c" }
  15. { "_id" : ObjectId("5f3b471a6530eb8aa5bf88a3"), "id" : 4, "name" : "d" }
  16. { "_id" : ObjectId("5f3b471a6530eb8aa5bf88a4"), "id" : 5, "name" : "e" }
  17. { "_id" : ObjectId("5f3b471b6530eb8aa5bf88a5"), "id" : 6, "name" : "f" }

这里记录该集合文档的变化,可以发现,mongodb为每条数据都分配了一个唯一且非空的”_id”:

此时查看oplog,如下

  1. /* 1 */
  2. {
  3. "ts" : Timestamp(1597720346, 2),
  4. "t" : NumberLong(11),
  5. "h" : NumberLong(0),
  6. "v" : 2,
  7. "op" : "i",
  8. "ns" : "testdb.mycol",
  9. "ui" : UUID("56c4e1ad-4a15-44ca-96c8-3b3b5be29616"),
  10. "wall" : ISODate("2020-08-18T03:12:26.231Z"),
  11. "o" : {
  12. "_id" : ObjectId("5f3b471a6530eb8aa5bf88a0"),
  13. "id" : 1.0,
  14. "name" : "a"
  15. }
  16. }
  17.  
  18. /* 2 */
  19. {
  20. "ts" : Timestamp(1597720346, 3),
  21. "t" : NumberLong(11),
  22. "h" : NumberLong(0),
  23. "v" : 2,
  24. "op" : "i",
  25. "ns" : "testdb.mycol",
  26. "ui" : UUID("56c4e1ad-4a15-44ca-96c8-3b3b5be29616"),
  27. "wall" : ISODate("2020-08-18T03:12:26.246Z"),
  28. "o" : {
  29. "_id" : ObjectId("5f3b471a6530eb8aa5bf88a1"),
  30. "id" : 2.0,
  31. "name" : "b"
  32. }
  33. }
  34.  
  35. ... ...

(2)更新操作

  1. rstest:PRIMARY> db.mycol.update({"id":1},{$set:{"name":"aa"}})
  2. WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })

这里更新了1行数据,可以看到,文档id是没有发生变化的

此时查看oplog,如下:

  1. /* 7 */
  2. {
  3. "ts" : Timestamp(1597720412, 1),
  4. "t" : NumberLong(11),
  5. "h" : NumberLong(0),
  6. "v" : 2,
  7. "op" : "u",
  8. "ns" : "testdb.mycol",
  9. "ui" : UUID("56c4e1ad-4a15-44ca-96c8-3b3b5be29616"),
  10. "o2" : {
  11. "_id" : ObjectId("5f3b471a6530eb8aa5bf88a0")
  12. },
  13. "wall" : ISODate("2020-08-18T03:13:32.649Z"),
  14. "o" : {
  15. "$v" : 1,
  16. "$set" : {
  17. "name" : "aa"
  18. }
  19. }
  20. }

这里值得我们注意:上面我们说到,oplog的”o2”参数是更新的where条件,我们在执行更新的时候,指定的where条件是”id=1”,id是我们自己定义的列,然而,在oplog里面指定的where条件是

"_id" : ObjectId("5f3b471a6530eb8aa5bf88a0"),很明显,他们都指向了同一条数据。这样,当我们使用oplog进行数据恢复的时候,直接根据”_id”去做数据更新,即使再执行N遍,也不会导致数据更新出错。

(3)再次更新操作

上面我们是对某一条数据进行更新,并且在update中指出了更新后的数据,这里再测试一下,我使用自增的方式更新数据。每条数据的id在当前的基础上加10

  1. rstest:PRIMARY> db.mycol.update({},{$inc:{"id":10}},{multi:true})
  2. WriteResult({ "nMatched" : 6, "nUpserted" : 0, "nModified" : 6 })

数据变化如图,可以看到,id虽然发生了变化,但是”_id”是没有改变的。

再来看oplog信息

  1. /* 8 */
  2. {
  3. "ts" : Timestamp(1597720424, 1),
  4. "t" : NumberLong(11),
  5. "h" : NumberLong(0),
  6. "v" : 2,
  7. "op" : "u",
  8. "ns" : "testdb.mycol",
  9. "ui" : UUID("56c4e1ad-4a15-44ca-96c8-3b3b5be29616"),
  10. "o2" : {
  11. "_id" : ObjectId("5f3b471a6530eb8aa5bf88a0")
  12. },
  13. "wall" : ISODate("2020-08-18T03:13:44.398Z"),
  14. "o" : {
  15. "$v" : 1,
  16. "$set" : {
  17. "id" : 11.0
  18. }
  19. }
  20. }
  21.  
  22. /* 9 */
  23. {
  24. "ts" : Timestamp(1597720424, 2),
  25. "t" : NumberLong(11),
  26. "h" : NumberLong(0),
  27. "v" : 2,
  28. "op" : "u",
  29. "ns" : "testdb.mycol",
  30. "ui" : UUID("56c4e1ad-4a15-44ca-96c8-3b3b5be29616"),
  31. "o2" : {
  32. "_id" : ObjectId("5f3b471a6530eb8aa5bf88a1")
  33. },
  34. "wall" : ISODate("2020-08-18T03:13:44.399Z"),
  35. "o" : {
  36. "$v" : 1,
  37. "$set" : {
  38. "id" : 12.0
  39. }
  40. }
  41. }
  42.  
  43. /* 10 */
  44. {
  45. "ts" : Timestamp(1597720424, 3),
  46. "t" : NumberLong(11),
  47. "h" : NumberLong(0),
  48. "v" : 2,
  49. "op" : "u",
  50. "ns" : "testdb.mycol",
  51. "ui" : UUID("56c4e1ad-4a15-44ca-96c8-3b3b5be29616"),
  52. "o2" : {
  53. "_id" : ObjectId("5f3b471a6530eb8aa5bf88a2")
  54. },
  55. "wall" : ISODate("2020-08-18T03:13:44.399Z"),
  56. "o" : {
  57. "$v" : 1,
  58. "$set" : {
  59. "id" : 13.0
  60. }
  61. }
  62. }
  63.  
  64. /* 11 */
  65. {
  66. "ts" : Timestamp(1597720424, 4),
  67. "t" : NumberLong(11),
  68. "h" : NumberLong(0),
  69. "v" : 2,
  70. "op" : "u",
  71. "ns" : "testdb.mycol",
  72. "ui" : UUID("56c4e1ad-4a15-44ca-96c8-3b3b5be29616"),
  73. "o2" : {
  74. "_id" : ObjectId("5f3b471a6530eb8aa5bf88a3")
  75. },
  76. "wall" : ISODate("2020-08-18T03:13:44.400Z"),
  77. "o" : {
  78. "$v" : 1,
  79. "$set" : {
  80. "id" : 14.0
  81. }
  82. }
  83. }
  84.  
  85. /* 12 */
  86. {
  87. "ts" : Timestamp(1597720424, 5),
  88. "t" : NumberLong(11),
  89. "h" : NumberLong(0),
  90. "v" : 2,
  91. "op" : "u",
  92. "ns" : "testdb.mycol",
  93. "ui" : UUID("56c4e1ad-4a15-44ca-96c8-3b3b5be29616"),
  94. "o2" : {
  95. "_id" : ObjectId("5f3b471a6530eb8aa5bf88a4")
  96. },
  97. "wall" : ISODate("2020-08-18T03:13:44.400Z"),
  98. "o" : {
  99. "$v" : 1,
  100. "$set" : {
  101. "id" : 15.0
  102. }
  103. }
  104. }
  105.  
  106. /* 13 */
  107. {
  108. "ts" : Timestamp(1597720424, 6),
  109. "t" : NumberLong(11),
  110. "h" : NumberLong(0),
  111. "v" : 2,
  112. "op" : "u",
  113. "ns" : "testdb.mycol",
  114. "ui" : UUID("56c4e1ad-4a15-44ca-96c8-3b3b5be29616"),
  115. "o2" : {
  116. "_id" : ObjectId("5f3b471b6530eb8aa5bf88a5")
  117. },
  118. "wall" : ISODate("2020-08-18T03:13:44.400Z"),
  119. "o" : {
  120. "$v" : 1,
  121. "$set" : {
  122. "id" : 16.0
  123. }
  124. }
  125. }

这里也值得我们注意:o2记录的是已经发生更改的文档_id,o就比较有意思了,记录的是发生变更之后的值。我们可以发现,如果我们把上面自增更新的SQL执行每执行1次,id都会加10,但是,我们重复执行N次oplog,并不会改变该条记录的值。

(4)再来看看删除操作

  1. rstest:PRIMARY> db.mycol.remove({"id":{"$gt":14}})
  2. WriteResult({ "nRemoved" : 2 })

数据变化如下图:

再来看看oplog日志:

  1. /* 14 */
  2. {
  3. "ts" : Timestamp(1597720485, 1),
  4. "t" : NumberLong(11),
  5. "h" : NumberLong(0),
  6. "v" : 2,
  7. "op" : "d",
  8. "ns" : "testdb.mycol",
  9. "ui" : UUID("56c4e1ad-4a15-44ca-96c8-3b3b5be29616"),
  10. "wall" : ISODate("2020-08-18T03:14:45.511Z"),
  11. "o" : {
  12. "_id" : ObjectId("5f3b471a6530eb8aa5bf88a4")
  13. }
  14. }
  15.  
  16. /* 15 */
  17. {
  18. "ts" : Timestamp(1597720485, 2),
  19. "t" : NumberLong(11),
  20. "h" : NumberLong(0),
  21. "v" : 2,
  22. "op" : "d",
  23. "ns" : "testdb.mycol",
  24. "ui" : UUID("56c4e1ad-4a15-44ca-96c8-3b3b5be29616"),
  25. "wall" : ISODate("2020-08-18T03:14:45.511Z"),
  26. "o" : {
  27. "_id" : ObjectId("5f3b471b6530eb8aa5bf88a5")
  28. }
  29. }

”op”:”d”选项记录了该操作是执行删除,具体删除什么数据,由o选项记录,可以看到,记录的是”_id”,可以看到,删除操作是根据”_id”执行的。

(三)结论

可以看到,在DML操作数据库时,oplog时基于"_id"记录文档变化的。那么,我们来总结一下开头提出的问题:未指定开始时间,oplog数据是否会重复操作呢?

  • 如果当前数据库已经存在相同id的数据,那么不会执行二次插入,主键冲突报错;
  • 在做更新时,记录的是更新文档的"_id"以及发生变更后的数据,因此,如果再次执行,只会修改该条数据,哪怕执行N遍,效果也和执行一遍是一样的,所有也就不怕重复操作单条数据了;
  • 在执行删除操作时,记录的是删除的文档"_id",同样,执行N遍和执行一遍效果是一样的,因为”_id”是唯一的。

因此,即使oplog从完全备份之前开始应用,也不会造成数据的多次变更。

【完】

相关文档:

1.MongoDB 2.7主从复制(master –> slave)环境基于时间点的恢复  
2.MongoDB 4.2副本集环境基于时间点的恢复

3.MongoDB恢复探究:为什么oplogReplay参数只设置了日志应用结束时间oplogLimit,而没有设置开始时间?

3.MongoDB恢复探究:为什么oplogReplay参数只设置了日志应用结束时间oplogLimit,而没有设置开始时间?的更多相关文章

  1. mongodb write 【摘自网上,只为记录,学习】

    mongodb有一个write concern的设置,作用是保障write operation的可靠性.一般是在client driver里设置的,和db.getLastError()方法关系很大 一 ...

  2. Python装饰器探究——装饰器参数

    Table of Contents 1. 探究装饰器参数 1.1. 编写传参的装饰器 1.2. 理解传参的装饰器 1.3. 传参和不传参的兼容 2. 参考资料 探究装饰器参数 编写传参的装饰器 通常我 ...

  3. Oracle恢复ORA-00600: 内部错误代码, 参数: [kcratr_scan_lastbwr] 问题的简单解决

    Oracle恢复ORA-00600: 内部错误代码, 参数: [kcratr_scan_lastbwr] 1. 简单处理 sqlplus / as sysdba startup mount recov ...

  4. 居然还有WM_TIMECHANGE(只在用户手动改变系统时间时才会产生作用)

    unit Unit1; interface uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms ...

  5. mongoDB之监控工具mongostat及其参数的具体含义

    mongostat是mongdb自带的状态检测工具,在命令行下使用.它会间隔固定时间获取mongodb的当前运行状态,并输出.如果你发现数据库突然变慢或者有其他问题的话,你第一手的操作就考虑采用mon ...

  6. Spring data mongodb @CreatedBy@LastModifiedBy@CreatedBy@LastModifiedBy SpringSecurityAuditorAware,只记录用户名

    要在Spring data mongodb 中使用@CreatedBy@LastModifiedBy@CreatedBy@LastModifiedBy  这四个注解 必须实现 SpringSecuri ...

  7. Tomcat参数调优包括日志、线程数、内存【转】

    [Tomcat中日志打印对性能测试的影响] 一般都提供了这样5个日志级别: ▪ Debug ▪ Info ▪ Warn ▪ Error ▪ Fatal 由于性能测试需要并发进行压力测试,如果日志级别是 ...

  8. log4j2用Log4jContextSelector启动参数配置全局异步日志是如何使用disruptor

    与 log4j2用asyncRoot配置异步日志是如何使用disruptor差异有几个: 给disruptor实例的EventFactory不同 此处EventFactory采用的是RingBuffe ...

  9. log4j配置参数详解——按日志文件大小、日期切分日志文件

    项目中尽管对log4j有基本的配置,例如按天生成日志文件以作区分,但如果系统日志文件过大,则就需要考虑以更小的单位切分或者其他切分方式.下面就总结一下log4j常用的配置参数以及切分日志的不同方式. ...

随机推荐

  1. B站学习的回顾总结

    视频地址 https://www.bilibili.com/video/av50680998/ 1.MVC 和MVVM有什么区别? MVC 是后端开发的概念: Model   view  contro ...

  2. 网页批量打印成PDF,并按条件合并成大PDF、生成页码

    题记:因为老板要求将过去一年内系统中的订单合同内容进行打印,并按月进行整理成纸质文件.合同在系统(web系统)中以html形式显示,打印单份都是在网页中右键打印,订单量上千份,每笔订单有两份合同,如果 ...

  3. ken桑带你读源码 之scrapy scrapy\core\scheduler.py

    从英文来看是调度程序  我们看看是怎么调度 首先爬虫队列有两个 一个是保存在内存中  没有历史记录   重新开始  42行  self.mqs = self.pqclass(self._newmq) ...

  4. 移动端宽高适配JS

    //定义全局变量 var winWidth = 0; /*窗口宽度*/ var winHeight = 0; /*窗口高度*/ //函数区 //实时获取浏览器窗口大小,当窗口大小变化开始相应操作 fu ...

  5. Django学习路23_if else 语句,if elif else 语句 forloop.first第一个元素 .last最后一个元素,注释

    if else 格式 {% if 条件 %} <标签>语句</标签> {%else%} <标签>语句</标签> {%endif} 标签都可以添加样式 { ...

  6. 完了!TCP出了大事!

    前情回顾:<非中间人就不能劫持TCP了吗?> 不速之客 夜黑风高,乌云蔽月. 两位不速之客,身着黑衣,一高一矮,潜入Linux帝国. 这一潜就是一个多月,直到他们收到了一条消息······ ...

  7. 统计一个16位二进制数中1的个数,并将结果以十六进制形式显示在屏幕上,用COM格式实现。

    问题 统计一个16位二进制数中1的个数,并将结果以十六进制形式显示在屏幕上,用COM格式实现. 代码 code segment assume cs:code org 100h main proc ne ...

  8. Python os.isatty() 方法

    概述 os.isatty() 方法用于判断如果文件描述符fd是打开的,同时与tty(-like)设备相连,则返回true, 否则False.高佣联盟 www.cgewang.com 语法 isatty ...

  9. PHP arsort() 函数

    ------------恢复内容开始------------ 实例 对关联数组按照键值进行降序排序: <?php$age=array("Peter"=>"35 ...

  10. python数据处理PDF高清电子书

    点击获取提取码:jzgv 内容简介 本书采用基于项目的方法,介绍用Python完成数据获取.数据清洗.数据探索.数据呈现.数据规模化和自动化的过程.主要内容包括:Python基础知识,如何从CSV.E ...