MongoDB基础入门到深入(一)安装、文档操作

@[TOC]

系列文章索引

MongoDB基础入门到深入(一)安装、文档操作 MongoDB基础入门到深入(二)聚合高级操作 MongoDB基础入门到深入(三)索引高级操作 MongoDB基础入门到深入(四)复制(副本)集 MongoDB基础入门到深入(五)分片集群 MongoDB基础入门到深入(六)多文档事务 MongoDB基础入门到深入(七)建模、调优 MongoDB基础入门到深入(八)MongoDB整合SpringBoot、Chang Streams

一、MongoDB介绍

1、什么是MongoDB

MongoDB是一个文档数据库(以 JSON 为数据模型),由C++语言编写,旨在为WEB应用提供可扩展的高性能数据存储解决方案。 文档来自于"JSON Document",并非我们一般理解的 PDF,WORD 文档。

MongoDB是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富,最像关系数据库的。它支持的数据结构非常松散,数据格式是BSON,一种类似JSON的二进制形式的存储格式,简称Binary JSON ,和JSON一样支持内嵌的文档对象和数组对象,因此可以存储比较复杂的数据类型。Mongo最大的特点是它支持的查询语言非常强大,其语法有点类似于面向对象的查询语言,几乎可以实现类似关系数据库单表查询的绝大部分功能,而且还支持对数据建立索引。原则上 Oracle 和 MySQL 能做的事情,MongoDB 都能做(包括 ACID 事务)

MongoDB是一个开源OLTP数据库,它灵活的文档模型(JSON)非常适合敏捷式开发、高可用和水平扩展的大数据应用。 OLTP:on-line Transaction Processing,联机(在线)事务处理 OLAP:on-line Analytical Processing,联机(在线)分析处理

MongoDB在数据库总排名第5,仅次于Oracle、MySQL等RDBMS,在NoSQL数据库排名首位。从诞生以来,其项目应用广度、社区活跃指数持续上升。 数据库排名网站:https://db-engines.com/en/ranking

2、MongoDB 版本变迁

3、MongoDB vs 关系型数据库

概念 MongoDB概念与关系型数据库(RDBMS)非常类似: 数据库(database):最外层的概念,可以理解为逻辑上的名称空间,一个数据库包含多个不同名称的集合。 集合(collection):相当于SQL中的表,一个集合可以存放多个不同的文档。 文档(document):一个文档相当于数据表中的一行,由多个不同的字段组成。 字段(field):文档中的一个属性,等同于列(column)。 索引(index):独立的检索式数据结构,与SQL概念一致。 _id:每个文档中都拥有一个唯一的_id字段,相当于SQL中的主键(primary key)。 视图(view):可以看作一种虚拟的(非真实存在的)集合,与SQL中的视图类似。从MongoDB 3.4版本开始提供了视图功能,其通过聚合管道技术实现。 聚合操作($lookup):MongoDB用于实现"类似"表连接(tablejoin)的聚合操作符。 尽管这些概念大多与SQL标准定义类似,但MongoDB与传统RDBMS仍然存在不少差异,包括: 半结构化,在一个集合中,文档所拥有的字段并不需要是相同的,而且也不需要对所用的字段进行声明。因此,MongoDB具有很明显的半结构化特点。除了松散的表结构,文档还可以支持多级的嵌套、数组等灵活的数据类型,非常契合面向对象的编程模型。 弱关系,MongoDB没有外键的约束,也没有非常强大的表连接能力。类似的功能需要使用聚合管道技术来弥补。

4、MongoDB技术优势

MongoDB基于灵活的JSON文档模型,非常适合敏捷式的快速开发。与此同时,其与生俱来的高可用、高水平扩展能力使得它在处理海量、高并发的数据应用时颇具优势。 JSON 结构和对象模型接近,开发代码量低 JSON的动态模型意味着更容易响应新的业务需求 复制集提供99.999%高可用 分片架构支持海量数据和无缝扩容

简单直观:从错综复杂的关系模型到一目了然的对象模型 快速:最简单快速的开发方式 灵活:快速响应业务变化 MongoDB优势:原生的高可用 MongoDB优势:横向扩展能力

5、MongoDB应用场景

从目前阿里云 MongoDB 云数据库上的用户看,MongoDB 的应用已经渗透到各个领域: 游戏场景,使用 MongoDB 存储游戏用户信息,用户的装备、积分等直接以内嵌文档的形式存储,方便查询、更新; 物流场景,使用 MongoDB 存储订单信息,订单状态在运送过程中会不断更新,以MongoDB 内嵌数组的形式来存储,一次查询就能将订单所有的变更读取出来; 社交场景,使用 MongoDB 存储存储用户信息,以及用户发表的朋友圈信息,通过地理位置索引实现附近的人、地点等功能; 物联网场景,使用 MongoDB 存储所有接入的智能设备信息,以及设备汇报的日志信息,并对这些信息进行多维度的分析; 视频直播,使用 MongoDB 存储用户信息、礼物信息等; 大数据应用,使用云数据库MongoDB作为大数据的云存储系统,随时进行数据提取分析,掌握行业动态。|

国内外知名互联网公司都在使用MongoDB:

6、何时用MongoDB

没有某个业务场景必须要使用MongoDB才能解决,但使用MongoDB通常能让你以更低的成本解决问题。如果你不清楚当前业务是否适合使用MongoDB,可以通过做几道选择题来辅助决策。 只要有一项需求满足就可以考虑使用MongoDB,匹配越多,选择MongoDB越合适。

二、MongoDB安装

