MongoDB

Table of Contents

1 MongoDB简介

MongoDB is a document-oriented NoSQL database.

参考:MongoDB Manual

1.1 MongoDB VS. CouchDB

MongoDB与CouchDB很相似,它们都是文档型存储,数据存储格式都是JSON型的。

MongoDB和CouchDB的一个重要区别:CouchDB是一个MVCC的系统,而MongoDB是一个update-in-place的系统。这二者的区别就是,MongoDB进行写操作时都是即时完成写操作,写操作成功则数据就写成功了;而CouchDB一个支持多版本控制的系统,此类系统通常支持多个结点写,而系统会检测到多个系统的写操作之间的冲突并以一定的算法规则予以解决。

参考:MongoDB与CouchDB 全方位对比

1.2 MongoDB和RDBMS的概念对应关系

MongoDB和RDBMS的概念对应关系及其不同如表 1 所示。

Table 1: MongoDB和RDBMS的概念对应关系及其不同
RDBMS MongoDB Difference
Table Collection In RDBMS, the table contains the columns and rows which are used to store the data whereas, in MongoDB, this same structure is known as a collection. The collection contains documents which in turn contains Fields, which in turn are key-value pairs.
Row Document In RDBMS, the row represents a single, implicitly structured data item in a table. In MongoDB, the data is stored in documents.
Column Field In RDBMS, the column denotes a set of data values. These in MongoDB are known as Fields.
Joins Embedded documents In RDBMS, data is sometimes spread across various tables and in order to show a complete view of all data, a join is sometimes formed across tables to get the data. In MongoDB, the data is normally stored in a single collection, but separated by using Embedded documents. So there is no concept of joins in Mongodb.

1.3 启动服务器

下面命令可启动MongoDB服务器:

$ mongod --config /usr/local/etc/mongod.conf
$ mongod --config --fork /usr/local/etc/mongod.conf   # --fork表示以daemon形式启动

MongoDB服务器默认监听在端口27017,可以通过 --port 参数修改监听端口。

下面是配置文件/usr/local/etc/mongod.conf的一个样例:

$ cat /usr/local/etc/mongod.conf
systemLog:
  destination: file
  path: /usr/local/var/log/mongodb/mongo.log
  logAppend: true
storage:
  dbPath: /usr/local/var/mongodb
net:
  bindIp: 127.0.0.1

1.4 启动客户端

直接运行 mongo 可启动客户端,默认连接本机的27017端口,执行help可以查看在线帮助文档。

$ mongo
MongoDB shell version v4.0.0
connecting to: mongodb://127.0.0.1:27017
MongoDB server version: 4.0.0
> help
	db.help()                    help on db methods
	db.mycoll.help()             help on collection methods
	sh.help()                    sharding helpers
	rs.help()                    replica set helpers
	help admin                   administrative help
	help connect                 connecting to a db help
	help keys                    key shortcuts
	help misc                    misc things to know
	help mr                      mapreduce

	show dbs                     show database names
	show collections             show collections in current database
	show users                   show users in current database
	show profile                 show most recent system.profile entries with time >= 1ms
	show logs                    show the accessible logger names
	show log [name]              prints out the last segment of log in memory, 'global' is default
	use <db_name>                set current database
	db.foo.find()                list objects in collection foo
	db.foo.find( { a : 1 } )     list objects in foo where a == 1
	it                           result of the last line evaluated; use to further iterate
	DBQuery.shellBatchSize = x   set default number of items to display on shell
	exit                         quit the mongo shell

可以使用参数 --host--port 来改变连接的服务器地址。如不带参数直接执行 mongo 相当于执行:

mongo --host 127.0.0.1 --port 27017

2 MongoDB基本操作

2.1 创建、查看、删除数据库

使用 show dbs 可查看系统中有哪些数据库。如:

> show dbs
admin   0.000GB
config  0.000GB
local   0.000GB

使用 use YOUR_DBNAME 可以切换或创建数据为数据库。如:

> use test1
switched to db test1

执行上面命令后,如果数据库test1不存在会创建它,否则切换到数据库test1。

注:使用 show dbs 不会显示刚刚创建的新数据库,你需要往数据库中插入数据后才会显示。

使用 db 可以查看当前数据库。如:

