转载 自用Mongodb

235 阅读27分钟

转载自zhenye-na.github.io/2020/01/27/… 侵权请联系删除

课程目标

MongoDB的副本集: 操作, 主要概念, 故障转移, 选举规则 MongoDB的分片集群:概念, 优点, 操作, 分片策略, 故障转移 MongoDB的安全认证

  • 理解 MongoDB 的业务场景, 熟悉 MongoDB 的简介, 特点和体系结构, 数据类型等.
  • 能够在 Windows 和 Linux 下安装和启动 MongoDB, 图形化管理界面 Compass 的安装使用
  • 掌握 MongoDB 基本常用命令实现数据的 CRUD
  • 掌握 MongoDB 的索引类型, 索引管理, 执行计划

1. MongoDB 相关概念**

1.1 业务场景**

传统的关系型数据库 (比如 MySQL), 在数据操作的”三高”需求以及对应的 Web 2.0 网站需求面前, 会有”力不从心”的感觉

所谓的三高需求:

高并发, 高性能, 高可用, 简称三高

  • High Performance: 对数据库的高并发读写的要求
  • High Storage: 对海量数据的高效率存储和访问的需求
  • High Scalability && High Available: 对数据的高扩展性和高可用性的需求

而 MongoDB 可以应对三高需求

具体的应用场景:

  • 社交场景, 使用 MongoDB 存储存储用户信息, 以及用户发表的朋友圈信息, 通过地理位置索引实现附近的人, 地点等功能.
  • 游戏场景, 使用 MongoDB 存储游戏用户信息, 用户的装备, 积分等直接以内嵌文档的形式存储, 方便查询, 高效率存储和访问.
  • 物流场景, 使用 MongoDB 存储订单信息, 订单状态在运送过程中会不断更新, 以 MongoDB 内嵌数组的形式来存储, 一次查询就能将订单所有的变更读取出来.
  • 物联网场景, 使用 MongoDB 存储所有接入的智能设备信息, 以及设备汇报的日志信息, 并对这些信息进行多维度的分析.
  • 视频直播, 使用 MongoDB 存储用户信息, 点赞互动信息等.

这些应用场景中, 数据操作方面的共同点有:

  1. 数据量大
  2. 写入操作频繁
  3. 价值较低的数据, 对事务性要求不高

对于这样的数据, 更适合用 MongoDB 来实现数据存储

那么我们什么时候选择 MongoDB 呢?

除了架构选型上, 除了上述三个特点之外, 还要考虑下面这些问题:

  • 应用不需要事务及复杂 JOIN 支持
  • 新应用, 需求会变, 数据模型无法确定, 想快速迭代开发
  • 应用需要 2000 - 3000 以上的读写QPS(更高也可以)
  • 应用需要 TB 甚至 PB 级别数据存储
  • 应用发展迅速, 需要能快速水平扩展
  • 应用要求存储的数据不丢失
  • 应用需要 99.999% 高可用
  • 应用需要大量的地理位置查询, 文本查询

如果上述有1个符合, 可以考虑 MongoDB, 2个及以上的符合, 选择 MongoDB 绝不会后悔.

如果用MySQL呢?

相对MySQL, 可以以更低的成本解决问题(包括学习, 开发, 运维等成本)

1.2 MongoDB 简介**

MongoDB是一个开源, 高性能, 无模式的文档型数据库, 当初的设计就是用于简化开发和方便扩展, 是NoSQL数据库产品中的一种.是最 像关系型数据库(MySQL)的非关系型数据库. 它支持的数据结构非常松散, 是一种类似于 JSON 的 格式叫BSON, 所以它既可以存储比较复杂的数据类型, 又相当的灵活. MongoDB中的记录是一个文档, 它是一个由字段和值对(field:value)组成的数据结构.MongoDB文档类似于JSON对象, 即一个文档认 为就是一个对象.字段的数据类型是字符型, 它的值除了使用基本的一些类型外, 还可以包括其他文档, 普通数组和文档数组.

“最像关系型数据库的 NoSQL 数据库” . MongoDB 中的记录是一个文档, 是一个 key-value pair. 字段的数据类型是字符型, 值除了使用基本的一些类型以外, 还包括其它文档, 普通数组以及文档数组

MongoDB 数据模型是面向文档的, 所谓文档就是一种类似于 JSON 的结构, 简单理解 MongoDB 这个数据库中存在的是各种各样的 JSON(BSON)

  • 数据库 (database)

    • 数据库是一个仓库, 存储集合 (collection)
  • 集合 (collection)

    • 类似于数组, 在集合中存放文档
  • 文档 (document)

    • 文档型数据库的最小单位, 通常情况, 我们存储和操作的内容都是文档

在 MongoDB 中, 数据库和集合都不需要手动创建, 当我们创建文档时, 如果文档所在的集合或者数据库不存在, 则会自动创建数据库或者集合

数据库 (databases) 管理语法**

操作语法
查看所有数据库show dbs; 或 show databases;
查看当前数据库db;
切换到某数据库 (若数据库不存在则创建数据库)use <db_name>;
删除当前数据库db.dropDatabase();

集合 (collection) 管理语法**

操作语法
查看所有集合show collections;
创建集合db.createCollection("<collection_name>");
删除集合db.<collection_name>.drop()

1.3. 数据模型**

1.4 MongoDB 的特点**

1.4.1 高性能**

MongoDB 提供高性能的数据持久化

  • 嵌入式数据模型的支持减少了数据库系统上的 I/O 活动
  • 索引支持更快的查询, 并且可以包含来自嵌入式文档和数组的键 (文本索引解决搜索的需求, TTL 索引解决历史数据自动过期的需求, 地理位置索引可以用于构件各种 O2O 应用)
  • mmapv1, wiredtiger, mongorocks (rocksdb) in-memory 等多引擎支持满足各种场景需求
  • Gridfs 解决文件存储需求

1.4.2 高可用**

MongoDB 的复制工具称作副本集 (replica set) 可以提供自动故障转移和数据冗余

1.4.3 高扩展**

水平扩展是其核心功能一部分

分片将数据分布在一组集群的机器上 (海量数据存储, 服务能力水平扩展)