1、linux安装MongoDB

(1)环境准备 linux系统: centos7 安装MongoDB社区版

(2)下载MongoDB Community Server 下载地址:https://www.mongodb.com/try/download/community

bash 复制代码
#下载MongoDB
wget https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-rhel70-4.4.9.tgz
tar -zxvf mongodb-linux-x86_64-rhel70-4.4.9.tgz

(3)启动MongoDB Server

bash 复制代码
#创建dbpath和logpath
mkdir -p /mongodb/data /mongodb/log  
#进入mongodb目录,启动mongodb服务
bin/mongod --port=27017 --dbpath=/mongodb/data --logpath=/mongodb/log/mongodb.log \
--bind_ip=0.0.0.0 --fork

--dbpath :指定数据文件存放目录 --logpath :指定日志文件,注意是指定文件不是目录 --logappend :使用追加的方式记录日志 --port:指定端口,默认为27017 --bind_ip:默认只监听localhost网卡 --fork: 后台启动 --auth: 开启认证模式

(4)添加环境变量

修改/etc/profile,添加环境变量,方便执行MongoDB命令

bash 复制代码
export MONGODB_HOME=/usr/local/soft/mongodb
PATH=$PATH:$MONGODB_HOME/bin   

然后执行source /etc/profile 重新加载环境变量

(5)利用配置文件启动服务 编辑/mongodb/conf/mongo.conf文件,内容如下:

yml 复制代码
systemLog:
  destination: file
  path: /mongodb/log/mongod.log # log path
  logAppend: true
storage:
  dbPath: /mongodb/data # data directory
  engine: wiredTiger  #存储引擎
  journal:            #是否启用journal日志
    enabled: true
net:
  bindIp: 0.0.0.0
  port: 27017 # port
processManagement:
  fork: true

注意:一定要yaml格式 启动mongod

bash 复制代码
mongod -f /mongodb/conf/mongo.conf

-f 选项表示将使用配置文件启动mongodb

(5)关闭MongoDB服务 方式一:

bash 复制代码
mongod --port=27017 --dbpath=/mongodb/data --shutdown 

方式二:

进入mongo shell

js 复制代码
use admin
db.shutdownServer()

2、Docker安装

bash 复制代码
#拉取mongo镜像
docker pull mongo
#运行mongo镜像
# 默认情况下,Mongo会将wiredTigerCacheSizeGB设置为与主机总内存成比例的值,而不考虑你可能对容器施加的内存限制。
#MONGO_INITDB_ROOT_USERNAME和MONGO_INITDB_ROOT_PASSWORD都存在就会启用身份认证(mongod --auth)
docker run --name mongo-server -p 27017:27017 \
-e MONGO_INITDB_ROOT_USERNAME=root \
-e MONGO_INITDB_ROOT_PASSWORD=root \
-d mongo --wiredTigerCacheSizeGB 1

#进入容器
docker exec -it mongo-server  /bin/bash
#进入Mongo shell
mongo -u root -p root
#创建用户,赋予test库的操作权限
>use test
>db.createUser({user:"dcl",pwd:"123456",roles:["readWrite"]})

3、Mongo shell使用

(1)登录

mongo是MongoDB的交互式JavaScript Shell界面,它为系统管理员提供了强大的界面,并为开发人员提供了直接测试数据库查询和操作的方法。

bash 复制代码
# --port:指定端口,默认为27017
# --host:连接的主机地址,默认127.0.0.1
mongo --port=27017 
mongo localhost:27017
# 指定用户名密码
mongo -u root -p root

(2)JavaScript支持

mongo shell是基于JavaScript语法的,MongoDB使用了SpiderMonkey作为其内部的JavaScript解释器引擎,这是由Mozilla官方提供的JavaScript内核解释器,该解释器也被同样用于大名鼎鼎的Firefox浏览器产品之中。SpiderMonkey对ECMA Script标准兼容性非常好,可以支持ECMA Script 6。可以通过下面的命令检查JavaScript解释器的版本:

(3)mongo shell常用命令

js 复制代码
#查看所有库
show dbs
# 切换到指定数据库,不存在则创建
use test
# 删除当前数据库  
db.dropDatabase()
#查看集合
show collections
#创建集合
db.createCollection("emp")
#删除集合
db.emp.drop()

3、安全认证

(1)创建管理员账号

js 复制代码
# 设置管理员用户名密码需要切换到admin库
use admin  
#创建管理员
db.createUser({user:"cxf",pwd:"cxf",roles:["root"]})
# 查看当前数据库所有用户信息 
show users 
#显示可设置权限
show roles 
#显示所有用户
db.system.users.find()

# 删除用户
db.dropUser("fox")
#删除当前数据库所有用户
db.dropAllUser()

# 用户认证,返回1表示认证成功,需要admin库
> use admin
switched to db admin
> db.auth("root", "root")
1

(2)常用权限

(3)重新赋予用户操作权限

js 复制代码
db.grantRolesToUser( "cxf" , [ 
    { role: "clusterAdmin", db: "admin" } ,
     { role: "userAdminAnyDatabase", db: "admin"},
     { role: "userAdminAnyDatabase", db: "admin"},
     { role: "readWriteAnyDatabase", db: "admin"} 
 ])

(4)创建应用数据库用户

js 复制代码
# 创建应用数据库用户
use appdb
db.createUser({user:"appdb",pwd:"cxf",roles:["dbOwner"]})

# 默认情况下,MongoDB不会启用鉴权,以鉴权模式启动MongoDB
mongod -f /mongodb/conf/mongo.conf --auth

