MongoDB

280 阅读20分钟

新增

如果文档不存在就创建文档,如果文档中不存在_id字段,则会自动给_id赋值为ObjectId类型的值。

insertOne

db.person.insertOne({"name":"pingwazi","age":18})

返回插入文档的ID

insertMany

db.person.insertMany([{"name":"pingwazi","age":18},{"name":"xiaoping","age":18}])

按顺序返回插入文档的ID

删除

deleteMany

db.person.deleteMany({})

deleteOne

db.person.deleteOne({})

修改

替换文档

db.person.replaceOne({name:"ping"},{"name":"pingwazi","age":1})

update

默认情况下修改一个文档,如果需要修改多个文档需要指定multi值为true。通常通常使用updateOne、updateMany来代替update

db.person.update({name:"ping"},{$set:{name:"pingwazi"}})

$set

更新文档中某个字段,如果

$unset

删除文档中某个字段

db.person.update({"name":"ping"},{$unset:{name:1}})

$inc

对数字类型字段进行加减操作

db.person.update({"_id":ObjectId("62792a7d57f6790bf720d07b")},{$inc:{age:-1}})

updateOne

修改一个,即使筛选条件匹配多个也只修改一个

db.person.updateOne({},{$set:{name:"ping"}})

updateMany

修改多个,筛选条件匹配多个文档就修改多个文档

db.person.updateMany({},{$set:{name:"ping"}})

修改数组

$push

在数组尾部追加一个元素,如果字段不存在就新增一个字段

db.person.update({"_id":ObjectId("62792a7d57f6790bf720d07b")},{$push:{nums:1}})

也可以使用$each一次性往数组中添加多个值

db.person.update({"_id":ObjectId("62792a7d57f6790bf720d07b")},{$push:{nums:{$each:[1,2,3,4,5]}}})

each可以和each可以和slice、sort配合使用,sort配合使用,slice可以限制数组中最多的元素个数,sort可以对添加的元素进行排序,如果三者结合使用会先使用sort可以对添加的元素进行排序,如果三者结合使用会先使用sort对数组元素排序,然后再使用slice对数组进行截取。slice对数组进行截取。 slice的值为正数,则从头开始取指定数量。为负数则从尾开始取

//对数组元素升序排列,然后保留最后7个元素
db.person.update({"_id":ObjectId("62792a7d57f6790bf720d07b")},{$push:{nums:{$each:[1,2,34,0,9,8,7],$slice:-7,$sort:1}}})

对对象进行排序的用例

db.person.update({"_id":ObjectId("62792a7d57f6790bf720d07b")},{$push:{nums:{$each:[{"index":1,"value":"1"},{"index":2,"value":"2"}],$slice:-7,$sort:{index:1}}}})

$addToSet

保证添加的元素在数组中不存在

db.person.update({"_id":ObjectId("62792a7d57f6790bf720d07b")},{$addToSet:{nums:4}})

当然也可以和eacheach、slice、$sort配合使用,用法同理

$pop

删除数组头部或者尾部的一个元素(1:从尾部删除,-1:从头部删除)

db.person.update({"_id":ObjectId("62792a7d57f6790bf720d07b")},{$pop:{nums:1}})

$pull

删除所有满足条件的元素,如下面删除值为9的元素

db.person.update({"_id":ObjectId("62792a7d57f6790bf720d07b")},{$pull:{nums:9}})

如果元素是对象,可用下面的方式

db.person.update({"_id":ObjectId("62792a7d57f6790bf720d07b")},{$pull:{nums:{index:0}}})

对数组指定位置进行操作

这里分两种情况,已知位置和未知位置 如果是前者的话就直接用元素下标即可,如果是后者,则可以使用$代替,但是这样就只能处理一个

已知位置

//修改第3个元素的index字段值,第3个元素是对象
db.person.update({"_id":ObjectId("62792a7d57f6790bf720d07b")},{$inc:{"nums.3.index":1}})
//给第2个元素的值加1,第2个元素是一个数字
db.person.update({"_id":ObjectId("62792a7d57f6790bf720d07b")},{$inc:{"nums.2":1}})

未知位置 这种更新需要在更新过滤条件中含有数组的筛选条件才行,并且如果过滤出的数组元素不止一个,则使用这种对数组元素的修改也只会修改第一个