> db
test1

使用 db.dropDatabase() 可以删除当前数据库。如删除test1数据库:

> db.dropDatabase()
{ "dropped" : "test1", "ok" : 1 }

2.2 创建、查看、删除collection (table)

可以使用 db.createCollection 来创建collection。如:

> db.createCollection("people")
{ "ok" : 1 }

一般你不用使用createCollect显式地创建collection,直接插入文档时会自动创建collection。如:

> db.testabc.insert({"name" : "cig01"})    // 如果testabc是不存在的collection,则会先创建它
WriteResult({ "nInserted" : 1 })

使用 show collections (或者 show tables )可以显示当前数据库下有哪些collection。如:

> show collections
people
testabc

使用 db.testabc.drop() 可以删除集合testabc。如:

> db.testabc.drop()
true

2.3 文档基本操作

2.3.1 插入文档

使用 db.COLLECTION_NAME.insert() 可以往当前数据库中集合COLLECTION_NAME下插入一个或多个文档。如插入一个文档:

> db.products.insert( { item: "card", qty: 15 } )
WriteResult({ "nInserted" : 1 })

如果插入的文档数据中没有 _id 字段,则会自动生成一个 _id 字段;如果插入的文档中带有 _id 字段,则用户自己需要确保 _id 字段不重复。

下面是一次插入多个文档的例子:

> db.products.insert([ { item: "pencil", qty: 15 }, { item: "book", qty: 10} ] )
BulkWriteResult({
	"writeErrors" : [ ],
	"writeConcernErrors" : [ ],
	"nInserted" : 2,
	"nUpserted" : 0,
	"nMatched" : 0,
	"nModified" : 0,
	"nRemoved" : 0,
	"upserted" : [ ]
})

使用 db.COLLECTION_NAME.insertOne()db.COLLECTION_NAME.insertMany() 可以往集合中分别插入一个和多个文档,执行成功后它们的返回数据类型和 db.COLLECTION_NAME.insert() 不一样。下面是使用 insertMany() 插入多个文档的实例:

>  db.products.insertMany([ { item: "pencil", qty: 30 }, { item: "book", qty: 40} ] )
{
	"acknowledged" : true,
	"insertedIds" : [
		ObjectId("5b6e7d9490789f89f897931d"),
		ObjectId("5b6e7d9490789f89f897931e")
	]
}

可见, insertMany() 的返回数据确实和 insert() 不一样。

注:在nodejs的MongoDB驱动中 insert() 已经被标记为过时的,推荐使用 insertOne()insertMany()

2.3.2 查询文档

使用 db.COLLECTION_NAME.find() 可以得到集合中的所有文档。如:

> db.products.find()
{ "_id" : ObjectId("5b6e7d6790789f89f897931a"), "item" : "card", "qty" : 15 }
{ "_id" : ObjectId("5b6e7d8c90789f89f897931b"), "item" : "pencil", "qty" : 15 }
{ "_id" : ObjectId("5b6e7d8c90789f89f897931c"), "item" : "book", "qty" : 10 }
{ "_id" : ObjectId("5b6e7d9490789f89f897931d"), "item" : "pencil", "qty" : 30 }
{ "_id" : ObjectId("5b6e7d9490789f89f897931e"), "item" : "book", "qty" : 40 }

通过指定 pretty() 方法可以更好地显示结果。如:

> db.products.find().pretty()
{
	"_id" : ObjectId("5b6e7d6790789f89f897931a"),
	"item" : "card",
	"qty" : 15
}
{
	"_id" : ObjectId("5b6e7d8c90789f89f897931b"),
	"item" : "pencil",
	"qty" : 15
}
{
	"_id" : ObjectId("5b6e7d8c90789f89f897931c"),
	"item" : "book",
	"qty" : 10
}
{
	"_id" : ObjectId("5b6e7d9490789f89f897931d"),
	"item" : "pencil",
	"qty" : 30
}
{
	"_id" : ObjectId("5b6e7d9490789f89f897931e"),
	"item" : "book",
	"qty" : 40
}
2.3.2.1 按条件查询

通过 find() 的参数指定条件,可以查询指定要求的文档,如表 2 所示。