MongoDB 支持基于片键创建数据区域, 在一个平衡的集群当中, MongoDB 将一个区域所覆盖的读写只定向到该区域的那些片

1.4.4 其他**

MongoDB支持丰富的查询语言, 支持读和写操作(CRUD), 比如数据聚合, 文本搜索和地理空间查询等. 无模式(动态模式), 灵活的文档模型

2. 基本常用命令**

2.1 数据库操作**

默认保留的数据库

  • admin: 从权限角度考虑, 这是 root 数据库, 如果将一个用户添加到这个数据库, 这个用户自动继承所有数据库的权限, 一些特定的服务器端命令也只能从这个数据库运行, 比如列出所有的数据库或者关闭服务器
  • local: 数据永远不会被复制, 可以用来存储限于本地的单台服务器的集合 (部署集群, 分片等)
  • config: Mongo 用于分片设置时, config 数据库在内部使用, 用来保存分片的相关信息
$ show dbs


$ use articledb

$ show dbs

当使用 use articledb 的时候. articledb 其实存放在内存之中, 当 articledb 中存在一个 collection 之后, mongo 才会将这个数据库持久化到硬盘之中.

2.2 文档基本 CRUD**

官方文档: docs.mongodb.com/manual/crud…

2.2.1 创建 Create**

Create or insert operations add new documents to a collection. If the collection does not currently exist, insert operations will create the collection automatically.

  • 使用 db.<collection_name>.insertOne() 向集合中添加一个文档, 参数一个 json 格式的文档
  • 使用 db.<collection_name>.insertMany() 向集合中添加多个文档, 参数为 json 文档数组

db.collection.insert({
  <document or array of documents>,
  writeConcern: <document>,
  ordered: <boolean>
})


// 向集合中添加一个文档
db.collection.insertOne(
   { item: "canvas", qty: 100, tags: ["cotton"], size: { h: 28, w: 35.5, uom: "cm" } }
)
// 向集合中添加多个文档
db.collection.insertMany([
   { item: "journal", qty: 25, tags: ["blank", "red"], size: { h: 14, w: 21, uom: "cm" } },
   { item: "mat", qty: 85, tags: ["gray"], size: { h: 27.9, w: 35.5, uom: "cm" } },
   { item: "mousepad", qty: 25, tags: ["gel", "blue"], size: { h: 19, w: 22.85, uom: "cm" } }
])

注:当我们向 collection 中插入 document 文档时, 如果没有给文档指定 _id 属性, 那么数据库会为文档自动添加 _id field, 并且值类型是 ObjectId(blablabla), 就是文档的唯一标识, 类似于 relational database 里的 primary key

  • mongo 中的数字, 默认情况下是 double 类型, 如果要存整型, 必须使用函数 NumberInt(整型数字), 否则取出来就有问题了
  • 插入当前日期可以使用 new Date()

如果某条数据插入失败, 将会终止插入, 但已经插入成功的数据不会回滚掉. 因为批量插入由于数据较多容易出现失败, 因此, 可以使用 try catch 进行异常捕捉处理, 测试的时候可以不处理.如:

try {
  db.comment.insertMany([
    {"_id":"1","articleid":"100001","content":"我们不应该把清晨浪费在手机上, 健康很重要, 一杯温水幸福你我 他.","userid":"1002","nickname":"相忘于江湖","createdatetime":new Date("2019-0805T22:08:15.522Z"),"likenum":NumberInt(1000),"state":"1"},
    {"_id":"2","articleid":"100001","content":"我夏天空腹喝凉开水, 冬天喝温开水","userid":"1005","nickname":"伊人憔 悴","createdatetime":new Date("2019-08-05T23:58:51.485Z"),"likenum":NumberInt(888),"state":"1"},
    {"_id":"3","articleid":"100001","content":"我一直喝凉开水, 冬天夏天都喝.","userid":"1004","nickname":"杰克船 长","createdatetime":new Date("2019-08-06T01:05:06.321Z"),"likenum":NumberInt(666),"state":"1"},
    {"_id":"4","articleid":"100001","content":"专家说不能空腹吃饭, 影响健康.","userid":"1003","nickname":"凯 撒","createdatetime":new Date("2019-08-06T08:18:35.288Z"),"likenum":NumberInt(2000),"state":"1"},
    {"_id":"5","articleid":"100001","content":"研究表明, 刚烧开的水千万不能喝, 因为烫 嘴.","userid":"1003","nickname":"凯撒","createdatetime":new Date("2019-0806T11:01:02.521Z"),"likenum":NumberInt(3000),"state":"1"}

]);

} catch (e) {
  print (e);
}

2.2.2 查询 Read**

  • 使用 db.<collection_name>.find() 方法对集合进行查询, 接受一个 json 格式的查询条件. 返回的是一个数组
  • db.<collection_name>.findOne() 查询集合中符合条件的第一个文档, 返回的是一个对象

可以使用 $in 操作符表示范围查询

db.inventory.find( { status: { $in: [ "A", "D" ] } } )

多个查询条件用逗号分隔, 表示 AND 的关系

db.inventory.find( { status: "A", qty: { $lt: 30 } } )

等价于下面 sql 语句

SELECT * FROM inventory WHERE status = "A" AND qty < 30

使用 $or 操作符表示后边数组中的条件是OR的关系

db.inventory.find( { $or: [ { status: "A" }, { qty: { $lt: 30 } } ] } )

等价于下面 sql 语句

SELECT * FROM inventory WHERE status = "A" OR qty < 30

联合使用 AND 和 OR 的查询语句

db.inventory.find( {
     status: "A",
     $or: [ { qty: { $lt: 30 } }, { item: /^p/ } ]
} )

在 terminal 中查看结果可能不是很方便, 所以我们可以用 pretty() 来帮助阅读

db.inventory.find().pretty()

匹配内容

db.posts.find({
  comments: {
    $elemMatch: {
      user: 'Harry Potter'
    }
  }
}).pretty()

// 正则表达式
db.<collection_name>.find({ content : /once/ })

创建索引

db.posts.createIndex({
  { title : 'text' }
})

// 文本搜索
// will return document with title "Post One"
// if there is no more posts created
db.posts.find({
  $text : {
    $search : ""Post O""
  }
}).pretty()