# 启用鉴权之后,连接MongoDB的相关操作都需要提供身份认证。
mongo 192.168.56.10:27017 -u root -p root --authenticationDatabase=admin

4、MongoDB工具

(1)官方GUI工具------COMPASS

MongoDB图形化管理工具(GUI),能够帮助您在不需要知道MongoDB查询语法的前提下,便利地分析和理解您的数据库模式,并且帮助您可视化地构建查询。 下载地址:https://www.mongodb.com/zh-cn/products/compass

(2)GUI工具------ Robo 3T(免费)

下载地址:https://robomongo.org/

(3)GUI工具------Studio 3T(收费,试用30天)

下载地址:https://studio3t.com/download/

(4)MongoDB Database Tools

下载地址:https://www.mongodb.com/try/download/database-tools

三、文档操作

1、集合操作

js 复制代码
# 使用数据库
use appdb
#查看集合
show collections
#创建集合
db.createCollection("user")
#删除集合
db.user.drop()

创建集合语法:db.createCollection(name, options) options参数: 字段|类型|描述 capped|布尔|(可选)如果为true,则创建固定集合。固定集合是指有着固定大小的集合,当达到最大值时,它会自动覆盖最早的文档。 size|数值|(可选)为固定集合指定一个最大值(以字节计)。如果 capped 为 true,也需要指定该字段。 max|数值|(可选)指定固定集合中包含文档的最大数量。

注意: 当集合不存在时,向集合中插入文档也会创建集合

2、插入文档

3.2 版本之后新增了 db.collection.insertOne()db.collection.insertMany()

(1)插入单个文档

insertOne: 支持writeConcern

js 复制代码
db.collection.insertOne(
   <document>,
   {
      writeConcern: <document>
   }
)

writeConcern 决定一个写操作落到多少个节点上才算成功。writeConcern 的取值包括: 0:发起写操作,不关心是否成功; 1~集群最大数据节点数:写操作需要被复制到指定节点数才算成功; majority:写操作需要被复制到大多数节点上才算成功。

insert: 若插入的数据主键已经存在,则会抛 DuplicateKeyException 异常,提示主键重复,不保存当前数据。 save: 如果 _id 主键存在则更新数据,如果不存在就插入数据。

js 复制代码
# 实例
> use appdb
switched to db appdb

> show collections

> db.user.insertOne({x:1})
{
	"acknowledged" : true,
	"insertedId" : ObjectId("6644044abba6afdfd68aacc5")
}

> db.user.insert({x:2})
WriteResult({ "nInserted" : 1 })

> db.user.save({x:3})
WriteResult({ "nInserted" : 1 })

> db.user.find()
{ "_id" : ObjectId("6644044abba6afdfd68aacc5"), "x" : 1 }
{ "_id" : ObjectId("66440457bba6afdfd68aacc6"), "x" : 2 }
{ "_id" : ObjectId("66440464bba6afdfd68aacc7"), "x" : 3 }

(2)批量插入文档

insertMany:向指定集合中插入多条文档数据

js 复制代码
db.collection.insertMany(
   [ <document 1> , <document 2>, ... ],
   {
      writeConcern: <document>,
      ordered: <boolean>      
   }
)

writeConcern:写入策略,默认为 1,即要求确认写操作,0 是不要求。 ordered:指定是否按顺序写入,默认 true,按顺序写入。

insertsave也可以实现批量插入

js 复制代码
# 实例
> db.user.insert([{y:1}, {y:2}])
BulkWriteResult({
	"writeErrors" : [ ],
	"writeConcernErrors" : [ ],
	"nInserted" : 2,
	"nUpserted" : 0,
	"nMatched" : 0,
	"nModified" : 0,
	"nRemoved" : 0,
	"upserted" : [ ]
})
> 
> db.user.save([{y:3}, {y:4, z:4}])
BulkWriteResult({
	"writeErrors" : [ ],
	"writeConcernErrors" : [ ],
	"nInserted" : 2,
	"nUpserted" : 0,
	"nMatched" : 0,
	"nModified" : 0,
	"nRemoved" : 0,
	"upserted" : [ ]
})
> db.user.insertMany([{y:5}, {y:6}])
{
	"acknowledged" : true,
	"insertedIds" : [
		ObjectId("6644064c10b9227f09b58322"),
		ObjectId("6644064c10b9227f09b58323")
	]
}
> db.user.find()
{ "_id" : ObjectId("6644061110b9227f09b5831d"), "y" : 1 }
{ "_id" : ObjectId("6644061110b9227f09b5831e"), "y" : 2 }
{ "_id" : ObjectId("6644063710b9227f09b58320"), "y" : 3 }
{ "_id" : ObjectId("6644063710b9227f09b58321"), "y" : 4, "z" : 4 }
{ "_id" : ObjectId("6644064c10b9227f09b58322"), "y" : 5 }
{ "_id" : ObjectId("6644064c10b9227f09b58323"), "y" : 6 }

(3)使用js批量插入

测试:批量插入50条随机数据 编辑脚本book.js

js 复制代码
var tags = ["nosql","mongodb","document","developer","popular"];
var types = ["technology","sociality","travel","novel","literature"];
var books=[];
for(var i=0;i<50;i++){
    var typeIdx = Math.floor(Math.random()*types.length);
    var tagIdx = Math.floor(Math.random()*tags.length);
    var favCount = Math.floor(Math.random()*100);
    var book = {
        title: "book-"+i,
        type: types[typeIdx],
        tag: tags[tagIdx],
        favCount: favCount,
        author: "xxx"+i
    };
    books.push(book)
}
db.books.insertMany(books);