//nums数组中有两个值为35的,但是这条语句只会修改第一个值为35的
db.person.update({"_id":ObjectId("62792a7d57f6790bf720d07b"),nums:35},{$inc:{"nums.$":1}})

修改数组字段中所有元素

//修改nums数组中的所有元素
db.user.updateMany({_id:ObjectId("62823cf127536bc4b52be68c")},{$set:{"nums.$[]":1}})

arrayFilters

在3.3.6版本新增的一个对数组元素操作,只要满足筛选条件就都可以进行修改

//数组元素是{"index":1,"value":"1"}的格式,下面的语句是找到元素的value值为1的,然后将加1
db.person.update({"_id":ObjectId("62792a7d57f6790bf720d07b")},{$inc:{"nums.$[elem].index":1}},{arrayFilters:[{"elem.value":"1"}]})

upsert

如果更新的文档不存在就以条件+更新文档为基础创建一个文档(存在就更新,不存在就插入)

db.person.updateOne({name:"not found"},{$set:{age:18}},{upsert:true})

$setOnInsert

是一个操作符,仅当更新的文档不存在进行插入式执行,存在的时候更新不会有任何效果

db.person.updateOne({name:"not found1"},{$set:{age:19},$setOnInsert:{addr:"四川成都"}},{upsert:true})

返回更新的文档

findOneAndUpdate、findOneAndReplace、findOneAndDelete用于原子操作,并可以通过指定returnNewDocument参数来确定是否返回操作后的数据(returnNewDocument对findOneAndDelete无效)

//查找name=pingwazi的数据,然后根据_id排序,再将age修改为19并返回修改后的文档(没有指定returnNewDocument值则返回修改前的文档)
db.person.findOneAndUpdate({name:"pingwazi"},{$set:{age:19}},{sort:{_id:-1},returnNewDocument:true})

查询

比较运算符查询

//$lt、$lte、$gt、$gte
db.person.findOne({age:{$lte:20,$gt:10}})

存在和不存在

inin、or、$not

//name是pingwazi/xiaoping
db.person.findOne({name:{$in:["pingwazi","xiaoping"]}})
//name是pingwazi 或者age=18
db.person.findOne({$or:[{name:"pingwazi"},{age:18}]})
//名字不是xiaoping、pingwazi
db.person.findOne({name:{$not:{$in:["xiaoping","pingwazi"]}}})

正则匹配

支持以下三种语法格式

db.person.findOne({name:{$regex:/Ping/i}})
db.person.findOne({name:{$regex:/ping/,$options:"i"}})
db.person.findOne({name:{$regex:"Ping",$options:"i"}})

options支持以下选项