2.2.3 更新 Update**

  • 使用 db.<collection_name>.updateOne(<filter>, <update>, <options>) 方法修改一个匹配 <filter> 条件的文档
  • 使用 db.<collection_name>.updateMany(<filter>, <update>, <options>) 方法修改所有匹配 <filter> 条件的文档
  • 使用 db.<collection_name>.replaceOne(<filter>, <update>, <options>) 方法替换一个匹配 <filter> 条件的文档
  • db.<collection_name>.update(查询对象, 新对象) 默认情况下会使用新对象替换旧对象

其中 <filter> 参数与查询方法中的条件参数用法一致.

如果需要修改指定的属性, 而不是替换需要用“修改操作符”来进行修改

  • $set 修改文档中的制定属性

其中最常用的修改操作符即为$set$unset,分别表示赋值取消赋值.

db.inventory.updateOne(
    { item: "paper" },
    {
        $set: { "size.uom": "cm", status: "P" },
        $currentDate: { lastModified: true }
    }
)

db.inventory.updateMany(
    { qty: { $lt: 50 } },
    {
        $set: { "size.uom": "in", status: "P" },
        $currentDate: { lastModified: true }
    }
)
  • uses the $set operator to update the value of the size.uom field to "cm" and the value of the status field to "P",
  • uses the $currentDate operator to update the value of the lastModified field to the current date. If lastModified field does not exist, $currentDate will create the field. See $currentDate for details.

db.<collection_name>.replaceOne() 方法替换除 _id 属性外的所有属性, 其<update>参数应为一个全新的文档.

db.inventory.replaceOne(
    { item: "paper" },
    { item: "paper", instock: [ { warehouse: "A", qty: 60 }, { warehouse: "B", qty: 40 } ] }
)

批量修改

// 默认会修改第一条
db.document.update({ userid: "30", { $set {username: "guest"} } })

// 修改所有符合条件的数据
db.document.update( { userid: "30", { $set {username: "guest"} } }, {multi: true} )

列值增长的修改

如果我们想实现对某列值在原有值的基础上进行增加或减少, 可以使用 $inc 运算符来实现

db.document.update({ _id: "3", {$inc: {likeNum: NumberInt(1)}} })
修改操作符**
NameDescription
$currentDateSets the value of a field to current date, either as a Date or a Timestamp.
$incIncrements the value of the field by the specified amount.
$minOnly updates the field if the specified value is less than the existing field value.
$maxOnly updates the field if the specified value is greater than the existing field value.
$mulMultiplies the value of the field by the specified amount.
$renameRenames a field.
$setSets the value of a field in a document.
$setOnInsertSets the value of a field if an update results in an insert of a document. Has no effect on update operations that modify existing documents.
$unsetRemoves the specified field from a document.

2.2.4 删除 Delete**

  • 使用 db.collection.deleteMany() 方法删除所有匹配的文档.
  • 使用 db.collection.deleteOne() 方法删除单个匹配的文档.
  • db.collection.drop()
  • db.dropDatabase()
db.inventory.deleteMany( { qty : { $lt : 50 } } )

Delete operations do not drop indexes, even if deleting all documents from a collection.

一般数据库中的数据都不会真正意义上的删除, 会添加一个字段, 用来表示这个数据是否被删除

2.3 文档排序和投影 (sort & projection)**

2.3.1 排序 Sort**

在查询文档内容的时候, 默认是按照 _id 进行排序

我们可以用 $sort 更改文档排序规则

{ $sort: { <field1>: <sort order>, <field2>: <sort order> ... } }

For the field or fields to sort by, set the sort order to 1 or -1 to specify an ascending or descending sort respectively, as in the following example:

db.users.aggregate(
   [
     { $sort : { age : -1, posts: 1 } }
     // ascending on posts and descending on age
   ]
)
$sort Operator and Memory**
$sort + $limit Memory Optimization**

When a $sort precedes a $limit and there are no intervening stages that modify the number of documents, the optimizer can coalesce the $limit into the $sort. This allows the $sort operation to only maintain the top n results as it progresses, where n is the specified limit, and ensures that MongoDB only needs to store n items in memory. This optimization still applies when allowDiskUse is true and the n items exceed the aggregation memory limit.

Optimizations are subject to change between releases.

有点类似于用 heap 做 topK 这种问题, 只维护 k 个大小的 heap, 会加速 process

举个栗子:

db.posts.find().sort({ title : -1 }).limit(2).pretty()

2.3.2 投影 Projection**

有些情况, 我们对文档进行查询并不是需要所有的字段, 比如只需要 id 或者 用户名, 我们可以对文档进行“投影”

  • 1 - display
  • 0 - dont display
> db.users.find( {}, {username: 1} )

> db.users.find( {}, {age: 1, _id: 0} )

2.4 forEach()**

> db.posts.find().forEach(fucntion(doc) { print('Blog Post: ' + doc.title) })

2.5 其他查询方式**

2.5.1 正则表达式**

$ db.collection.find({field:/正则表达式/})

$ db.collection.find({字段:/正则表达式/})

2.5.2 比较查询**

<<=>>= 这些操作符也是很常用的, 格式如下:

db.collection.find({ "field" : { $gt: value }}) // 大于: field > value
db.collection.find({ "field" : { $lt: value }}) // 小于: field < value
db.collection.find({ "field" : { $gte: value }}) // 大于等于: field >= value
db.collection.find({ "field" : { $lte: value }}) // 小于等于: field <= value
db.collection.find({ "field" : { $ne: value }}) // 不等于: field != value

2.5.3 包含查询**

包含使用 $in 操作符. 示例:查询评论的集合中 userid 字段包含 1003 或 1004的文档

db.comment.find({userid:{$in:["1003","1004"]}})

不包含使用 $nin 操作符. 示例:查询评论集合中 userid 字段不包含 1003 和 1004 的文档

db.comment.find({userid:{$nin:["1003","1004"]}})

2.6 常用命令小结**

选择切换数据库:use articledb
插入数据:db.comment.insert({bson数据})
查询所有数据:db.comment.find();
条件查询数据:db.comment.find({条件})
查询符合条件的第一条记录:db.comment.findOne({条件})
查询符合条件的前几条记录:db.comment.find({条件}).limit(条数)
查询符合条件的跳过的记录:db.comment.find({条件}).skip(条数)