进入mongo shell,执行

js 复制代码
# 进入mongo shell
mongo -u root -p root
# 执行,返回true执行成功
use appdb
load("book.js")

# 查询
db.books.find()

3、查询文档

(1)简单查询

find 查询集合中的若干文档。语法格式如下:

js 复制代码
db.collection.find(query, projection)

query :可选,使用查询操作符指定查询条件。 projection :可选,使用投影操作符指定返回的键。查询时返回文档中所有键值, 只需省略该参数即可(默认省略)。投影时,_id为1的时候,其他字段必须是1;_id是0的时候,其他字段可以是0;如果没有_id字段约束,多个其他字段必须同为0或同为1

如果查询返回的条目数量较多,mongo shell则会自动实现分批显示。默认情况下每次只显示20条,可以输入it命令读取下一批。

findOne查询集合中的第一个文档。语法格式如下:

js 复制代码
db.collection.findOne(query, projection)

如果你需要以易读的方式来读取数据,可以使用pretty)方法,语法格式如下:

js 复制代码
db.collection.find().pretty()

注意:pretty()方法以格式化的方式来显示所有文档

(2)条件查询

指定条件查询

js 复制代码
#查询带有nosql标签的book文档:
db.books.find({tag:"nosql"})
#按照id查询单个book文档:
db.books.find({_id:ObjectId("61caa09ee0782536660494d9")})
#查询分类为"travel"、收藏数超过60个的book文档:
db.books.find({type:"travel",favCount:{$gt:60}})

查询条件对照表:

查询逻辑对照表: 查询逻辑运算符: $lt: 存在并小于 $lte: 存在并小于等于 $gt: 存在并大于 $gte: 存在并大于等于 $ne: 不存在或存在但不等于 $in: 存在并在指定数组中 $nin: 不存在或不在指定数组中 $or: 匹配两个或多个条件中的一个 $and: 匹配全部条件

(3)正则表达式查询

MongoDB 使用 $regex 操作符来设置匹配字符串的正则表达式。

js 复制代码
#使用正则表达式查找type包含 so 字符串的book
db.books.find({type:{$regex:"so"}})
#或者
db.books.find({type:/so/})

4、排序和分页

(1)指定排序

在 MongoDB 中使用 sort() 方法对数据进行排序

js 复制代码
#指定按收藏数(favCount)降序返回
# 1 为升序排列,而 -1 是用于降序排列
db.books.find({type:"travel"}).sort({favCount:-1})

(2)分页

skip用于指定跳过记录数,limit则用于限定返回结果数量。可以在执行find命令的同时指定skip、limit参数,以此实现分页的功能。比如,假定每页大小为8条,查询第3页的book文档:

js 复制代码
db.books.find().skip(8).limit(4)

处理分页问题 -- 巧分页 数据量大的时候,应该避免使用skip/limit形式的分页。 替代方案:使用查询条件+唯一排序条件; 例如: 第一页:db.posts.find({}).sort({_id: 1}).limit(20); 第二页:db.posts.find({_id: {$gt: <第一页最后一个_id>}}).sort({_id: 1}).limit(20); 第三页:db.posts.find({_id: {$gt: <第二页最后一个_id>}}).sort({_id: 1}).limit(20);

处理分页问题 -- 避免使用 count 尽可能不要计算总页数,特别是数据量大和查询条件不能完整命中索引时。 考虑以下场景:假设集合总共有 1000w 条数据,在没有索引的情况下考虑以下查询:

js 复制代码
db.coll.find({x: 100}).limit(50);
db.coll.count({x: 100}); 

前者只需要遍历前 n 条,直到找到 50 条 x=100 的文档即可结束; 后者需要遍历完 1000w 条找到所有符合要求的文档才能得到结果。 为了计算总页数而进行的 count() 往往是拖慢页面整体加载速度的原因

5、更新文档

(1)update语法

可以用update命令对指定的数据进行更新,命令的格式如下:

js 复制代码
db.collection.update(query,update,options)

query:描述更新的查询条件; update:描述更新的动作及新的内容;

options:描述更新的选项 upsert: 可选,如果不存在update的记录,是否插入新的记录。默认false,不插入 multi: 可选,是否按条件查询出的多条记录全部更新。 默认false,只更新找到的第一条记录 writeConcern :可选,决定一个写操作落到多少个节点上才算成功。

(2)更新操作符

(3)更新单个文档

js 复制代码
db.books.update({_id:ObjectId("61caa09ee0782536660494d9")},{$inc:{favCount:1}})

(4)更新多个文档

默认情况下,update命令只在更新第一个文档之后返回,如果需要更新多个文档,则可以使用multi选项。 将分类为"novel"的文档的增加发布时间(publishedDate)

js 复制代码
# 更新
# multi : 可选,mongodb 默认是false,只更新找到的第一条记录,如果这个参数为true,就把按条件查出来多条记录全部更新
db.books.update({type:"novel"},{$set:{publishedDate:new Date()}},{"multi":true})

update命令的选项配置较多,为了简化使用还可以使用一些快捷命令: updateOne:更新单个文档。 updateMany:更新多个文档。 replaceOne:替换单个文档。

(5)upsert命令

upsert是一种特殊的更新,其表现为如果目标文档不存在,则执行插入命令。

js 复制代码
db.books.update(
    {title:"my book"},
    {$set:{tags:["nosql","mongodb"],type:"none",author:"cxf"}},
    {upsert:true}
)