Table 2: MongoDB查询条件
操作 格式 MongoDB范例 RDBMS类似语句
等于 {<key>:<value>} db.collection.find({"field1":"value1"}) WHERE field1 = 'value1'
小于 {<key>:{$lt:<value>}} db.collection.find({"field2":{$lt:30}}) WHERE field2 < 30
小于或等于 {<key>:{$lte:<value>}} db.collection.find({"field2":{$lte:30}}) WHERE field2 <= 30
大于 {<key>:{$gt:<value>}} db.collection.find({"field2":{$gt:30}}) WHERE field2 > 30
大于或等于 {<key>:{$gte:<value>}} db.collection.find({"field2":{$gte:30}}) WHERE field2 >= 30
不等于 {<key>:{$ne:<value>}} db.collection.find({"field2":{$ne:30}}) WHERE field2 != 30

比如,查询item为book的文档:

> db.products.find({item: "book"})
{ "_id" : ObjectId("5b6e7d8c90789f89f897931c"), "item" : "book", "qty" : 10 }
{ "_id" : ObjectId("5b6e7d9490789f89f897931e"), "item" : "book", "qty" : 40 }

又如,查询qty大于等于30的文档:

> db.products.find({qty: {$gte: 30}})
{ "_id" : ObjectId("5b6e7d9490789f89f897931d"), "item" : "pencil", "qty" : 30 }
{ "_id" : ObjectId("5b6e7d9490789f89f897931e"), "item" : "book", "qty" : 40 }
2.3.2.2 组合查询(AND和OR)

find() 方法可以传入多个key,每个key以逗号隔开,即相当于RDBMS中的AND条件。如查询item为book,并且qty大于等于30的文档:

> db.products.find({item: "book", qty: {$gte: 30}})                  // AND实例
{ "_id" : ObjectId("5b6e7d9490789f89f897931e"), "item" : "book", "qty" : 40 }

OR条件语句使用了关键字 $or ,其语法为:

db.collection.find(
   {
      $or: [
         {key1: value1}, {key2:value2}
      ]
   }
)

下面是查询item为book,或者qty大于等于30的文档:

> db.products.find({$or: [{item: "book"}, {qty: {$gte: 30}} ] })     // OR实例
{ "_id" : ObjectId("5b6e7d8c90789f89f897931c"), "item" : "book", "qty" : 10 }
{ "_id" : ObjectId("5b6e7d9490789f89f897931d"), "item" : "pencil", "qty" : 30 }
{ "_id" : ObjectId("5b6e7d9490789f89f897931e"), "item" : "book", "qty" : 40 }
2.3.2.3 过滤某些字段

查询文档时,默认会返回所有字段。

我们可以通过 db.collection.find(query, projection) 的第二个参数 projection 来指定返回或不返回哪些字段。它有两种使用方式。

方式一(称为inclusion模式):设置需要返回的字段为1或者true,那么其它字段都不会返回。

> db.products.find()
{ "_id" : ObjectId("5b6e7d6790789f89f897931a"), "item" : "card", "qty" : 15 }
{ "_id" : ObjectId("5b6e7d8c90789f89f897931b"), "item" : "pencil", "qty" : 15 }
{ "_id" : ObjectId("5b6e7d8c90789f89f897931c"), "item" : "book", "qty" : 10 }
{ "_id" : ObjectId("5b6e7d9490789f89f897931d"), "item" : "pencil", "qty" : 30 }
{ "_id" : ObjectId("5b6e7d9490789f89f897931e"), "item" : "book", "qty" : 40 }
> db.products.find({}, {_id: 1, item: 1} )    // inclusion模式
{ "_id" : ObjectId("5b6e7d6790789f89f897931a"), "item" : "card" }
{ "_id" : ObjectId("5b6e7d8c90789f89f897931b"), "item" : "pencil" }
{ "_id" : ObjectId("5b6e7d8c90789f89f897931c"), "item" : "book" }
{ "_id" : ObjectId("5b6e7d9490789f89f897931d"), "item" : "pencil" }
{ "_id" : ObjectId("5b6e7d9490789f89f897931e"), "item" : "book" }

方式二(称为exclusion模式):设置不需要返回的字段为0或者false,那么其它字段都会返回。