修改数据:db.comment.update({条件},{修改后的数据})
        或
        db.comment.update({条件},{$set:{要修改部分的字段:数据})

修改数据并自增某字段值:db.comment.update({条件},{$inc:{自增的字段:步进值}})

删除数据:db.comment.remove({条件})
统计查询:db.comment.count({条件})
模糊查询:db.comment.find({字段名:/正则表达式/})
条件比较运算:db.comment.find({字段名:{$gt:值}})
包含查询:db.comment.find({字段名:{$in:[值1, 值2]}})
        或
        db.comment.find({字段名:{$nin:[值1, 值2]}})

条件连接查询:db.comment.find({$and:[{条件1},{条件2}]})
           或
           db.comment.find({$or:[{条件1},{条件2}]})

3. 文档间的对应关系**

  • 一对一 (One To One)
  • 一对多 (One To Many)
  • 多对多 (Many To Many)

举个例子, 比如“用户-订单”这个一对多的关系中, 我们想查询某一个用户的所有或者某个订单, 我们可以

var user_id = db.users.findOne( {username: "username_here"} )._id
db.orders.find( {user_id: user_id} )

4. MongoDB 的索引**

4.1 概述**

索引支持在 MongoDB 中高效地执行查询.如果没有索引, MongoDB 必须执行全集合扫描, 即扫描集合中的每个文档, 以选择与查询语句 匹配的文档.这种扫描全集合的查询效率是非常低的, 特别在处理大量的数据时, 查询可以要花费几十秒甚至几分钟, 这对网站的性能是非常致命的.

如果查询存在适当的索引, MongoDB 可以使用该索引限制必须检查的文档数.

索引是特殊的数据结构, 它以易于遍历的形式存储集合数据集的一小部分.索引存储特定字段或一组字段的值, 按字段值排序.索引项的排 序支持有效的相等匹配和基于范围的查询操作.此外, MongoDB 还可以使用索引中的排序返回排序结果.

MongoDB 使用的是 B Tree, MySQL 使用的是 B+ Tree

// create index
db.<collection_name>.createIndex({ userid : 1, username : -1 })

// retrieve indexes
db.<collection_name>.getIndexes()

// remove indexes
db.<collection_name>.dropIndex(index)

// there are 2 ways to remove indexes:
// 1. removed based on the index name
// 2. removed based on the fields

db.<collection_name>.dropIndex( "userid_1_username_-1" )
db.<collection_name>.dropIndex({ userid : 1, username : -1 })

// remove all the indexes, will only remove non_id indexes
db.<collection_name>.dropIndexes()

4.2 索引的类型**

4.2.1 单字段索引**

MongoDB 支持在文档的单个字段上创建用户定义的升序/降序索引, 称为单字段索引 Single Field Index

对于单个字段索引和排序操作, 索引键的排序顺序(即升序或降序)并不重要, 因为 MongoDB 可以在任何方向上遍历索引.

4.2.2 复合索引**

MongoDB 还支持多个字段的用户定义索引, 即复合索引 Compound Index

复合索引中列出的字段顺序具有重要意义.例如, 如果复合索引由 { userid: 1, score: -1 } 组成, 则索引首先按 userid 正序排序, 然后 在每个 userid 的值内, 再在按 score 倒序排序.

4.2.3 其他索引**

  • 地理空间索引 Geospatial Index
  • 文本索引 Text Indexes
  • 哈希索引 Hashed Indexes
地理空间索引(Geospatial Index)**

为了支持对地理空间坐标数据的有效查询, MongoDB 提供了两种特殊的索引: 返回结果时使用平面几何的二维索引和返回结果时使用球面几何的二维球面索引.

文本索引(Text Indexes)**

MongoDB 提供了一种文本索引类型, 支持在集合中搜索字符串内容.这些文本索引不存储特定于语言的停止词(例如 “the”, “a”, “or”), 而将集合中的词作为词干, 只存储根词.

哈希索引(Hashed Indexes)**

为了支持基于散列的分片, MongoDB 提供了散列索引类型, 它对字段值的散列进行索引.这些索引在其范围内的值分布更加随机, 但只支持相等匹配, 不支持基于范围的查询.

4.3 索引的管理操作**

4.3.1 索引的查看**

语法

db.collection.getIndexes()

默认 _id 索引: MongoDB 在创建集合的过程中, 在 _id 字段上创建一个唯一的索引, 默认名字为 _id , 该索引可防止客户端插入两个具有相同值的文 档, 不能在 _id 字段上删除此索引.

注意:该索引是唯一索引, 因此值不能重复, 即 _id 值不能重复的.

在分片集群中, 通常使用 _id 作为片键.

4.3.2 索引的创建**

语法

db.collection.createIndex(keys, options)

参数

image-20200506203419523

options(更多选项)列表

image-20200506203453430

注意在 3.0.0 版本前创建索引方法为 db.collection.ensureIndex() , 之后的版本使用了 db.collection.createIndex() 方法, ensureIndex() 还能用, 但只是 createIndex() 的别名.

举个🌰

$  db.comment.createIndex({userid:1})
{
  "createdCollectionAutomatically" : false,
  "numIndexesBefore" : 1,
  "numIndexesAfter" : 2,
  "ok" : 1
}

$ db.comment.createIndex({userid:1,nickname:-1})
...

4.3.3 索引的删除**

语法

# 删除某一个索引
$ db.collection.dropIndex(index)

# 删除全部索引
$ db.collection.dropIndexes()

提示:

_id 的字段的索引是无法删除的, 只能删除非 _id 字段的索引

示例

# 删除 comment 集合中 userid 字段上的升序索引
$ db.comment.dropIndex({userid:1})

4.4 索引使用**

4.4.1 执行计划**

分析查询性能 (Analyze Query Performance) 通常使用执行计划 (解释计划 - Explain Plan) 来查看查询的情况

$ db.<collection_name>.find( query, options ).explain(options)

比如: 查看根据 user_id 查询数据的情况

未添加索引之前

"stage" : "COLLSCAN", 表示全集合扫描

添加索引之后

"stage" : "IXSCAN", 基于索引的扫描

4.4.2 涵盖的查询**

当查询条件和查询的投影仅包含索引字段是, MongoDB 直接从索引返回结果, 而不扫描任何文档或将文档带入内存, 这些覆盖的查询十分有效

docs.mongodb.com/manual/core…

5. 在 Nodejs 中使用 MongoDB - mongoose**

mongoose 是一个对象文档模型(ODM)库

mongoosejs.com/

  • 可以为文档创建一个模式结构(Schema)
  • 可以对模型中的对象/文档进行验证
  • 数据可以通过类型转换转换为对象模型
  • 可以使用中间件应用业务逻辑

5.1 mongoose 提供的新对象类型**

  • Schema

    • 定义约束了数据库中的文档结构
    • 个人感觉类似于 SQL 中建表时事先规定表结构
  • Model

    • 集合中的所有文档的表示, 相当于 MongoDB 数据库中的 collection
  • Document

    • 表示集合中的具体文档, 相当于集合中的一个具体的文档

5.2 简单使用 Mongoose**

mongoosejs.com/docs/guide.…

使用 mongoose 返回的是一个 mogoose Query object, mongoose 执行 query 语句后的结果会被传进 callback 函数 callback(error, result)

A mongoose query can be executed in one of two ways. First, if you pass in a callback function, Mongoose will execute the query asynchronously and pass the results to the callback.

A query also has a .then() function, and thus can be used as a promise.

const q = MyModel.updateMany({}, { isDeleted: true }, function() {
  console.log("Update 1");
}));