nMatched、nModified都为0,表示没有文档被匹配及更新,nUpserted=1提示执行了upsert动作

(6)实现replace语义

update命令中的更新描述(update)通常由操作符描述,如果更新描述中不包含任何操作符,那么MongoDB会实现文档的replace语义。

js 复制代码
// 将整个文档 替换为justTitle
db.books.update(
    {title:"my book"},
    {justTitle:"my first book"}
)

(7)findAndModify命令

findAndModify兼容了查询和修改指定文档的功能,findAndModify只能更新单个文档

js 复制代码
//将某个book文档的收藏数(favCount)加1
db.books.findAndModify({
    query:{_id:ObjectId("61caa09ee0782536660494dd")},
    update:{$inc:{favCount:1}}
})

该操作会返回符合查询条件的文档数据,并完成对文档的修改。 默认情况下,findAndModify会返回修改前的"旧"数据。如果希望返回修改后的数据,则可以指定new选项

js 复制代码
db.books.findAndModify({
    query:{_id:ObjectId("61caa09ee0782536660494dd")},
    update:{$inc:{favCount:1}},
    new: true
})

与findAndModify语义相近的命令如下: findOneAndUpdate:更新单个文档并返回更新前(或更新后)的文档。 findOneAndReplace:替换单个文档并返回替换前(或替换后)的文档。

6、删除文档

(1)使用 remove 删除文档

remove 命令需要配合查询条件使用; 匹配查询条件的文档会被删除; 指定一个空文档条件会删除所有文档;

js 复制代码
db.user.remove({age:28})// 删除age 等于28的记录
db.user.remove({age:{$lt:25}})   // 删除age 小于25的记录
db.user.remove( { } ) // 删除所有记录
db.user.remove() //报错

remove命令会删除匹配条件的全部文档,如果希望明确限定只删除一个文档,则需要指定justOne参数,命令格式如下:

js 复制代码
// 删除一个
db.collection.remove(query,justOne)
// 例如:删除满足type:novel条件的首条记录
db.books.remove({type:"novel"},true)

(2)使用 delete 删除文档(推荐)

官方推荐使用 deleteOne()deleteMany() 方法删除文档,语法格式如下:

js 复制代码
db.books.deleteMany ({})  //删除集合下全部文档
db.books.deleteMany ({ type:"novel" })  //删除 type等于 novel 的全部文档
db.books.deleteOne ({ type:"novel" })  //删除 type等于novel 的一个文档

注意: remove、deleteMany等命令需要对查询范围内的文档逐个删除,如果希望删除整个集合,则使用drop命令会更加高效

(3)返回被删除文档(findOneAndDelete)

remove、deleteOne等命令在删除文档后只会返回确认性的信息,如果希望获得被删除的文档,则可以使用findOneAndDelete命令

js 复制代码
db.books.findOneAndDelete({type:"novel"})

除了在结果中返回删除文档,findOneAndDelete命令还允许定义"删除的顺序",即按照指定顺序删除找到的第一个文档

js 复制代码
db.books.findOneAndDelete({type:"novel"},{sort:{favCount:1}})

remove、deleteOne等命令只能按默认顺序删除,利用这个特性,findOneAndDelete可以实现队列的先进先出。

7、最佳实践

关于文档结构 防止使用太长的字段名(浪费空间) 防止使用太深的数组嵌套(超过2层操作比较复杂) 不使用中文,标点符号等非拉丁字母作为字段名

关于写操作 update 语句里只包括需要更新的字段 尽可能使用批量插入来提升写入性能 使用TTL自动过期日志类型的数据

四、MongoDB数据模型

1、BSON协议与数据类型

(1)JSON

JSON是当今非常通用的一种跨语言Web数据交互格式,属于ECMAScript标准规范的一个子集。JSON(JavaScript Object Notation, JS对象简谱)即JavaScript对象表示法,它是JavaScript对象的一种文本表现形式。 作为一种轻量级的数据交换格式,JSON的可读性非常好,而且非常便于系统生成和解析,这些优势也让它逐渐取代了XML标准在Web领域的地位,当今许多流行的Web应用开发框架,如SpringBoot都选择了JSON作为默认的数据编/解码格式。

JSON只定义了6种数据类型: string: 字符串 number : 数值 object: JS的对象形式,用{key:value}表示,可嵌套 array: 数组,JS的表示方式[value],可嵌套 true/false: 布尔类型 null: 空值

大多数情况下,使用JSON作为数据交互格式已经是理想的选择,但是JSON基于文本的解析效率并不是最好的,在某些场景下往往会考虑选择更合适的编/解码格式,一些做法如: 在微服务架构中,使用gRPC(基于Google的Protobuf)可以获得更好的网络利用率。 分布式中间件、数据库,使用私有定制的TCP数据包格式来提供高性能、低延时的计算能力。

(2)BSON

BSON由10gen团队设计并开源,目前主要用于MongoDB数据库。BSON(Binary JSON)是二进制版本的JSON,其在性能方面有更优的表现。BSON在许多方面和JSON保持一致,其同样也支持内嵌的文档对象和数组结构。二者最大的区别在��JSON是基于文本的,而BSON则是二进制(字节流)编/解码的形式。在空间的使用上,BSON相比JSON并没有明显的优势。