> db.products.find({}, {qty: 0})               // exclusion模式
{ "_id" : ObjectId("5b6e7d6790789f89f897931a"), "item" : "card" }
{ "_id" : ObjectId("5b6e7d8c90789f89f897931b"), "item" : "pencil" }
{ "_id" : ObjectId("5b6e7d8c90789f89f897931c"), "item" : "book" }
{ "_id" : ObjectId("5b6e7d9490789f89f897931d"), "item" : "pencil" }
{ "_id" : ObjectId("5b6e7d9490789f89f897931e"), "item" : "book" }

注:在设置projection参数时,我们要么把想要返回的字段都指定为1(或true);要么把不想要返回的字段都指定为0(或false);我们不能指定部分字段为0,部分字段为1。

2.3.3 更新文档

使用 db.collection.update()db.collection.save() 可以使用更新文档。

update() 的用法如下:

db.collection.update(
   <query>,
   <update>,
   {
     upsert: <boolean>,
     multi: <boolean>,   // 默认只更新找到的第一个文档,如果这个参数为true,则更新所有文档
     writeConcern: <document>,  // 设置抛出异常的级别
     collation: <document>,
     arrayFilters: [ <filterdocument1>, ... ]
   }
)

其中:query用来指定查询条件,而update指定更新的对象和一些更新操作符(如 $set , $inc 等,可参考 https://docs.mongodb.com/manual/reference/operator/update/ )。

例如,更新item为book的文档,把其qty字段设置为100:

> db.products.update({item: "book"}, {$set : {qty: 100}})
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
> db.products.find()
{ "_id" : ObjectId("5b6e7d6790789f89f897931a"), "item" : "card", "qty" : 15 }
{ "_id" : ObjectId("5b6e7d8c90789f89f897931b"), "item" : "pencil", "qty" : 15 }
{ "_id" : ObjectId("5b6e7d8c90789f89f897931c"), "item" : "book", "qty" : 100 }
{ "_id" : ObjectId("5b6e7d9490789f89f897931d"), "item" : "pencil", "qty" : 30 }
{ "_id" : ObjectId("5b6e7d9490789f89f897931e"), "item" : "book", "qty" : 40 }

上面例子中,只会更新第一个找到的文档,要更新所有找到的文档,则需要指定 multitrue ,如:

> db.products.update({item: "book"}, {$set : {qty: 100}}, {multi:true})  // 所有book的qty设置为100
WriteResult({ "nMatched" : 2, "nUpserted" : 0, "nModified" : 1 })
> db.products.update({item: "book"}, {$inc : {qty: 1}}, {multi:true})    // 所有book的qty增加1
WriteResult({ "nMatched" : 2, "nUpserted" : 0, "nModified" : 2 })

save() 方法通过传入的文档来替换已有文档。不指定 _id 字段 save() 方法类似于 insert() 方法;如果指定 _id 字段,则会更新该 _id 的数据。例如,替换 _id 为5b6e7d9490789f89f897931e的文档:

> db.products.save({ "_id" : ObjectId("5b6e7d9490789f89f897931e"), "item" : "book1", "qty" : 100 })
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })

2.3.4 删除文档

使用 db.collection.remove() 可以删除指定文档,用法如下:

db.collection.remove(
   <query>,
   {
     justOne: <boolean>,      // 是否只删除第一个匹配文档,默认为false
     writeConcern: <document>,
     collation: <document>
   }
)

下面是删除item为pencil的文档的实例:

> db.products.find()
{ "_id" : ObjectId("5b6e7d6790789f89f897931a"), "item" : "card", "qty" : 15 }
{ "_id" : ObjectId("5b6e7d8c90789f89f897931b"), "item" : "pencil", "qty" : 15 }
{ "_id" : ObjectId("5b6e7d8c90789f89f897931c"), "item" : "book", "qty" : 101 }
{ "_id" : ObjectId("5b6e7d9490789f89f897931d"), "item" : "pencil", "qty" : 30 }
{ "_id" : ObjectId("5b6e7d9490789f89f897931e"), "item" : "book1", "qty" : 100 }
> db.products.remove({item: "pencil"})      // 删除item为pencil的文档
WriteResult({ "nRemoved" : 2 })
> db.products.find()
{ "_id" : ObjectId("5b6e7d6790789f89f897931a"), "item" : "card", "qty" : 15 }
{ "_id" : ObjectId("5b6e7d8c90789f89f897931c"), "item" : "book", "qty" : 101 }
{ "_id" : ObjectId("5b6e7d9490789f89f897931e"), "item" : "book1", "qty" : 100 }