q.then(() => console.log("Update 2"));
q.then(() => console.log("Update 3"));

上面这一段代码会执行三次 updateMany() 操作, 第一次是因为 callback, 之后的两次是因为 .then() (因为 .then() 也会调用 updatemany())

连接数据库并且创建 Model 类

const mongoose = require('mongoose');
// test is the name of database, will be created automatically
mongoose.connect('mongodb://localhost:27017/test', {useNewUrlParser: true});

const Cat = mongoose.model('Cat', { name: String });

const kitty = new Cat({ name: 'Zildjian' });
kitty.save().then(() => console.log('meow'));

监听 MongoDB 数据库的连接状态

在 mongoose 对象中, 有一个属性叫做 connection, 该对象就表示数据库连接.通过监视该对象的状态, 可以来监听数据库的连接和端口

mongoose.connection.once("open", function() {
  console.log("connection opened.")
});

mongoose.connection.once("close", function() {
  console.log("connection closed.")
});

5.3 Mongoose 的 CRUD**

首先定义一个 Schema

const mongoose = require('mongoose');
const Schema = mongoose.Schema;

const blogSchema = new Schema({
    title:  String, // String is shorthand for {type: String}
    author: String,
    body:   String,
    comments: [{ body: String, date: Date }],
    date: { type: Date, default: Date.now },
    hidden: Boolean,
    meta: {
        votes: Number,
        favs:  Number
    }
});

然后在 blogSchema 基础上创建 Model

const Blog = mongoose.model('Blog', blogSchema);
// ready to go!

module.exports = Blog;

当调用上面这一行代码时, MongoDB 会做如下操作

  1. 是否存在一个数据库叫做 Blog 啊? 没的话那就创建一个
  2. 每次用到 Blog 库的时候都要注意内部数据要按照 blogSchema 来规定

向数据库中插入文档数据

Blog.create({
  title: "title"
  ...
}, function (err){
  if (!err) {
    console.log("successful")
  }
});

简单的查询一下下

// named john and at least 18 yo
MyModel.find({ name: 'john', age: { $gte: 18 }});

mongoose 支持的用法有:

6. 使用 Mocha 编写测试 “Test Driven Development”**

Mocha 是一个 js 测试的包, 编写测试有两个关键字 describe 和 it

  • describe 是一个”统领块”, 所有的 test functions 都会在它”名下”
  • it 表示每一个 test function

create_test.js

const assert = require('assert')
// assume we have a User model defined in src/user.js
const User = require('../src/user')

// after installing Mocha, we have global access
// to describe and it keywords
describe('Creating records', () => {
  it('saves a user', () => {
    const joe = new User({ name: "Joe" });
    joe.save();
    assert()
  });
});

7. NoSQL Databases**

Benefits of NoSQL

  • Easy for inserting and retrieving data, since they are contained in one block, in one json object
  • Flexible schema, if a new attribute added, it is easy to just add / append to the object
  • Scalability, horizontally partition the data (availability > consistency)
  • Aggregation, find metrics and etc

Drawbacks of NoSQL

  • Update = Delete + Insert, not built for update
  • Not consistent, ACID is not guaranteed, do not support transactions
  • Not read optimized. Read entire block find the attribute. But SQL, just need one column (read time compartively slow)
  • Relations are not implicit
  • JOINS are hard to accomplish, all manually

MongoDB 数据库高级进阶 - 集群和安全**

配套资料: https://pan.baidu.com/s/18au42FIhSNrXY9p7MbmNbg 提取码: 29ad

感谢 B 站用户 冷鸟丨会飞 分享

课程目标

  • MongoDB 的副本集:操作、主要概念、故障转移、选举规则
  • MongoDB 的分片集群:概念、优点、操作、分片策略、故障转移
  • MongoDB 的安全认证

1. MongoDB 副本集 - Replica Sets**

1.1 简介**

MongoDB 中的副本集(Replica Set)是一组维护相同数据集的 mongod 服务。 副本集可提供冗余和高可用性,是所有生产部署的基础。

也可以说,副本集类似于有自动故障恢复功能的主从集群。通俗的讲就是用多台机器进行同一数据的异步同步,从而使多台机器拥有同一数据的多个副本,并且当主库当掉时在不需要用户干预的情况下自动切换其他备份服务器做主库。而且还可以利用副本服务器做只读服务器,实现读写分离,提高负载

冗余和数据可用性**

复制提供冗余并提高数据可用性。 通过在不同数据库服务器上提供多个数据副本,复制可提供一定级别的容错功能,以防止丢失单个数据库服务器。

在某些情况下,复制可以提供增加的读取性能,因为客户端可以将读取操作发送到不同的服务上, 在不同数据中心维护数据副本可以增加分布式应用程序的数据位置和可用性。 还可以为专用目的维护其他副本,例如灾难恢复,报告或备份。

MongoDB 中的复制**