MongoDB在文档存储、命令协议上都采用了BSON作为编/解码格式,主要具有如下优势: 类JSON的轻量级语义,支持简单清晰的嵌套、数组层次结构,可以实现模式灵活的文档结构更高效的遍历,BSON在编码时会记录每个元素的长度,可以直接通过seek操作进行元素的内容读取,相对JSON解析来说,遍历速度更快。 更丰富的数据类型,除了JSON的基本数据类型,BSON还提供了MongoDB所需的一些扩展类型,比如日期、二进制数据等,这更加方便数据的表示和操作。

(3)BSON的数据类型

MongoDB中,一个BSON文档最大大小为16M,文档嵌套的级别不超过100 https://docs.mongodb.com/v4.4/reference/bson-types/

(4)$type操作符

$type操作符基于BSON类型来检索集合中匹配的数据类型��并返回结果。

js 复制代码
db.books.find({"title" : {$type : 2}})
//或者
db.books.find({"title" : {$type : "string"}})

2、日期类型

MongoDB的日期类型使用UTC(Coordinated Universal Time)进行存储,也就是+0时区的时间

js 复制代码
db.dates.insert([{data1:Date()},{data2:new Date()},{data3:ISODate()}])
db.dates.find().pretty()

使用new Date与ISODate最终都会生成ISODate类型的字段(对应于UTC时间):

3、ObjectId生成器

MongoDB集合中所有的文档都有一个唯一的_id字段,作为集合的主键。在默认情况下,_id字段使用ObjectId类型,采用16进制编码形式,共12个字节。

为了避免文档的_id字段出现重复,ObjectId被定义为3个部分: 4字节表示Unix时间戳(秒)。 5字节表示随机数(机器号+进程号唯一)。 3字节表示计数器(初始化时随机)。

大多数客户端驱动都会自行生成这个字段,比如MongoDB Java Driver会根据插入的文档是否包含_id字段来自动补充ObjectId对象。这样做不但提高了离散性,还可以降低MongoDB服务器端的计算压力。在ObjectId的组成中,5字节的随机数并没有明确定义,客户端可以采用机器号、进程号来实现:

js 复制代码
// 生成一个新的ObjectID()
x = ObjectId()

4、内嵌文档和数组

(1)内嵌文档

一个文档中可以包含作者的信息,包括作者名称、性别、家乡所在地,一个显著的优点是,当我们查询book文档的信息时,作者的信息也会一并返回。

js 复制代码
// 插入
db.books.insert({
    title: "撒哈拉的故事",
    author: {
        name:"三毛",
        gender:"女",
        hometown:"重庆"
    }
})
// 查询三毛的作品
db.books.find({"author.name":"三毛"})

// 修改三毛的家乡所在地
db.books.updateOne({"author.name":"三毛"},{$set:{"author.hometown":"重庆/台湾"}})

(2)内嵌数组

增加tags标签

js 复制代码
db.books.updateOne({"author.name":"三毛"},{$set:{tags:["旅行","随笔","散文","爱情","文学"]}})

查询数组元素

js 复制代码
# 会查询到所有的tags
db.books.find({"author.name":"三毛"},{title:1,tags:1})
#利用$slice获取最后一个tag
# $silice是一个查询操作符,用于指定数组的切片方式
db.books.find({"author.name":"三毛"},{title:1,tags:{$slice:-1}})

数组末尾追加元素,可以使用$push操作符

js 复制代码
db.books.updateOne({"author.name":"三毛"},{$push:{tags:"猎奇"}})

$push操作符可以配合其他操作符,一起实现不同的数组修改操作,比如和$each操作符配合可以用于添加多个元素

js 复制代码
db.books.updateOne({"author.name":"三毛"},{$push:{tags:{$each:["伤感","想象力"]}}})

如果加上$slice操作符,那么只会保留经过切片后的元素

js 复制代码
db.books.updateOne({"author.name":"三毛"},{$push:{tags:{$each:["伤感","想象力"],$slice:-3}}})

根据元素查询

js 复制代码
#会查出所有包含伤感的文档
db.books.find({tags:"伤感"})
# 会查出所有同时包含"伤感","想象力"的文档
db.books.find({tags:{$all:["伤感","想象力"]}})

(3)嵌套型的数组

数组元素可以是基本类型,也可以是内嵌的文档结构

js 复制代码
{
    tags:[
        {tagKey:xxx,tagValue:xxxx},
        {tagKey:xxx,tagValue:xxxx}
    ]
}

这种结构非常灵活,一个很适合的场景就是商品的多属性表示 一个商品可以同时包含多个维度的属性,比如尺码、颜色、风格等,使用文档可以表示为:

js 复制代码
db.goods.insertMany([{
    name:"羽绒服",
    tags:[
        {tagKey:"size",tagValue:["M","L","XL","XXL","XXXL"]},
        {tagKey:"color",tagValue:["黑色","宝蓝"]},
        {tagKey:"style",tagValue:"韩风"}
    ]
},{
    name:"羊毛衫",
    tags:[
        {tagKey:"size",tagValue:["L","XL","XXL"]},
        {tagKey:"color",tagValue:["蓝色","杏色"]},
        {tagKey:"style",tagValue:"韩风"}
    ]
}])

以上的设计是一种常见的多值属性的做法,当我们需要根据属性进行检索时,需要用到$elementMatch操作符:

js 复制代码
#筛选出color=黑色的商品信息
db.goods.find({
    tags:{
        $elemMatch:{tagKey:"color",tagValue:"黑色"}
    }
})

如果进行组合式的条件检索,则可以使用多个$elemMatch操作符:

js 复制代码
# 筛选出color=蓝色,并且size=XL的商品信息
db.goods.find({
    tags:{
        $all:[
            {$elemMatch:{tagKey:"color",tagValue:"黑色"}},
            {$elemMatch:{tagKey:"size",tagValue:"XL"}}
        ]  
    }
})

5、固定集合

固定集合(capped collection)是一种限定大小的集合,其中capped是覆盖、限额的意思。跟普通的集合相比,数据在写入这种集合时遵循FIFO原则。可以将这种集合想象为一个环状的队列,新文档在写入时会被插入队列的末尾,如果队列已满,那么之前的文档就会被新写入的文档所覆盖。通过固定集合的大小,我们可以保证数据库只会存储"限额"的数据,超过该限额的旧数据都会被丢弃

(1)使用示例

创建固定集合

js 复制代码
db.createCollection("logs",{capped:true,size:4096,max:10})

max:指集合的文档数量最大值,这里是10条 size:指集合的空间占用最大值,这里是4096字节(4KB) 这两个参数会同时对集合的上限产生影响。也就是说,只要任一条件达到阈值都会认为集合已经写满。其中size是必选的,而max则是可选的。 可以使用collection.stats命令查看文档的占用空间

js 复制代码
db.logs.stats()

测试: 尝试在这个集合中插入15条数据,再查询会发现,由于文档数量上限被设定为10条,前面插入的5条数据已经被覆盖了

js 复制代码
for(var i=0;i<15;i++){
    db.logs.insert({t:"row-"+i})
}

(2)优势与限制

固定集合在底层使用的是顺序I/O操作,而普通集合使用的是随机I/O。顺序I/O在磁盘操作上由于寻道次数少而比随机I/O要高效得多,因此固定集合的写入性能是很高的。此外,如果按写入顺序进行数据读取,也会获得非常好的性能表现

但它也存在一些限制,主要有如下5个方面: 1、无法动态修改存储的上限,如果需要修改max或size,则只能先执行collection.drop命令,将集合删除后再重新创建。 2、无法删除已有的数据,对固定集合中的数据进行删除将会得到如下错误: 3、对已有数据进行修改,新文档大小必须与原来的文档大小一致,否则不允许更新: 4、默认情况下,固定集合只有一个_id索引,而且最好是按数据写入的顺序进行读取。当然,也可以添加新的索引,但这会降低数据写入的性能。 5、固定集合不支持分片,同时,在MongoDB 4.2版本中规定了事务中也无法对固定集合执行写操作

(3)适用场景

固定集合很适合用来存储一些"临时态"的数据。"临时态"意味着数据在一定程度上可以被丢弃。同时,用户还应该更关注最新的数据,随着时间的推移,数据的重要性逐渐降低,直至被淘汰处理。

一些适用的场景如下: 系统日志,这非常符合固定集合的特征,而日志系统通常也只需要一个固定的空间来存放日志。在MongoDB内部,副本集的同步日志(oplog)就使用了固定集合。 存储少量文档,如最新发布的TopN条文章信息。得益于内部缓存的作用,对于这种少量文档的查询是非常高效的。

(4)使用固定集合实现FIFO队列

在股票实时系统中,大家往往最关心股票价格的变动。而应用系统中也需要根据这些实时的变化数据来分析当前的行情。倘若将股票的价格变化看作是一个事件,而股票交易所则是价格变动事件的"发布者",股票APP、应用系统则是事件的"消费者"。这样,我们就可以将股票价格的发布、通知抽象为一种数据的消费行为,此时往往需要一个消息队列来实现该需求。

结合业务场景: 利用固定集合实现存储股票价格变动信息的消息队列

1、创建stock_queue消息队列,其可以容纳10MB的数据

js 复制代码
db.createCollection("stock_queue",{capped:true,size:10485760})

2、定义消息格式

js 复制代码
{
    timestamped:new Date(),
    stock: "MongoDB Inc",
    price: 20.33
}

timestamp指股票动态消息的产生时间。 stock指股票的名称。 price指股票的价格,是一个Double类型的字段。

为了能支持按时间条件进行快速的检索,比如查询某个时间点之后的数据,可以为timestamp添加索引

js 复制代码
db.stock_queue.createIndex({timestamped:1})

3、构建生产者,发布股票动态

模拟股票的实时变动

js 复制代码
function pushEvent(){
    while(true){
        db.stock_queue.insert({
            timestamped:new Date(),
            stock: "MongoDB Inc",
            price: 100*Math.random(1000)
        });
        print("publish stock changed");
        sleep(1000);
    }
}

执行pushEvent函数,此时客户端会每隔1秒向stock_queue中写入一条股票信息:

js 复制代码
pushEvent()

4、构建消费者,监听股票动态 对于消费方来说,更关心的是最新数据,同时还应该保持持续进行"拉取",以便知晓实时发生的变化。根据这样的逻辑,可以实现一个listen函数

js 复制代码
function listen(){
    var cursor = db.stock_queue.find({timestamped:{$gte:new Date()}}).tailable();
    while(true){
        if(cursor.hasNext()){
                print(JSON.stringify(cursor.next(),null,2));
        }
        sleep(1000);
    }
}

find操作的查询条件被指定为仅查询比当前时间更新的数据,而由于采用了读取游标的方式,因此游标在获取不到数据时并不会被关闭,这种行为非常类似于Linux中的tail-f命令。在一个循环中会定时检查是否有新的数据产生,一旦发现新的数据(cursor.hasNext()=true),则直接将数据打印到控制台。

执行这个监听函数,就可以看到实时发布的股票信息

js 复制代码
listen()

五、WiredTiger读写模型详解