在执行 remove() 命令前先执行 find() 命令来判断执行的条件是否正确,以避免误删数据。

3 MongoDB集群操作

MongoDB的集群搭建方式主要有三种:主从模式,Replica Set模式(副本集模式),Sharding模式。 其中,主从模式已经被副本集取代,目前副本集模式应用较为广泛,Sharding模式提供了MongoDB的“水平扩展”能力,但配置维护较为复杂。

3.1 Replica Set

A replica set in MongoDB is a group of mongod processes that maintain the same data set. Replica sets provide redundancy and high availability.

一个副本集只能有一个主节点(Primary),可以有多个从节点(Secondaries),还可以有一个仲裁节点(当参与选举的节点无法选出主节点时仲裁节点充当仲裁的作用,仲裁节点不存储数据)。 主节点可以读写,而从节点不能写只能读。 主节点将数据修改操作记录在operation log(简称oplog)中,oplog会自动复制到从节点中,从节点通过应用oplog到自己数据集来实现数据的同步。一般配置奇数个节点,至少三个节点,如图 1 所示。

Sorry, your browser does not support SVG.

Figure 1: MongoDB的Replica Set模式

如果主节点宕机后会进行自动选举,选举出一个从节点为新的主节点,从而实现自动故障转移。

参考:
https://docs.mongodb.com/manual/replication/
https://docs.mongodb.com/manual/tutorial/deploy-replica-set/

3.1.1 第一步、在每个机器上启动MongoDB

后文将介绍如果部署3个节点的MongoDB副本集集群,假设3个节点的域名分别为表 3 所示。

Table 3: 3个节点的域名
Replica Set Member Hostname
Member 0 mongodb0.example.net
Member 1 mongodb1.example.net
Member 2 mongodb2.example.net

在每个机器上启动MongoDB很简单,分别登录这3个机器,带 --replSet <name> 参数启动mongod即可:

$ mongod --replSet "rs0" --fork --config <path-to-config>

说明:这和启动普通的mongod节点没有什么区别,唯一的不同在于需要设置副本集的名字(这个例子中为rs0)。注: 同一副本集群的replSet名称必需相同。 除了在命令行直接指定外,也可以在其配置文件中指定副本集的名字,如:

replication:
   replSetName: "rs0"

3.1.2 第二步、在某一台机制上执行rs.initiate命令

登录3台机器中的任意一台(比如mongodb0.example.net),进入到mongo shell中,执行 rs.initiate 命令,在这个命令的参数中指定副本集名字和其它节点的信息,比如:

rs.initiate( {
   _id : "rs0",
   members: [
      { _id: 0, host: "mongodb0.example.net:27017" },
      { _id: 1, host: "mongodb1.example.net:27017" },
      { _id: 2, host: "mongodb2.example.net:27017" }
   ]
})

3.1.3 第三步、查看各个节点状态

通过前面的步骤,集群的基本配置已经完成了。在mongo shell中执行 rs.status() 可以查看各个节点状态。如:

......
	"members" : [
		{
			"_id" : 0,
			"name" : "mongodb0.example.net:27017",
			"health" : 1,
			"state" : 1,
			"stateStr" : "PRIMARY",
			"uptime" : 1125,
......
        {
			"_id" : 1,
			"name" : "mongodb1.example.net:27018",
			"health" : 1,
			"state" : 2,
			"stateStr" : "SECONDARY",
			"uptime" : 735,
......
		{
			"_id" : 2,
			"name" : "mongodb2.example.net:27019",
			"health" : 1,
			"state" : 2,
			"stateStr" : "SECONDARY",
			"uptime" : 735,

从输出中可知,mongodb0.example.net是主节点,其它两个是从节点。

3.1.4 集群的其它操作

MongoDB还支持其它一些操作,如rs.stepDown():将主节点降级为从节点;rs.add()增加节点;rs.remove()删除节点,等等。可以执行 rs.help() 查看帮助,如:

> rs.help()
	rs.status()                                { replSetGetStatus : 1 } checks repl set status
	rs.initiate()                              { replSetInitiate : null } initiates set with default settings
	rs.initiate(cfg)                           { replSetInitiate : cfg } initiates set with configuration cfg
	rs.conf()                                  get the current configuration object from local.system.replset
	rs.reconfig(cfg)                           updates the configuration of a running replica set with cfg (disconnects)
	rs.add(hostportstr)                        add a new member to the set with default attributes (disconnects)
	rs.add(membercfgobj)                       add a new member to the set with extra attributes (disconnects)
	rs.addArb(hostportstr)                     add a new member which is arbiterOnly:true (disconnects)
	rs.stepDown([stepdownSecs, catchUpSecs])   step down as primary (disconnects)
	rs.syncFrom(hostportstr)                   make a secondary sync from the given member
	rs.freeze(secs)                            make a node ineligible to become primary for the time specified
	rs.remove(hostportstr)                     remove a host from the replica set (disconnects)
	rs.slaveOk()                               allow queries on secondary nodes

	rs.printReplicationInfo()                  check oplog size and time range
	rs.printSlaveReplicationInfo()             check replica set members and replication lag
	db.isMaster()                              check who is primary

3.2 Sharding集群

所谓Sharding就是将同一个集合的不同子集分发存储到不同的机器(shard)上,MongoDB使用Sharding机制来支持超大数据量,将不同的CRUD路由到不同的机器上执行,以提到数据库的吞吐性能。

这里不重点介绍Sharding模式的配置,有兴趣的朋友可参考:https://docs.mongodb.com/manual/sharding/

4 MongoDB Change Streams(可监控数据变化)

Change Streams是MongoDB 3.6中增加的功能。应用程序使用Change Stream可以订阅Collection的变化。Change Stream是基于oplog实现的,所以这个功能只在Replica Set集群或Sharding集群中可用。

参考:https://docs.mongodb.com/manual/changeStreams/

4.1 实例程序

下面是Change Stream的实例程序(假设文件名为test.js):

const MongoClient = require("mongodb").MongoClient;   // npm i mongodb@3

// 假设MongoDB Replica Set集群的名字为rs0,集群服务器为localhost:27017,localhost:27018,localhost:27019
MongoClient.connect("mongodb://localhost:27017,localhost:27018,localhost:27019?replicaSet=rs0")
  .then(client => {
    console.log("Connected correctly to server");
    // specify db and collections
    const db = client.db("testdb1");
    const collection = db.collection("people");

    const changeStream = collection.watch();

    // start listen to changes
    changeStream.on("change", function(change) {
      console.log(change);  // 数据库testdb1中集合people有变化时,这里都会输出
    });
});

上面程序实现了监控数据库testdb1中集合people,一旦有变化就会输出变化内容。

执行上面程序,会输出下面内容后一直等待。

$ node test.js
Connected correctly to server

打开一个mongo shell,我们往数据库testdb1中集合people中插入一条数据,如下:

rs0:PRIMARY> use testdb1
switched to db testdb1
rs0:PRIMARY> db.people.insert({"name" : "cig01"})
WriteResult({ "nInserted" : 1 })

这时,运行前面程序的窗口会显示:

Connected correctly to server
{ _id:
   { _data:
      Binary {
        _bsontype: 'Binary',
        sub_type: 0,
        position: 49,
        buffer:
         <Buffer 82 5b e6 98 87 00 00 00 02 46 64 5f 69 64 00 64 5b e6 98 87 3b 60 bc 76 02 ae 0f 37 00 5a 10 04 6a 58 86 6b f4 11 42 b4 8a f6 3a ec 8e 52 86 7b 04> } },
  operationType: 'insert',
  fullDocument: { _id: 5be698873b60bc7602ae0f37, name: 'cig01' },
  ns: { db: 'testdb1', coll: 'people' },
  documentKey: { _id: 5be698873b60bc7602ae0f37 } }

Author: cig01

Created: <2018-08-05 日 00:00>

Last updated: <2018-11-10 六 16:45>

Creator: Emacs 25.3.1 (Org mode 9.1.4)