副本集是一组维护相同数据集的 mongod 实例。 副本集包含多个数据承载节点和可选的一个仲裁节点。 在承载数据的节点中,一个且仅一个成员被视为主节点,而其他节点被视为次要(从)节点。

主节点接收所有写操作。 副本集只能有一个主要能够确认具有 {w:"most"} 写入关注的写入; 虽然在某些情况下,另一个 mongod 实例可能暂时认为自己也是主要的。主要记录其操作日志中的数据集的所有 更改,即 oplog。

辅助(副本)节点复制主节点的oplog并将操作应用于其数据集,以使辅助节点的数据集反映主节点的数据 集。 如果主要人员不在,则符合条件的中学将举行选举以选出新的主要人员。

主从复制和副本集区别**

主从集群和副本集最大的区别就是副本集没有固定的”主节点” ;整个集群会选出一个”主节点”,当其挂掉后,又在剩下的从节点中选中其他节点为主节点,副本集总有一个活跃点 (主、primary) 和一个或多个备份节点 (从、secondary)

1.2 副本集的三个角色**

副本集有两种类型三种角色

两种类型:

  • 主节点(Primary)类型:数据操作的主要连接点,可读写
  • 次要(辅助、从)节点(Secondary)类型:数据冗余备份节点,可以读或选举

三种角色:

  • 主要成员(Primary):主要接收所有写操作。就是主节点
  • 副本成员(Replicate):从主节点通过复制操作以维护相同的数据集,即备份数据,不可写操作,但可以读操作(但需要配置)。是默认的一种从节点类型
  • 仲裁者(Arbiter):不保留任何数据的副本,只具有投票选举作用。当然也可以将仲裁服务器维护为副本集的一部分,即副本成员同时也可以是仲裁者。也是一种从节点类型。

关于仲裁者的额外说明:

您可以将额外的 mongod 实例添加到副本集作为仲裁者。 仲裁者不维护数据集。 仲裁者的目的是通过响应其他副本集成员的心跳和选举请求来维护副本集中的仲裁。 因为它们不存储数据集,所以仲裁器可以是提供副本集仲裁功能的好方法,其资源成本比具有数据集的全功能副本集成员更便宜。

如果您的副本集具有偶数个成员,请添加仲裁者以获得主要选举中的大多数投票。 仲裁者不需要专用 硬件。

仲裁者将永远是仲裁者,而主要人员可能会退出并成为次要人员,而次要人员可能成为选举期间的主要人员。

如果你的副本+主节点的个数是偶数,建议加一个仲裁者,形成奇数,容易满足大多数的投票。

如果你的副本+主节点的个数是奇数,可以不加仲裁者。

说人话就是 Paxos 协议算法, 建议阅读

1.3 动手实现一个副本集**

1.3.1 创建节点**

使用一个主节点, 一个副节点, 一个仲裁节点

  1. 用端口号区分不同的节点
  2. 副本集名称都是 myrs

主节点 (PRIMARY), 副节点 (SECONDARY), 以及仲裁节点 (ARBITER) 的创建过程详见配套文档

1.3.2 初始化配置副本集和主节点**

使用客户端命令连接任意一个节点,但这里尽量要连接主节点 (27017节点)

$ /usr/local/mongodb/bin/mongo --host=180.76.159.126 --port=27017

连接上之后,很多命令无法使用, 比如 show dbs 等,必须初始化副本集才行

初始化新的副本集

# example, `configuration` is optional
# rs.initiate(configuration)

$ rs.initiate()
{
  "info2" : "no configuration specified. Using a default configuration for the set",
  "me" : "<ip_address>:27017",
  "ok" : 1,
  "operationTime" : Timestamp(1565760476, 1),
  "$clusterTime" : {
    "clusterTime" : Timestamp(1565760476, 1),
    "signature" : {
    "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
    "keyId" : NumberLong(0)
    }
  }
}
myrs:SECONDARY> <hit enter>
myrs:PRIMARY>
  1. ok 的值为 1, 说明创建成功
  2. 命令行提示符发生变化,变成了一个从节点角色,此时默认不能读写。稍等片刻,回车,变成主节 点。

1.3.3 查看配置**

# configuration - optional
$ rs.conf(configuration)

myrs:PRIMARY> rs.conf()
{
  "_id" : "myrs",
  "version" : 1,
  "protocolVersion" : NumberLong(1),
  "writeConcernMajorityJournalDefault" : true,
  "members" : [{
    "_id" : 0,
    "host" : "180.76.159.126:27017",
    "arbiterOnly" : false,
    "buildIndexes" : true,
    "hidden" : false,
    "priority" : 1,
    "tags" : {},
    "slaveDelay" : NumberLong(0),
    "votes" : 1
  }],
  "settings" : {
    "chainingAllowed" : true,
    "heartbeatIntervalMillis" : 2000,
    "heartbeatTimeoutSecs" : 10,
    "electionTimeoutMillis" : 10000,
    "catchUpTimeoutMillis" : -1,
    "catchUpTakeoverDelayMillis" : 30000,
    "getLastErrorModes" : {},
    "getLastErrorDefaults" : {
      "w" : 1,
      "wtimeout" : 0
    },
    "replicaSetId" : ObjectId("5d539bdcd6a308e600d126bb")
  }
}
  • "_id" : "myrs" :副本集的配置数据存储的主键值,默认就是副本集的名字
  • "members" :副本集成员数组,此时只有一个: "host" : "180.76.159.126:27017" ,该成员不是仲裁节点: "arbiterOnly" : false ,优先级(权重值): "priority" : 1
  • "settings" :副本集的参数配置。

1.3.4 查看副本集状态**

返回包含状态信息的文档。此输出使用从副本集的其他成员发送的心跳包中获得的数据反映副本集的当 前状态

$ rs.status()

1.3.5 添加副本节点以及仲裁节点**

在主节点添加从节点,将其他成员加入到副本集

$ rs.add(host, arbiterOnly)

添加一个仲裁节点到副本集

$ rs.addArb(host)

1.4 副本集的数据读写操作**

副本节点 (SECONDARY) 默认不能 read , 更不可能 write 数据, 需要

$ rs.slaveOk()
myrs:SECONDARY> show dbs; 