描述限制
i不区分大小写
m当正则中有^和$的边界符,则可以一行一行的匹配
x忽略怎则表达式中所有空白字符(换行符、制表符、空格),并将"#"认为是正则表达式注释的开头,就是允许我们在正则表达式中添加注释(#需要和\n结合使用,两者结合才能终止注释)需要和$options结合使用
s允许"."匹配包括换行符在内的所有字符需要和$options结合使用
//选项x的使用示例 不区分大小写的方式匹配name包含pingwazi
db.person.findOne({name:{$regex:'ping #name\n wazi #name\n',$options:"xi"}})

查询数组

//查询数组中存在一个元素等于某个值
db.person.find({nums:36})
//查询数组中存在一个元素在条件数组中
db.person.find({nums:{$in:[36,23]}})
//查询条件数组中的元素在目标数组中都存在的文档
db.person.find({nums:{$all:[36,35]}})

指定数组返回的数量

//返回数组后面两条数据 正数的就是前面的
db.person.find({},{nums:{$slice:-2}})
//跳过前面两个再取两个
db.person.find({},{nums:{$slice:[2,2]}})

返回数组匹配元素在指定位置的数据

db.person.find({nums:{$in:[35,36]}},{"nums.$":1})

$elemMatch

数组元素依次与查询条件进行匹配,所有元素全都匹配才会返回

//数组元素是单个值的匹配条件
db.person.find({nums:{$elemMatch:{$gte:2,$lte:50}}})
//数组元素是一个对象的匹配条件
db.person.find({nums:{$elemMatch:{index:{$gte:2,$lte:10}}}})

索引

对于常用的查询,建立索引可以提高查询效率,但索引也需要占用存储空间,并且在更新、插入文档时都需要更新索引,这也是一个开销。 当返回数据占文档总数据的百分比较大时,也不会使用索引,而是直接全表扫描,因为使用索引会进行两次查询,而全表扫描只进行一次,效率要高一些。

索引操作

//创建索引
db.person.createIndex({name:1})//单值索引
db.person.createIndex({name:1,age:1})//复合索引
db.person.createIndex({"name":1},{"unique":true})//唯一索引 如果部分文档中索引字段不存在,则这样创建的唯一索引就会报错,因为不存在的字段值认为是null,两个不存在的字段就会认为是重复值了。这个个使用需要使用部分索引的方式
//获取索引列表
db.person.getIndexes()
//删除索引
db.person.dropIndex({"name":1})
//删除全部索引(慎用)
db.person.dropIndexes()

部分索引

db.person.createIndex({name:1},{unique:true,partialFilterExpression:{name:{$exists:true}}})

所有TTL

在创建索引时,如果索引字段的值是日期类型,那边可以给这个索引设置一个ttl过期时间expireAfterSeconds,当当前事件晚于字段时间expireAfterSeconds时就会删删除这个文档,ongo服务是每分钟扫描一次。

//24小时过期
db.sessions.createIndex({"updatedAt" : 1}, {"expireAfterSeconds" : 60*60*24})

文本索引

支持搜索,类似es的搜索

//创建索引 default_language指定语言,默认是英语  这会涉及到分词 看了官方文档不支持中文
db.user.createIndex({"name":"text","addr":"text"},{"default_language" : "english"})
//利用搜索搜索
db.user.find({"$text":{$search:"四川"}})

索引原则

复合索引满足左匹配,并且索引方向乘-1后的是同一个索引

符合索引建立原则

  • 等值过滤的键应该在最前面;
  • 用于排序的键应该在多值字段之前;
  • 多值过滤的键应该在最后面。 总结:通过索引能够过滤掉大部分数据,能够索引排序就不用内存排序
$ne $not $nin通常无法使用索引
$or 的本质是根据or条件数量启动对应个数的查询计划,然后再合并结果,因此or的条件能够使用索引,但在实际开发中能够单个查询就尽量用单个查询

复合索引的字段中仅最多只能有一个数组字段,另外可以给数组指定位置的元素建立索引

db.person.createIndex({nums.0:1})

索引基数

实际上就是索引区分度,一个索引区分度(重复性)越高,查询效率也就越高

explain

explain还支持其他参数 db.person.find({name:"user_1"}).explain("executionStats")

{ queryPlanner: 
   { plannerVersion: 1,
     namespace: 'user.user',
     indexFilterSet: false,
     parsedQuery: { name: { '$eq': 'user_1' } },
     winningPlan: //获胜的查询计划
      { stage: 'FETCH',
        inputStage: 
         { stage: 'IXSCAN',
           keyPattern: { name: 1 },
           indexName: 'name_1',
           isMultiKey: false,//是否使用多键索引(索引中存在数组字段就会是true)
           multiKeyPaths: { name: [] },
           isUnique: false,//索引是否唯一
           isSparse: false,
           isPartial: false,
           indexVersion: 2,
           direction: 'forward',
           indexBounds: { name: [ '["user_1", "user_1"]' ] } } },//索引
     rejectedPlans: [] },
  executionStats: 
   { executionSuccess: true,
     nReturned: 1,//返回文档数量
     executionTimeMillis: 0,//查询总耗时
     totalKeysExamined: 1,//扫描索引数量
     totalDocsExamined: 1,//扫描文档数量
     executionStages: 
      { stage: 'FETCH',//当前查询结果是直接获取文档。COLLSCAN/全表扫描、IXSCAN/索引扫描、FETCH/根据索引去检索文档、SHARD_MERGE/合并分片结果、IDHACK/针对_id进行查询
        nReturned: 1,
        executionTimeMillisEstimate: 0,
        works: 2,
        advanced: 1,
        needTime: 0,
        needYield: 0,
        saveState: 0,
        restoreState: 0,
        isEOF: 1,
        docsExamined: 1,
        alreadyHasObj: 0,
        inputStage: 
         { stage: 'IXSCAN',//当前查询索引扫描
           nReturned: 1,
           executionTimeMillisEstimate: 0,
           works: 2,
           advanced: 1,
           needTime: 0,
           needYield: 0,//本次查询为写操作让步(暂停)的次数
           saveState: 0,
           restoreState: 0,
           isEOF: 1,
           keyPattern: { name: 1 },
           indexName: 'name_1',
           isMultiKey: false,
           multiKeyPaths: { name: [] },
           isUnique: false,
           isSparse: false,
           isPartial: false,
           indexVersion: 2,
           direction: 'forward',
           indexBounds: { name: [ '["user_1", "user_1"]' ] },
           keysExamined: 1,
           seeks: 1,
           dupsTested: 0,
           dupsDropped: 0 } } },
  serverInfo: 
   { host: '4a8d90f38820',
     port: 27017,
     version: '4.2.19',
     gitVersion: 'e68a7d47305e14e090cba9ce3d92533053299996' },
  ok: 1 }

固定集合

固定集合在创建时就需要指定集合大小,并且固定集合不能删除,只能当新集合插入时,空间不足而删除旧集合。并且在创建集合是还可以指定集合中文档的数量,一旦指定了文档的数量,两者是且的关系。

//创建固定集合 最大尺寸=100000字节,最大文档数=100
db.createCollection("my_collection2",{"capped" : true, "size" : 100000, "max" : 100});

聚合

管道、阶段 一个管道有多个阶段组成,每个阶段都有一个输入和输出

//过滤->排序->跳过->取指定数量->映射
db.user.aggregate([{$match:{"name":/ping/}},{$sort:{"name":-1}},{$skip:1},{$limit:1},{$project:{_id:0,name:1}}])

在投射阶段,如果需要对嵌套字段进行投射,可以使用$符号标记字段路径。

$unwind

就是将数组展开,一个数组元素一个文档

//查询语句
db.user.aggregate({$match:{_id:ObjectId('62823cf127536bc4b52be68c')}},{$unwind:"$nums"})
//数据源
{ _id: ObjectId("62823cf127536bc4b52be68c"),
  name: 'pingwazi',
  addr: '四川省成都市',
  nums: [ 1, 2 ] }
//查询结果
{ _id: ObjectId("62823cf127536bc4b52be68c"),
  name: 'pingwazi',
  addr: '四川省成都市',
  nums: 1 }
{ _id: ObjectId("62823cf127536bc4b52be68c"),
  name: 'pingwazi',
  addr: '四川省成都市',
  nums: 2 }

过滤数组中指定条件的数据

//查询语句 $filter是命令
//input是命令输入参数,只要是数组就行,这里使用路径标识符$指定
//as是数组元素别名
//cond是过滤数组的条件,$$ns是引用数组元素
db.user.aggregate(
  { $match: { _id: ObjectId("62823cf127536bc4b52be68c") } },
  {
    $project: {
      _id: 0,
      name: 1,
      nums: {
        $filter: { input: "$nums", as: "ns", cond: { $gt: ["$$ns", 3] } },
      },
    },
  }
);
//查询结果
{ name: 'pingwazi', nums: [ 4, 5 ] }

只要数组指定位置的元素

//查询文档 0表示第一个,-1表示最后一个
db.user.aggregate(
  { $match: { _id: ObjectId("62823cf127536bc4b52be68c") } },
  {
    $project: {
      _id: 0,
      name: 1,
      fn: { $arrayElemAt: ["$nums", 0] },
      ln: { $arrayElemAt: ["$nums", -1] },
    },
  }
);
//查询结果
{ name: 'pingwazi', fn: 1, ln: 5 }

数组元素分页操作

//查询文档 $slice分页取数组元素 跳过2个取1个
db.user.aggregate(
  { $match: { _id: ObjectId("62823cf127536bc4b52be68c") } },
  {
    $project: {
      _id: 0,
      name: 1,
      nums: { $slice: ["$nums", 2, 1] },
    },
  }
);
//结果
{ name: 'pingwazi', nums: [ 3 ] }

聚合函数

求和(sum)、求平均值(sum)、求平均值(avg)、求最大值(max)、最小值(max)、最小值(min)、第一个值(first)、最后一个值(first)、最后一个值(last)、将结果映射到推送到数组中($push)等

//根据name进行分组,并且一个组内的addr放到addrs数组中 这里可以举一反三 $addToSet
db.user.aggregate({$group:{_id:"$name",addrs:{$push:"$addr"}}})

合并文档

$mergeObjects 合并多个文档(注意是文档,不是单个值),最后的合并结果值以最后一个字段值为准(后面的字段值会覆盖前面的字段值)

//初始化数据
db.user.insertMany([{name:"xiaoping",interest:{name:"单车",score:10}},{name:"xiaoping",interest:{name:"跑步",score:10,"stage":"精通"}}]);
//合并示例
db.user.aggregate([{$group:{_id:"$name",interest:{$mergeObjects:"$interest"}}}])

替换根文档

replaceWith本质上还是replaceWith 本质上还是replaceRoot 替换根文档 通常与mergeObjects结合使用,mergeObjects合并两个文档,repaceWith替换输出根文档。

db.user.aggregate([{$group:{_id:"$name",interest:{$mergeObjects:"$interest"}}},{$replaceWith:"$interest"}])

表关联查询

3.2加入了$lookup操作符,支持将两个表按照指定条件进行关联

固定字段等值连接

语法

{
   $lookup:
     {
       from: <collection to join>,//待关联的表
       localField: <field from the input documents>,//用于关联的本地表字段
       foreignField: <field from the documents of the "from" collection>,//用于关联的关联表字段
       as: <output array field> //关联结果在输出文档中的字段,如果文档中已经存在此字段,则会被覆盖掉
     }
}

示例

//数据准备
db.person.insertMany([{ name: "xiaoping", age: 10 }, { name: "xiaowang", age: 11 }, { name: "pingwazi", age: 18 }]);
db.addr.insertMany([{ name: "xiaoping", addr: "四川省成都市" }, { name: "xiaowang", addr: "四川省巴中市" }, { name: "pingwazi", addr: "平昌" }]);

//连接查询
db.person.aggregate([{$lookup:{from:"addr",localField:"name",foreignField:"name",as:"addr"}}]);

任意连接条件

语法

{
    $lookup:{
        from:"连接的表",
        let:{var1:"$fieldName"},
        pipeline:[
            $match:{},//连接条件 pipeline阶段会把符合条件的连接表中的数据找到赋值给as上的字段
        ],//pipeline举一反三 用法都类似
        as:"连接结果字段"
    }
}

示例

//数据准备
db.person.insertMany([{ name: "xiaoping", age: 10 }, { name: "xiaowang", age: 11 }, { name: "pingwazi", age: 18 }]);
db.addr.insertMany([{ name: "xiaoping", addr: "四川省成都市" }, { name: "xiaowang", addr: "四川省巴中市" }, { name: "pingwazi", addr: "平昌" }]);
//连接查询
db.person.aggregate([{
    $lookup: { 
        from: "addr",
        let:{pname:"$name"},
        pipeline:[{$match:{$expr:{$eq:["$name","$$pname"]}}}],
        as:"paddr"
     }
}]);

设计模式

多态模式

当集合中的所有文档都具有相似但不相同的结构时,我们将其称为多态模式(类比到编程语言的多台性来理解)。再白话一点解释就是一个集合中的文件,存在部分字段相同,其他字段可以不同,当需要查询时就可以避免连表(join)查询。具体的文档放到应用程序中去判断并处理

//it行业 有电脑
{"name":"xiaoping","workType":"it","computer":{"cpu":"2.5GHz","memory":"16GB"}},
//建筑行业  有尺子
{"name":"xiaoli","workType":"build",buildTool:{"name":"ruler"}},

属性模式

出于性能原因考虑,为了优化搜索我们可能需要许多索引以照顾到所有子集。创建所有这些索引可能会降低性能。属性模式为这种情况提供了一个很好的解决方案。

//如下文档 如果要搜索 field**_*的字段的话,就需要建立索引,但是索引又可能会很多,这样就会影响写操作的性能
{"name":"xiaoping","fieldKey_1":1,"fieldValue_1":1,"fieldKey_2":2,"fieldValue_2":2}

//使用属性模式建模 这种建模就只需要在field.key 和field.value上建立所有 索引的数量减少了 并且也便于扩展
{"name":"xiaoping","filed":[{"key":1,"value":1},{"key":2,"value":2}]}

桶模式

类比到编程语言的HashMap的设计上理解,就是将具有相同属性的数据放到一起,这样就能够快速定位数据,并且通过这种方式可以减少文档的数量,提供索引效率

//例如下面这个文档结构,将一天的数据放到一个文档中,并且提供聚合好的值count供查询使用,通过这样的方式文档数量会少很多,并且利用提前聚合好数据可以快速的查询
{
    "name":"xiaoping",
    "startTime":"2022-05-15 00:00:00",
    "endTime":"2022-05-15 23:59:59",
    "data":[
        {
            "key":"1",
            "value":"1",
            "ts":123123
        },
        {
            "key":"2",
            "value":"3",
            "ts":123124
        }
    ],
    "count":2
}

异常值模式

大部分文档都是正常的,只有少数文档存在异常情况(异常的定义需依据实际业务,比如可以认为文档中某一个数组字段的元素超过1000个就是一个异常数据),如果因为少数异常情况而调整设计,进而导致整体性能下降,这样是得不偿失的。 通常的做法是在异常数据中增加一个异常字段,然后在程序中判断这个异常字段做特殊处理。

{
    "name":"xiaoping",
    "nums":[1,2,3,4,...,1000],//例如数组元素超过1000个,就为文档增加一个hasExtra字段,应用程序在处理的时候发现hasExtra为true,就去另外一个地方读取数据。 这样就可以确保大多数情况的方式是快速的,只有极少数访问会稍微慢一点
    "hasExtra":true
}

计算模式

可以理解为之前公司同事做的中间表优化。对于大数据量进行统计时,如果性能较差,可以启一个后台程序每隔一段时间进行计算,并将计算结果保存下来,这样客户端在获取统计信息时就可以直接获取计算好的数据。 这种模式对统计数据实时性要求高的不太友好。

子集模式

这种模式实际上就是将冷热数据分离,将经常访问的数据放到一个集合中,将偶尔访问的数据放到另外一个集合中。 以电商网站的评论为例,用户通过接口取一个商品的详细信息时,商品的评论可以只包含最新的10条。如果用户需要访问更多评论,这点击按钮再进行加载更多即可,而加载更多就可以从子集中获取。

扩展引用模式

此模式就是允许冗余数据,以减少join操作,进而提高查询性能 这个模式的优点也是确定,因为会产生冗余数据,当需要更新时就需要更新很多地方

近似值模式

对于统计数据(例如文章访问次数),如果对精确度的要求不是100%准确的话,可以使用近似值代替。这样可以有效的降低数据库的写操作。可以在内存中记录先做临时记录,当内存中的数据达到一定阈值时(例如100此访问了)就更新到数据库,这样原本需要100此的数据库写入就只需要1此数据库写操作。

树形模式

目前实际开发中用到的,在一个文档中存储一个pid、rootID,通过rootID可以找到整个树,通过pid可以找到直接父节点。 官方推荐的模式是在文档中存储所有的祖先或者后代,这样通过一次查询就能够拿到所有祖先和后代数据,这种模式有一个问题就是当某个节点更换父节点时,这个节点下面的所有后代都需要同步更新。 具体根据实际的业务来

预分配模式

提前生成好一批数据,后面直接用,在给blurrr的邀请码就是这样的设计,虽然出发点和官方说的预分配模式有一定的差异,但是用的方式是一样的。

文档版本控制模式

一个数据库中包含两个集合,一个集合中存放最新版本的文档,另外一个集合存放历史版本的文档

模式版本控制模式

文档中包含版本信息,应用程序根据版本做特殊判断,并执行对应版本的业务逻辑处理。这是mongodb的优势,因为他允许一个集合中,文档与文档之间的字段可以是不同的。这样就可以在数据库不停机的情况下完成数据结构的转换。 这个模式有点类似多态模式

模式总结

  • 多态模式、异常模式、模式版本控制模式 可以都理解为多态模式 文档中存在相同的部分,也存在不同的部分,应用程序根据文档中的特性做对应的处理
  • 属性模式 减少索引维护
  • 桶模式 减少文档数量,对文档进行归档处理
  • 计算模式 类似中间表,提前计算和统计数据 但是对数据实时性不太友好
  • 子集模式 冷热数据分离,提高查询效率
  • 近似值模式 延缓数据入库,降低写操作频率
  • 树形模式、预分配模式 已在实际开发中应用,并且和官方说的有一定出入,需要更新实际的业务而定
  • 文档版本控制模式 就是将修改前的文档存储起来,方便以后回溯

副本集

为满足高可用,mongo的部署架构可以使用一主多从,在正常情况下读写都到主节点上,当主节点因为某些原因无法工作时,自动在从节点中选举一个节点作为主节点。通过这样来避免单机故障。

基于docker搭建副本集环境

  • 1.拉取对应版本的mongodb镜像
docker pull mongo:4.2
  • 2.创建mongo专有网络
docker network create mongo
  • 3.启动三个容器 启动命令中需要指定网络,否则容器之间无法连接,并且启动命令需要使用replSet启动
docker run -d --name mongo-42-27018 -p 27018:27017 --network mongo  mongo:4.2 --replSet rs1
docker run -d --name mongo-42-27019 -p 27019:27017 --network mongo mongo:4.2 --replSet rs1
docker run -d --name mongo-42-27020 -p 27020:27017 --network mongo mongo:4.2 --replSet rs1
  • 4.进入容器初始化集群
//进入容器
docker exec -it mongo-42-27018 mongo
//初始化副本集 注意members中的端口号是容器内部的端口号
rs.initiate({
  _id: "rs1",
  members: [
    { _id: 0, host: "mongo-42-27018:27017" },
    { _id: 1, host: "mongo-42-27019:27017" },
    { _id: 2, host: "mongo-42-27020:27017" },
  ],
});
//显示如下内容就初始化成功
{ "ok" : 1 }
  • 5.验证副本集是否可用
//进入主节点、创建一个数据库,并插入一条数据
use person;
db.person.insert({name:"ping"});
//进入从节点、执行命令让从节点支持读操作,并查询数据
db.getMongo().setSecondaryOk()
use person;
db.person.find();//查询数据
//显示如下内容说明从节点ok
{ "_id" : ObjectId("6291a3c4ff5d3c493623533f"), "name" : "ping" }

常用命令

  • db.getMongo().setSecondaryOk() 在从节点上执行,则从节点支持读操作
  • rs.status() 获取副本集状态
  • rs.isMater() 判断当前是否是主节点 副本集成员支持,可以指定副本集成员的优先级、是否隐藏(客户端无法访问,做数据备份使用)、是否需要同步索引、是否是仲裁节点(仅做选举使用)

主节点选举逻辑

副本集中有一个重要的概念“大多数”,定义为“副本集中一半以上的成员”
注意:副本集中部分成员不可用,并不会影响“大多数”的定义,因为“大多数”是根据副本集配置来计算的。举例:副本集中总共有5个成员,其中三个不可用,那么大多数的表示也还是三个,而不是2两个

副本集成员总数大多数的数量
11
22
32
43
53
64

当一个从节点因为网络问题无法连接主节点时,他会发起一个让他自己成为主节点的投票,如果获得支持的票数满足大多数,他就会成为主节点。
其他成员在投票时
如果自己可以和主节点通信,则投反对票
如果自己的数据比发起投票节点的数据还新,则投反对票
如果自己也无法和主节点连接,数据也和发起节点一样新,那么就比较两者的优先级,对方的优先级高则投赞成票,否则反对票

主节点的数据在整个副本集中都应该是最新的

读偏好

连接字符串支持参数readPreference,用于设置读请求的偏好。

参数值描述优缺点
primary主节点读取优点:数据一致性高 缺点:单点故障,如果没有主节点查询会报错,主节点压力较大
primaryPreferred主节点读取偏好优点:数据一致性相对较高,可以避免单点故障,当主节点挂了后,读请求会路由到从节点上 缺点:缺点当主节点挂了后,在从节点上读取数据可能会有数据一致性问题
secondary从节点读取优点:分摊主节点的压力,如果没有可用从节点,读取将报错 缺点:数据一致性问题会比较突出
secondaryPreferred从节点读取偏好优点:分摊主节点的压力,当从节点挂了后会切换到其他从节点,如果没有任何从节点可用,就用主节点 缺点:数据一致性问题会比较突出
nearest读取延时最低优点:延时低,选取ping的平均延时最低的节点进行读取 缺点:数据一致性无法保证

写关注

db.person.insertOne({name:"2"},{writeConcern:{w:"majority"}})

在写入数据时指定写关注参数,majority就是指得到“大多数”节点写操作确认。其中w还支持指定数字n,表示得到n个副本集成员的写操作确认(包含主节点)。还支持自定义表达式。

自定义表达式

第一步:给副本集成员打上标签

var config = rs.config() 
config.members[0].tags = {"dc" : "us"}
config.members[1].tags = {"dc" : "us"}
config.members[2].tags = {"dc" : "us"}
config.members[0].tags = {"dc" : "us"}
config.members[1].tags = {"dc" : "us"}
config.members[2].tags = {"dc" : "us"}

第二步:设置自定规则

var config = rs.config() 
config.settings = {}
config.settings.getLastErrorModes = [{"eachDC" : {"dc" : 2}}]
rs.reconfig(config)

eachDC是规则名称,dc是标签名称,2是分组数量。合起来的含义就是2个数据中心中每个中心至少有一个副本集成员写操作确认。

第三步:使用自定义规则

//至少得到两个数据中心中各一个副本集成员的写操作确认
db.person.insertOne({name:"2"},{writeConcern:{w:"eachDC"}})

分片

分片是将数据分散存储在不同的分片集群上,以充分利用多台服务器。一个分片中可以存储多个数据块,数据块以片键进行区分

在单机上部署分片集群(实验)

//启动一个mongo shell
mongo --nodb --norc
//初始化分片
st = ShardingTest({ name:"one-min-shards", chunkSize:1,
shards:2,
       rs:{
         nodes:3,
         oplogSize:10
       },
       other:{
         enableBalancer:true
} });
//在启动一个mongo shell
mongo --nodb
//连接到mongos(使用shardingTest启动的分片集群会有10个进程,两个分片副本集(每个副本集各3个节点)、一个配置副本集(3个节点)、一个mongos进程。端口默认从20000开始,因此mongos的端口就是20009)
db = (new Mongo("localhost:20009")).getDB("accounts")
//插入数据
for (var i = 0; i < 100; i++) {
  db.users.insert({ username: "user" + i, created_at: new Date() });
}
//检查数据插入情况
db.user.account()

//数据库中的集合默认不会分发数据,而是将数据都存储在主分片上
//开启数据库的分片功能
sh.enableSharding("accounts")
//集合分片还需要有一个分片键(片键),也就是用于分片的文档字段需要创建索引(为了提高查询效率,也很好理解)
//创建片键
db.users.createIndex({"username" : 1})
//通过指定集合的字段来进行分片操作
sh.shardCollection("accounts.users", {"username" : 1})
//分片的数据块会落在minKey~maxKey之间,这两个范围分别表示无穷小和无穷大
//如果在查询条件中包含片键,则是定向操作,如果没有则就是分散-收集查询

//停止分片集群(会自动清理掉相关进程) 注意是在启动分片集群的mongo shell中执行以下命令
sh.stop()

片键

是分割数据块的核心,支持单字段或复合字段作为片键,通常要选择具有不同值的字段作为片键,另外需要谨慎使用单向自增字段作为片键,因为这会导致所有新增文档都在顶部数据块中,无法分散写操作,对性能也有一定的影响。
通常使用"类型+自增字段"复合片键、hash字段片键等具有一定随机性的字段

持久化

通常数据库写操作除了在数据库数据文件中记录,还会在副本集成员上记录操作日志,并且日志默认是每隔50ms就刷新到磁盘上,而数据库数据文件会每隔60ms就将数据刷新到磁盘上,这60ms就是一个检查点,当一个检查点处理后,就会自动清理掉检查点之前的写操作日志。如果在下一个检查点到来之前服务器奔溃了,这时候重启服务器就可以根据操作日志恢复因程序奔溃而未写入磁盘的数据。

写关注

//w:是指写操作需要得到到多少成员的确认,wtimeout:是指在完成w之前,最多等待的超时时间,j:要求写操作日志在对应副本集成员的journal文件中写入后才算成功(如果是默认值false,则这个副本集还是有一定的时间间隙因为奔溃而无法恢复数据)
db.person.insertOne({name:"2"},{writeConcern:{w:"majority",wtimeout:100,j:true}})

读关注

注意不要和读偏好(readPreference)混淆了,读关注(readConcern)解决的是读取数据的一致性问题。有以下四个级别

级别描述
local读取当前节点的local上面读取数据,这个数据可能还在内存中,没有刷新到磁盘上,有丢失的风险,并且数据也可能没有得到大多数节点的写操作确认,有回滚的风险
available读取数据在当前节点上已经持久到到磁盘,但数据也任然有可能没有得到大多数节点的写操作确认,有回滚的风险
marjority读取已得到大多数节点的写操作确认,数据安全
snapshort从快照中读取,数据已经节点的快照中,快照的数据是得到了大多数节点的写操作确认,数据安全