1、WiredTiger介绍

MongoDB从3.0开始引入可插拔存储引擎的概念。目前主要有MMAPV1、WiredTiger存储引擎可供选择。在3.22源的消耗,节省约60%以上的硬盘资源;

(1)WiredTiger读写模型 - 读缓存

理想情况下,MongoDB可以提供近似内存式的读写性能。WiredTiger引擎实现了数据的二级缓存,第一层是操作系统的页面缓存,第二层则是引擎提供的内部缓存。 读取数据时的流程如下: 数据库发起Buffer I/O读操作,由操作系统将磁盘数据页加载到文件系统的页缓存区。 引擎层读取页缓存区的数据,进行解压后存放到内部缓存区。 在内存中完成匹配查询,将结果返回给应用。

MongoDB为了尽可能保证业务查询的"热数据"能快速被访问,其内部缓存的默认大小达到了内存的一半,该值由wiredTigerCacheSize参数指定,其默认的计算公式如下:

js 复制代码
wiredTigerCacheSize=Math.max(0.5*(RAM-1GB),256MB)

(2)WiredTiger读写模型 - 写缓冲

当数据发生写入时,MongoDB并不会立即持久化到磁盘上,而是先在内存中记录这些变更,之后通过CheckPoint机制将变化的数据写入磁盘。为什么要这么处理?主要有以下两个原因: 如果每次写入都触发一次磁盘I/O,那么开销太大,而且响应时延会比较大。 多个变更的写入可以尽可能进行I/O合并,降低资源负荷。

思考:MongoDB会丢数据吗? MongoDB单机下保证数据可靠性的机制包括以下两个部分:

CheckPoint(检查点)机制 快照(snapshot)描述了某一时刻(point-in-time)数据在内存中的一致性视图,而这种数据的一致性是WiredTiger通过MVCC(多版本并发控制)实现的。当建立CheckPoint时,WiredTiger会在内存中建立所有数据的一致性快照,并将该快照覆盖的所有数据变化一并进行持久化(fsync)。成功之后,内存中数据的修改才得以真正保存。默认情况下,MongoDB每60s建立一次CheckPoint,在检查点写入过程中,上一个检查点仍然是可用的。这样可以保证一旦出错,MongoDB仍然能恢复到上一个检查点。

Journal日志 Journal是一种预写式日志(write ahead log)机制,主要用来弥补CheckPoint机制的不足。如果开启了Journal日志,那么WiredTiger会将每个写操作的redo日志写入Journal缓冲区,该缓冲区会频繁地将日志持久化到磁盘上。默认情况下,Journal缓冲区每100ms执行一次持久化。此外,Journal日志达到100MB,或是应用程序指定journal:true,写操作都会触发日志的持久化。一旦MongoDB发生宕机,重启程序时会先恢复到上一个检查点,然后根据Journal日志恢复增量的变化。由于Journal日志持久化的间隔非常短,数据能得到更高的保障,如果按照当前版本的默认配置,则其在断电情况下最多会丢失100ms的写入数据。

WiredTiger写入数据的流程: 应用向MongoDB写入数据(插入、修改或删除)。 数据库从内部缓存中获取当前记录所在的页块,如果不存在则会从磁盘中加载(Buffer I/O) WiredTiger开始执行写事务,修改的数据写入页块的一个更新记录表,此时原来的记录仍然保持不变。 如果开启了Journal日志,则在写数据的同时会写入一条Journal日志(Redo Log)。该日志在最长不超过100ms之后写入磁盘 数据库每隔60s执行一次CheckPoint操作,此时内存中的修改会真正刷入磁盘。

Journal日志的刷新周期可以通过参数storage.journal.commitIntervalMs指定,MongoDB 3.4及以下版本的默认值是50ms,而3.6版本之后调整到了100ms。由于Journal日志采用的是顺序I/O写操作,频繁地写入对磁盘的影响并不是很大。

CheckPoint的刷新周期可以调整storage.syncPeriodSecs参数(默认值60s),在MongoDB 3.4及以下版本中,当Journal日志达到2GB时同样会触发CheckPoint行为。如果应用存在大量随机写入,则CheckPoint可能会造成磁盘I/O的抖动。在磁盘性能不足的情况下,问题会更加显著,此时适当缩短CheckPoint周期可以让写入平滑一些。

相关推荐
程序员大金20 分钟前
基于SpringBoot+Vue+MySQL的垃圾分类回收管理系统
java·vue.js·spring boot·后端·mysql·mybatis
coding侠客27 分钟前
Spring Boot 注解探秘:常用配置值读取注解的魔力
java·spring boot·后端·spring·spring cloud
计算机学姐28 分钟前
基于Python的可视化在线学习系统
开发语言·vue.js·后端·python·学习·mysql·django
计算机学姐30 分钟前
基于Python的电影票房数据分析系统
开发语言·vue.js·hive·后端·python·spark·django
程序员大金1 小时前
基于SpringBoot+Vue+MySQL的影院购票系统
java·vue.js·spring boot·后端·mysql·mybatis
u0112900643 小时前
Go语言变量的声明
开发语言·后端·golang
jingling5554 小时前
后端开发刷题 | 把数字翻译成字符串(动态规划)
java·数据结构·后端·算法·动态规划
一粟1024 小时前
Spring Boot:现代化Java应用开发的艺术
java·spring boot·后端
码农小伙4 小时前
Spring-bean的生命周期-尾篇
java·后端·spring
百炼成神 LV@菜哥6 小时前
如何使用Filter(过滤器二)
java·后端