"errmsg" : "not master and slaveOk=false",

# 非主节点同时 slaveOk=false 无法读写

数据会自动同步, 但是会有延迟


仲裁者节点, 不存放任何数据 -> rs.slaveOk() 也看不到数据

1.5 主节点的选举原则**

MongoDB在副本集中,会自动进行主节点的选举,主节点选举的触发条件

  1. 主节点故障
  2. 主节点网络不可达 (默认心跳信息为 10 秒)
  3. 人工干预 rs.stepDown(600)

一旦触发选举,就要根据一定规则来选主节点

选举规则是根据票数来决定谁获胜

  • 票数最高,且获得了“大多数”成员的投票支持的节点获胜。

    • “大多数”的定义为:假设复制集内投票成员数量为N,则大多数为 N/2 + 1。例如:3个投票成员, 则大多数的值是2。当复制集内存活成员数量不足大多数时,整个复制集将无法选举出Primary, 复制集将无法提供写服务,处于只读状态。
  • 若票数相同,且都获得了“大多数”成员的投票支持的,数据新的节点获胜。

    • 数据的新旧是通过操作日志 oplog 来对比的。

在获得票数的时候,优先级(priority)参数影响重大。

可以通过设置优先级(priority)来设置额外票数。优先级即权重,取值为0-1000,相当于可额外增加 0-1000的票数,优先级的值越大,就越可能获得多数成员的投票(votes)数。指定较高的值可使成员 更有资格成为主要成员,更低的值可使成员更不符合条件。

默认情况下,优先级的值是 1

1.6 故障测试**

1.6.1 副本节点故障测试**

关闭 27018 副本节点

  • 主节点和仲裁节点对 27018 的心跳失败。因为主节点还在,因此,没有触发投票选举。
  • 如果此时,在主节点写入数据。再启动从节点,会发现,主节点写入的数据会自动同步给从节点

1.6.2 主节点故障测试**

关闭27017节点

  • 从节点和仲裁节点对27017的心跳失败,当失败超过10秒,此时因为没有主节点了,会自动发起投票。
  • 而副本节点只有27018,因此,候选人只有一个就是27018,开始投票。
  • 27019向27018投了一票,27018本身自带一票,因此共两票,超过了“大多数”
  • 27019是仲裁节点,没有选举权,27018不向其投票,其票数是0.

最终结果,27018成为主节点。具备读写功能。 在27018写入数据查看。

1.6.3 仲裁节点和主节点故障**

先关掉仲裁节点27019, 关掉现在的主节点27018 登录27017后

  • 27017仍然是从节点,副本集中没有主节点了,导致此时,副本集是只读状态, 无法写入。

  • 为啥不选举了?

    • 因为27017的票数,没有获得大多数,即没有大于等于2,它只有默认的一票(优先级 是1)

    • 如果要触发选举,随便加入一个成员即可。

      • 如果只加入27019仲裁节点成员,则主节点一定是27017,因为没得选了,仲裁节点不参与选举, 但参与投票
      • 如果只加入27018节点,会发起选举。因为27017和27018都是两票,则按照谁数据新,谁当主节点。

1.6.4 仲裁节点和从节点故障**

先关掉仲裁节点 27019,关掉现在的副本节点 27018

10秒后,27017 主节点自动降级为副本节点。(服务降级)

副本集不可写数据了,已经故障了。

2. 分片集群 - Sharded Cluster**

2.1 分片概念**

分片 (sharding) 是一种跨多台机器分布数据的方法, MongoDB 使用分片来支持具有非常大的数据集和高吞吐量操作的部署。

换句话说:分片 (sharding) 是指将数据拆分,将其分散存在不同的机器上的过程。有时也用分区 (partitioning) 来表示这个概念。将数据分散到不同的机器上,不需要功能强大的大型计算机就可以储存更多的数据,处理更多的负载。

具有大型数据集或高吞吐量应用程序的数据库系统可以会挑战单个服务器的容量。例如,高查询率会耗尽服务器的 CPU 容量。工作集大小大于系统的 RAM 会强调磁盘驱动器的 I/O 容量。

有两种解决系统增长的方法:垂直扩展水平扩展

  • 垂直扩展意味着增加单个服务器的容量,例如使用更强大的CPU,添加更多RAM或增加存储空间量。可 用技术的局限性可能会限制单个机器对于给定工作负载而言足够强大。此外基于云的提供商基于可用的硬件配置具有硬性上限。结果,垂直缩放有实际的最大值。
  • 水平扩展意味着划分系统数据集并加载多个服务器,添加其他服务器以根据需要增加容量。虽然单个机器的总体速度或容量可能不高,但每台机器处理整个工作负载的子集,可能提供比单个高速大容量服务器更高的效率。扩展部署容量只需要根据需要添加额外的服务器,这可能比单个机器的高端硬件的总体 成本更低。权衡是基础架构和部署维护的复杂性增加。

MongoDB 支持通过分片进行水平扩展

2.2 分片集群包含的组件**

MongoDB 分片群集包含以下组件:

  • 分片(存储):每个分片包含分片数据的子集。 每个分片都可以部署为副本集。
  • mongos (路由):mongos充当查询路由器,在客户端应用程序和分片集群之间提供接口。
  • config servers (”调度” 的配置):配置服务器存储群集的元数据和配置设置。 从MongoDB 3.4 开始,必须将配置服务器部署为副本集(CSRS)。

2.3 分片集群架构目标**

两个分片节点副本集(3+3)+ 一个配置节点副本集(3)+ 两个路由节点(2),共 11 个服务节点

副本集的创建详见文档以及视频

添加分片

$ sh.addShard("IP:Port", "IP:Port", "IP:Port")

查看分片状态情况

$ sh.status()

如果添加分片失败,需要先手动移除分片,检查添加分片的信息的正确性后,再次添加分片。 移除分片:

$ use admin
$ db.runCommand( { removeShard: "myshardrs02" } )
  • 如果只剩下最后一个 shard,是无法删除的
  • 移除时会自动转移分片数据,需要一个时间过程
  • 完成后,再次执行删除分片命令才能真正删除

开启分片功能

$ sh.enableSharding("articledb")
$ sh.enableSharding("库名")

$ sh.shardCollection("库名.集合名",{"key":1})

集合分片,使用 sh.shardCollection() 方法指定集合和分片键

$ sh.shardCollection(namespace, key, unique)

对集合进行分片时, 你需要选择一个 片键 (Shard Key) shard key 是每条记录都必须包含的, 且建立了索引的单个字段或复合字段, MongoDB按照片键将数据划分到不同的数据块中,并将数据块均衡地分布到所有分片中. 为了按照片键划分数据块, MongoDB使用基于哈希的分片方式(随机平均分配)或者基于范围的分片方式(数值大小分配) 。

用什么字段当片键都可以,如:nickname作为片键,但一定是必填字段。

分片策略(规则)**

哈希策略**

对于 基于哈希 的分片 , MongoDB计算一个字段的哈希值, 并用这个哈希值来创建数据块.

在使用基于哈希分片的系统中, 拥有”相近”片键的文档很可能不会存储在同一个数据块中, 因此数据的分离性更好一些.

范围策略**

对于 基于范围 的分片 , MongoDB 按照片键的范围把数据分成不同部分. 假设有一个数字的片键 : 想象一个从负无穷到正无穷的直线,每一个片键的值都在直线上画了一个点. MongoDB把这条直线划分为更短的不重叠的片段, 并称之为数据块 ,每个数据块包含了片键在一定范围内的数据.

在使用片键做范围划分的系统中, 拥有”相近”片键的文档很可能存储在同一个数据块中, 因此也会存储在同一个分片中.

基于范围的分片方式与基于哈希的分片方式性能对比**

基于范围的分片方式提供了更高效的范围查询, 给定一个片键的范围,分发路由可以很简单地确定哪个数 据块存储了请求需要的数据,并将请求转发到相应的分片中. 不过, 基于范围的分片会导致数据在不同分片上的不均衡,有时候,带来的消极作用会大于查询性能的积极作用. 比如, 如果片键所在的字段是线性增长的, 一定时间内的所有请求都会落到某个固定的数据块中, 最终导致分布在同一个分片中. 在这种情况下, 一小部分分片承载了集群大部分的数据,系统并不能很好地进行 扩展. 与此相比, 基于哈希的分片方式以范围查询性能的损失为代价, 保证了集群中数据的均衡.哈希值的随机性 使数据随机分布在每个数据块中, 因此也随机分布在不同分片中.但是也正由于随机性, 一个范围查询很难 确定应该请求哪些分片, 通常为了返回需要的结果,需要请求所有分片.

如无特殊情况,一般推荐使用 Hash Sharding. 而使用 _id 作为片键是一个不错的选择,因为它是必有的,你可以使用数据文档 _id 的哈希作为片键。 这个方案能够是的读和写都能够平均分布,并且它能够保证每个文档都有不同的片键所以数据块能够很 精细。 似乎还是不够完美,因为这样的话对多个文档的查询必将命中所有的分片。虽说如此,这也是一种比较 好的方案了。 理想化的 shard key 可以让 documents 均匀地在集群中分布

3. 安全认证**

3.1 MongoDB的用户和角色权限简介**

默认情况下,MongoDB实例启动运行时是没有启用用户访问权限控制的,也就是说,在实例本机服务 器上都可以随意连接到实例进行各种操作,MongoDB不会对连接客户端进行用户验证,这是非常危险 的。

mongodb官网上说,为了能保障mongodb的安全可以做以下几个步骤

  1. 使用新的端口,默认的 27017 端口如果一旦知道了 ip 就能连接上,不太安全
  2. 设置 mongodb 的网络环境,最好将 mongodb 部署到公司服务器内网,这样外网是访问不到的。公 司内部访问使用 vpn 等
  3. 开启安全认证。认证要同时设置服务器之间的内部认证方式,同时要设置客户端连接到集群的账号 密码认证方式。

为了强制开启用户访问控制(用户验证),则需要在MongoDB实例启动时使用选项 –auth 或在指定启动 配置文件中添加选项 auth=true

在开始之前需要了解一下概念

启用访问控制**

  • MongoDB使用的是基于角色的访问控制(Role-Based Access Control,RBAC)来管理用户对实例的访问。 通过对用户授予一个或多个角色来控制用户访问数据库资源的权限和数据库操作的权限,在对用户分配 角色之前,用户无法访问实例
  • 在实例启动时添加选项 –auth 或指定启动配置文件中添加选项 auth=true

角色**

在MongoDB中通过角色对用户授予相应数据库资源的操作权限,每个角色当中的权限可以显式指定, 也可以通过继承其他角色的权限,或者两都都存在的权限。

权限**

权限由指定的数据库资源(resource)以及允许在指定资源上进行的操作(action)组成

  1. 资源(resource)包括:数据库、集合、部分集合和集群
  2. 操作(action)包括:对资源进行的增、删、改、查(CRUD)操作

在角色定义时可以包含一个或多个已存在的角色,新创建的角色会继承包含的角色所有的权限。在同一 个数据库中,新创建角色可以继承其他角色的权限,在 admin 数据库中创建的角色可以继承在其它任意 数据库中角色的权限。

# 查询所有角色权限(仅用户自定义角色)
$ db.runCommand({ rolesInfo: 1 })

# 查询所有角色权限(包含内置角色)
$ db.runCommand({ rolesInfo: 1, showBuiltinRoles: true })


# 查询当前数据库中的某角色的权限
$ db.runCommand({ rolesInfo: "<rolename>" })

# 查询其它数据库中指定的角色权限
$ db.runCommand({ rolesInfo: { role: "<rolename>", db: "<database>" } }

# 查询多个角色权限
$ db.runCommand({
  rolesInfo: [
    "<rolename>",
    {
      role: "<rolename>",
      db: "<database>"
    },
    ...

  ]
})

常用的内置角色:

  • 数据库用户角色:read、readWrite
  • 所有数据库用户角色:readAnyDatabase、readWriteAnyDatabase、 userAdminAnyDatabase、dbAdminAnyDatabase
  • 数据库管理角色: dbAdmin、dbOwner、userAdmin
  • 集群管理角色: clusterAdmin、clusterManager、clusterMonitor、hostManager
  • 备份恢复角色: backup、restore
  • 超级用户角色: root
  • 内部角色: system

自用 非技术讨论