MongoDB数据库使用

1.基本概念

MongoDB简介

  1. BSON数据结构,类似JSON的二进制形势存储
  2. 支持ACID事务,是一个开源的OLTP数据库
  3. 半结构化:在一个集合中,文档所拥有的字段并不需要是相同的,而且也不需要对所用的字段进行声明。

WiredTiger读写模型详解

  • 读缓存:理想情况下,MongoDB可以提供近似内存式的读写性能。WiredTiger引擎实现了数据的二级缓存,第一层是操作系统的页面缓存,第二层则是引擎提供的内部缓存

    • 数据库发起Buffer I/O读操作,由操作系统将磁盘数据页加载到文件系统的页缓存区
    • 引擎层读取页缓存区的数据,进行解压后存放到内部缓存区
    • 在内存中完成匹配查询,将结果返回给应用。
      在这里插入图片描述
  • 写缓冲: 当数据发生写入时,MongoDB并不会立即持久化到磁盘上,而是先在内存中记录这些变更,之后通过CheckPoint机制将变化的数据写入磁盘。

  • 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日志(RedoLog)。该日志在最长不超过100ms之后写入磁盘
    • 数据库每隔60s执行一次CheckPoint操作,此时内存中的修改会真正刷入磁盘。
    • Journal日志的刷新周期可以通过参数storage.journal.commitIntervalMs指定
    • CheckPoint的刷新周期可以调整storage.syncPeriodSecs参数(默认值60s)

2.MongoDB环境搭建

1.单机部署

  • 环境:centos7.X
  • MongoDB官网下载地址https://www.mongodb.com/try/download/community。我这边使用的版本地址https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-rhel70-4.4.29.tgz
  • 解压tar就不多说了,tar -zxvf 就是了
  • 指定一个mongo.conf配置文件进行启动,配置文件内容
systemLog:
 destination: file
 path: /root/mongodb4.4.9/mongodb-data/log/mongodb.log # 日志文件地址
 logAppend: true
storage:
 dbPath: /root/mongodb4.4.9/mongodb-data/data # data directory
 engine: wiredTiger #存储引擎
 wiredTiger:
  engineConfig:
   cacheSizeGB: 0.25 #最好指定下内存占用,mongoDB默认会占用物理机一半内存-1GB,如果不超过256MB则以256MB运行
 journal: #是否启用journal日志
  enabled: true
net:
 bindIp: 0.0.0.0
 port: 27017 # port
processManagement:
 fork: true #后台启动
  • 启动mongodb命令:./bin/mongod -f config/mongo.conf。指定配置文件启动
  • 关闭mongodb服务方式
    • mongod ‐‐port=27017 ‐‐dbpath=/mongodb/data ‐‐shutdown
    • 进入mongo shell。指定use admin进入admin库,然后执行db.shutdownServer()
  • 连接进入mongo shell使用
bin/mongo ‐‐port=27017
bin/mongo localhost:27017

3.文档操作

系统操作命令

show dbs|show databases	#显示数据库列表
use 数据库名	#切换数据库,如果不存在创建数据库
db.dropDatabase()	#删除数据库
show collections | show tables	#显示当前数据库的集合列表
db.集合名.stats()	#查看集合详情
db.集合名.drop()	#删除集合
show users	#显示当前数据库的用户列表
show roles	#显示当前数据库的角色列表
show profile	#显示最近发生的操作
load("xxx.js")	#执行一个JavaScript脚本文件
exit | quit()	#退出当前shell
help	#查看mongodb支持哪些命令
db.help()	#查询当前数据库支持的方法
db.集合名.help()	#显示集合的帮助信息
db.version()	#查看数据库版本

安全认证命令
如何创建管理员账号

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

默认情况下,MongoDB不会启用鉴权,以鉴权模式启动MongoDB

mongod ‐f /mongodb/conf/mongo.conf ‐‐auth

启用鉴权之后,连接MongoDB的相关操作都需要提供身份认证。

mongo --port=27017 ‐u fox ‐p fox ‐‐authenticationDatabase=admin

插入文档

  • insertOne: 支持writeConcern。writeConcern的取值:
    • 0:发起写操作,不关心是否成功;
    • 1~集群最大数据节点数:写操作需要被复制到指定节点数才算成功;
    • majority:写操作需要被复制到大多数节点上才算成功。
  • insertMany:批量写入,也是支持writeConcern
  • insert和save也可以实现单条插入保存或多条插入保存
  • 通过load命令执行js脚本文件插入
db.user.insertOne({name:"李四",age:19},{writeConcern:0})

#批量插入
db.user.insertMany([{name:"李四2",age:29},{name:"李四3",age:34}])

#insert插入
db.user.insert({name:"王五",age:20})

查询文档

  • db.collection.find(query, projection)
    • query指定查询条件
    • projection:指定需要查询的字段,默认返回所有字段。投影时,_id为1的时候,其他字段必须是1;_id是0的时候,其他字段可以是0;如果没有_id字段约束,多个其他字段必须同为0或同为1。
  • findOne查询集合中的第一个文档
  • 以易读的方式查询db.collection.find().pretty()
  • 条件对照表信息
{a:1}	等于
{a:{$ne:1}}	不等于
{a: {$gt: 1}}	大于
{a: {$gte: 1}}	大于等于
{a: {$lt: 1}}	小于
{a: {$lte: 1}}	小于等于
{a: 1, b: 1}或{$and: [{a: 1}, {b: 1}]}	and条件拼接
{$or: [{a: 1}, {b: 1}]}	or条件组合
{a: {$exists: false}}	is null查询
{a: {$in: [1, 2, 3]}}	in查询
  • 排序sort,db.user.find({age:{$gte:20}}).sort({age:-1}),-1降序,1升序
  • 分页查询:skip+limit,skip表示跳过前面的记录数,limit分页大小。
  • 深度分页处理:使用skip进行深度分页需要跳过的记录数量非常多,如果可以通过id来进行优化,每次翻页通过大于前一页的最大id,然后进行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来计算总页数。数据量大会影响查询性能。
  • 正则表达式匹配查询$regex
db.user.find({},{_id:0,"name":0})

#查询大于等于20的
db.user.find({age:{$gte:20}})

#排序
db.user.find({age:{$gte:20}}).sort({age:-1})

#分页
db.user.find().skip(2).limit(2)

#正则匹配查询
db.user.find({name:{$regex:"四"}})
db.user.find({name:/四/})

更新文档

  • update命令更新db.collection.update(query,update,options)
    • query:描述更新的查询条件;
    • update:描述更新的动作及新的内容;
    • options:描述更新的选项:
      • upsert可选,如果不存在update的记录,是否插入新的记录。默认false,不插入
      • multi: 可选,是否按条件查询出的多条记录全部更新。 默认false,只更新找到的第一条记录
      • writeConcern :可选,决定一个写操作落到多少个节点上才算成功
  • 默认情况下,update只会更新第一条匹配的文档,通过指定multi可以更新多条
  • 也可以通过updateOne、updateMany、replaceOne命令更新
  • findAndModify命令,兼容了查询和修改指定文档的功能,findAndModify只能更新单个文档,默认会返回旧文档,可以指定new选项来返回新的文档。
  • 更新操作符
{$set:{field:value}}	指定一个键并更新值,若键不存在则创建
{$unset : {field : 1 }}		删除一个键
{$inc : {field : value } }	对数值类型进行增减
{$rename : {old_field_name :new_field_name } }	修改字段名称
{ $push : {field : value } }	将数值追加到数组中,若数组不存在则会进行初始化
{$pushAll : {field : value_array }}	追加多个值到一个数组字段内
{$pull : {field : _value } }	从数组中删除指定的元素
{$addToSet : {field : value } }	添加元素到数组中,具有排重功能
{$pop : {field : 1 }} 	删除数组的第一个或最后一个元素
#更新单个文档
db.user.update({"_id" : ObjectId("65f95e54c9c880614c490934")},{name:"李四4",age:20})

#更新多个文档,更新多个文档不能使用replate模式的更新,即是不包含更新操作符的更新动作
db.user.update({"name":"李四4"},{$inc:{age:1}},{"multi":true})

#更新并且返回新的文档
db.user.findAndModify({query:{"name":"李四4"},update:{age:24},new:true})

删除文档

  • 使用 remove 删除文档
db.user.remove({age:28})// 删除age 等于28的记录
db.user.remove({age:{$lt:25}}) // 删除age 小于25的记录
db.user.remove( { } ) // 删除所有记录
db.user.remove() //报错
  • remove命令会删除匹配条件的全部文档,如果希望明确限定只删除一个文档,则需要指定justOne参数db.collection.remove(query,justOne)
db.user.remove({name:"张三"},true)
  • 也可以使用 deleteOne() 和 deleteMany()来删除文档。
  • 使用findOneAndDelete命令可以返回被删除的文档信息,还允许定义“删除的顺序”,即按照指定顺序删除找到的第一个文档
#删除以age升序的第一个name为张三的文档
db.user.findOneAndDelete({name:"张三"},{sort:{age:1}})

4.数据类型

日期类型

  • Date(),new Date(),ISODate()三种设置当前时间的方式
db.dates.insert([{data1:Date()},{data2:new Date()},{data3:ISODate()}])

ObjectId生成器

  • MongoDB集合中所有的文档都有一个唯一的_id字段,作为集合的主键。在默认情况下,_id字段使用ObjectId类型,采用16进制编码形式,共12个字节。
  • 4字节表示Unix时间戳(秒)。
  • 5字节表示随机数(机器号+进程号唯一)。
  • 3字节表示计数器(初始化时随机)。

内嵌文档和数组

#插入数据
db.books.insert({
title: "撒哈拉的故事",
author: {
 name:"三毛",
 gender:"女",
 hometown:"重庆"
 }
})

#查询数据
db.books.find({"author.name":"三毛"})

#修改数据
db.books.updateOne({"author.name":"三毛"},{$set:{"author.hometown":"重庆/台湾"}})

数组类型

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

# 会查询到所有的tags
db.books.find({"author.name":"三毛"},{title:1,tags:1})

#利用$slice获取最后一个tag
db.books.find({"author.name":"三毛"},{title:1,tags:{$slice:-1}})

#数组末尾追加元素,可以使用$push操作符
db.books.updateOne({"author.name":"三毛"},{$push:{tags:"猎奇"}})

#$push操作符可以配合其他操作符,一起实现不同的数组修改操作,比如和$each操作符配合可以用于添加多个元素
db.books.updateOne({"author.name":"三毛"},{$push:{tags:{$each:["伤感","想象力"]}}})

#如果加上$slice操作符,那么只会保留经过切片后的元素
db.books.updateOne({"author.name":"三毛"},{$push:{tags:{$each:["伤感","想象力"],$slice:-3}}})

#会查出所有包含伤感的文档
db.books.find({tags:"伤感"})

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

嵌套型的数组
可以用来储存商品的多属性

#一个商品可以同时包含多个维度的属性,比如尺码、颜色、风格等
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:"韩风"}
 ]
}])

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

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

固定集合

  • 固定集合(capped collection)是一种限定大小的集合,其中capped是覆盖、限额的意思。跟普通的集合相比,数据在写入这种集合时遵循FIFO原则。可以将这种集合想象为一个环状的队列,新文档在写入时会被插入队列的末尾,如果队列已满,那么之前的文档就会被新写入的文档所覆盖。通过固定集合的大小,我们可以保证数据库只会存储“限额”的数据,超过该限额的旧数据都会被丢弃。
  • 固定集合在底层使用的是顺序I/O操作,而普通集合使用的是随机I/O
  • 无法动态修改存储的上限
  • 无法删除已有的数据
  • 固定集合不支持分片
#max:指集合的文档数量最大值,这里是10条
#size:指集合的空间占用最大值,这里是4096字节(4KB)
#这两个参数会同时对集合的上限产生影响。也就是说,只要任一条件达到阈值都会认为集合已经写满。其中size是必选的,而max则是可选的。
db.createCollection("logs",{capped:true,size:4096,max:10})

5.聚合操作

单一作用聚合

MongoDB提供 db.collection.estimatedDocumentCount(), db.collection.count(),db.collection.distinct() 这类单一作用的聚合函数。 所有这些操作都聚合来自单个集合的文档。

  • db.collection.estimatedDocumentCount():返回集合或视图中所有文档的计数
  • db.collection.count():返回与find()集合或视图的查询匹配的文档计数 。等同于db.collection.find(query).count()构造
  • db.collection.distinct():在单个集合或视图中查找指定字段的不同值,并在数组中返回结果。
#返回age大于20的不同name的数组
8 db.user.distinct("name",{age:{$gt:20}})
  • :在分片群集上,如果存在孤立文档或正在进行块迁移,则db.collection.count()没有查询谓词可能导致计数不准确。要避免这些情况,请在分片群集上使用db.collection.aggregate()方法。

聚合管道

  • MongoDB 聚合框架(Aggregation Framework)是一个计算框架,可以作用在一个或几个集合上;对集合中的数据进行的一系列运算;将这些数据转化为期望的形式;
  • 管道(Pipeline)和阶段(Stage),整个聚合运算过程称为管道(Pipeline),它是由多个阶段(Stage)组成的
pipeline = [$stage1, $stage2, ...$stageN];
db.collection.aggregate(pipeline, {options})
  • pipelines 一组数据聚合阶段。除 o u t 、 out、 outMerge和$geonear阶段之外,每个阶段都可以在管道中出现多次。
  • options 可选,聚合操作的其他参数。包含:查询计划、是否使用临时文件、游标、最大操作时间、读写策略、强制索引等等

聚合管道操作符

$match 	筛选条件
$project 	投影
$lookup 	左外连接
$sort 	排序
$group 	分组
$skip/$limit 	分页
$unwind 	展开数组
$graphLookup 	图搜索
$facet/$bucket 	分面搜索

聚合表达式

获取字段信息
$<field> : 用 $ 指示字段路径
$<field>.<sub field> : 使用 $ 和 . 来指示内嵌文档的路径

常量表达式
$literal :<value> : 指示常量 <value>

系统变量表达式
$$<variable> 使用 $$ 指示系统变量
$$CURRENT 指示管道中当前操作的文档

数据脚本

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 tagIdx2 = Math.floor(Math.random()*tags.length);
var favCount = Math.floor(Math.random()*100);
var username = "xx00"+Math.floor(Math.random()*10);
var age = 20 + Math.floor(Math.random()*15);
var book = {
title: "book‐"+i,
type: types[typeIdx],
tag: [tags[tagIdx],tags[tagIdx2]],
favCount: favCount,
author: {name:username,age:age}
};
books.push(book)
}
db.books.insertMany(books);

$project
$project投影操作,将原始字段投影成指定名称, 如将集合中的 title 投影成 name。可以灵活控制输出文档的格式,也可以剔除不需要的字段

#更改字段名展示
db.books.aggregate([{$project:{name:"$title"}}])

#控制哪些字段需要展示,为0不展示,1展示
db.books.aggregate([{$project:{name:"$title",_id:0,type:1,author:1}}])

#控制嵌套文档的展示
db.books.aggregate([
{$project:{name:"$title",_id:0,type:1,"author.name":1}}
])
或者
db.books.aggregate([
{$project:{name:"$title",_id:0,type:1,author:{name:1}}}
])

$match
$match用于对文档进行筛选,筛选管道操作和其他管道操作配合时候时,尽量放到开始阶段,这样可以减少后续管道
操作符要操作的文档数,提升效率

db.books.aggregate([
{$match:{type:"technology"}},
{$project:{name:"$title",_id:0,type:1,author:{name:1}}}

$count
计数并返回与查询匹配的结果数

 db.books.aggregate([
{$match:{type:"technology"}},
{$count: "type_count"}
])

$group

  • 按指定的表达式对文档进行分组,并将每个不同分组的文档输出到下一个阶段。输出文档包含一个_id字段,该字段按键包含不同的组
  • 输出文档还可以包含计算字段,该字段保存由$group的_id字段分组的一些accumulator表达式的值。 $group不会输出具体的文档而只是统计信息。
  • $group阶段的内存限制为100M。默认情况下,如果stage超过此限制,$group将产生错误。但是,要允许处理大型数据集,请将allowDiskUse选项设置为true以启用$group操作以写入临时文件。
  • accumulator操作符
$avg 	计算均值
$first 	返回每组第一个文档,如果有排序,按照排序,如果没有按照默认的存储的顺序的第一个文档。
$last 	返回每组最后一个文档,如果有排序,按照排序,如果没有按照默认的存储的顺序的最后个文档。
$max 	根据分组,获取集合中所有文档对应值得最大值。 
$min 	根据分组,获取集合中所有文档对应值得最小值。 
$push 	将指定的表达式的值添加到一个数组中。 
$addToSet 	将表达式的值添加到一个集合中(无重复值,无序)。 
$sum 	计算总和 
$stdDevPop 	返回输入值的总体标准偏差(population standard deviation) 
$stdDevSamp 	返回输入值的样本标准偏差(the sample standard deviation)
#分组null,表示不分组,只用聚合操作
db.books.aggregate([
{$group:{_id:null,count:{$sum:1},pop:{$sum:"$favCount"},avg:{$avg:"$favCount"}}}
])

#统计每个作者的book收藏总数
db.books.aggregate([
{$group:{_id:"$author.name",pop:{$sum:"$favCount"}}}
])

#多个字段分组:统计每个作者的每本book的收藏数
db.books.aggregate([
{$group:{_id:{name:"$author.name",title:"$title"},pop:{$sum:"$favCount"}}}
])

#每个作者的book的type合集
db.books.aggregate([
{$group:{_id:"$author.name",types:{$addToSet:"$type"}}}
])

$unwind
可以将数组拆分为单独的文档,语法如下:

{
$unwind:
{
#要指定字段路径,在字段名称前加上$符并用引号括起来。
path: <field path>,
#可选,一个新字段的名称用于存放元素的数组索引。该名称不能以$开头。
includeArrayIndex: <string>,
#可选,default :false,若为true,如果路径为空,缺少或为空数组,则$unwind输出文档
preserveNullAndEmptyArrays: <boolean>
 } }
#姓名为xx006的作者的book的tag数组拆分为多个文档
db.books.aggregate([
{$match:{"author.name":"xx006"}},
{$unwind:"$tag"}
])

#每个作者的book的tag合集,将数组拆开再合并回去
db.books.aggregate([
{$unwind:"$tag"},
{$group:{_id:"$author.name",types:{$addToSet:"$tag"}}}
])

#使用includeArrayIndex选项来输出数组元素的数组索引
db.books.aggregate([
{$match:{"author.name":"fox"}},
{$unwind:{path:"$tag", includeArrayIndex: "arrayIndex"}}
])

# 使用preserveNullAndEmptyArrays选项在输出中包含缺少size字段,null或空数组的文档
db.books.aggregate([
{$match:{"author.name":"fox"}},
{$unwind:{path:"$tag", preserveNullAndEmptyArrays: true}}
])

$limit
限制传递到管道中下一阶段的文档数

db.books.aggregate([
{$limit : 5 }
])

$skip
跳过进入stage的指定数量的文档,并将其余文档传递到管道中的下一个阶段

db.books.aggregate([
{$skip : 5 }
])

$sort
对所有输入文档进行排序,并按排序顺序将它们返回到管道。1升序,-1降序

db.books.aggregate([
{$sort : {favCount:‐1,title:1}}
])

$lookup
Mongodb 3.2版本新增,主要用来实现多表关联查询。每个输入待处理的文档,经过$lookup 阶段的处理,输出的新文档中会包含一个新生成的数组(可根据需要命名新key )

#语法
db.collection.aggregate([{
$lookup: {
from: "<collection to join>",#等待被join连接的集合
localField: "<field from the input documents>", #源集合的字段,用来关联的字段
foreignField: "<field from the documents of the from collection>",#被join的集合的字段
as: "<output array field>" #为输出文档的新增值命名。如果输入的集合中已存在该值,则会覆盖掉
 }
 })

#示例.通过customerId将两个集合关联,被连接表的信息输出到customerOrder字段上
db.customer.aggregate([
{$lookup: {
from: "order",
localField: "customerId",
foreignField: "customerId",
as: "customerOrder"
}
}
])

MapReduce

MapReduce操作将大量的数据处理工作拆分成多个线程并行处理,然后将结果合并在一起。从MongoDB 5.0开始,map-reduce操作已被弃用。这里就不多做介绍了,一般使用聚合操作会更方便。

6.MongoDB索引

  • 数据结构:B+树。与mysql的B+树区别,叶子节点之间没有指针关联。
  • B+ Tree中的leaf page包含一个页头(page header)、块头(block header)和真正的数据(key/value),其中页头定义了页的类型、页中实际载荷数据的大小、页中记录条数等信息;块头定义了此页的checksum、块在磁盘上的寻址位置等信息。
  • WiredTiger有一个块设备管理的模块,用来为page分配block。如果要定位某一行数据(key/value)的位置,可以先通过block的位置找到此page(相对于文件起始位置的偏移量),再通过page找到行数据的相对位置,最后可以得到行数据相对于文件起始位置的偏移量offsets。

索引的基本操作

创建索引

  • 语法:db.collection.createIndex(keys, options)
    • Key 值为你要创建的索引字段,1 按升序创建索引, -1 按降序创建索引。可以指定多个字段建立联合索引
    • options可选参数列表:
参数类型描述
backgroundBoolean建索引过程会阻塞其它数据库操作,background可指定以后台方式创建索引,即增加 “background” 可选参数。 “background” 默认值为false。
uniqueBoolean建立的索引是否唯一。指定为true创建唯一索引。默认值为false.
namestring索引的名称。如果未指定,MongoDB的通过连接索引的字段名和排序顺序生成一个索引名称。
sparseBoolean对文档中不存在的字段数据不启用索引;这个参数需要特别注意,如果设置为true的话,在索引字段中不会查询出不包含对应字段的文档。默认值为 false.
expireAfterSecondsinteger指定一个以秒为单位的数值,完成 TTL设定,设定集合的生存时间。
vindex version索引的版本号。默认的索引版本取决于mongod创建索引时运行的版本。
weightsdocument索引权重值,数值在 1 到 99,999 之间,表示该索引相对于其他索引字段的得分权重。
default_languagestring对于文本索引,该参数决定了停用词及词干和词器的规则的列表。 默认为英语
language_overridestring对于文本索引,该参数指定了包含在文档中的字段名,语言覆盖默认的language,默认值为 language.
# 创建索引后台执行
db.values.createIndex({open: 1, close: 1}, {background: true})
# 创建唯一索引
db.values.createIndex({title:1},{unique:true})

查看索引

 #查看索引信息
db.books.getIndexes()
#查看索引键
db.books.getIndexKeys()

#查看索引占用空间,is_detail:可选参数,传入除0或false外的任意数据,
#都会显示该集合中每个索引的大小及总大小。如果传入0或false则只显示该集合中所有索引的总大小。默认值为false。
db.collection.totalIndexSize([is_detail])

删除索引

#删除集合指定索引
db.col.dropIndex("索引名称")
#删除集合所有索引 不能删除主键索引
db.col.dropIndexes()

索引类型

单键索引
在某一个特定的字段上建立索引 mongoDB在ID上建立了唯一的单键索引

db.books.createIndex({title:1})

复合索引
复合索引是多个字段组合而成的索引,其性质和单字段索引类似。但不同的是,复合索引中字段的顺序、字段的升降序对查询性能有直接的影响

db.books.createIndex({type:1,favCount:1})

多键索引

  • 在数组的属性上建立索引,称为多键索引
  • 复合索引也可以建立多键索引, 但不支持一个复合索引中同时出现多个数组字段
#创建多键索引,ratings是数组类型
db.inventory.createIndex( { ratings: 1 } )

#复合多键索引,在普通字段item和数组字段ratings上建立索引
db.inventory.createIndex( { item:1,ratings: 1 } )

地理空间索引

  • 地理空间索引(2dsphereindex)就是专门用于实现位置检索的一种特殊索引
  • $near查询操作符,用于实现附近商家的检索,返回数据结果会按距离排序。
  • $geometry操作符用于指定一个GeoJSON格式的地理空间对象,type=Point表示地理坐标点,coordinates则是用户当前所在的经纬度位置;$maxDistance限定了最大距离,单位是米。
#构建数据
db.restaurant.insert({
restaurantId: 0,
restaurantName:"兰州牛肉面",
location : {
type: "Point",
coordinates: [ ‐73.97, 40.77 ]
}
})

#创建索引
db.restaurant.createIndex({location : "2dsphere"})

#查询附近10000米商家信息
db.restaurant.find( {
location:{
$near :{
$geometry :{
type : "Point" ,
coordinates : [ ‐73.88, 40.78 ]
} ,
$maxDistance:10000
}
1}
} )

全文索引

  • MongoDB支持全文检索功能,可通过建立文本索引来实现简易的分词检索。
  • $text操作符可以在有text index的集合上执行文本检索。$text将会使用空格和标点符号作为分隔符对检索字符串进行分词, 并且对检索字符串中所有的分词结果进行一个逻辑上的 OR 操作
  • 没有提供对中文分词的功能
#创建name和description的全文索引
db.stores.createIndex({name: "text", description: "text"})

Hash索引
在索引字段上进行精确匹配,但不支持范围查询,不支持多键hash;

db.users.createIndex({username : 'hashed'})

通配符索引

  • MongoDB的文档模式是动态变化的,而通配符索引可以建立在一些不可预知的字段上,以此实现查询的加速。MongoDB 4.2 引入了通配符索引来支持对未知或任意字段的查询
  • 通配符索引是稀疏的,不索引空字段。因此,通配符索引不能支持查询字段不存在的文档。
#product_attributes的嵌套文档上建立通配符索引
db.products.createIndex( { "product_attributes.$**" : 1 } )

索引属性

唯一索引

  • 唯一性索引对于文档中缺失的字段,会使用null值代替,因此不允许存在多个文档缺失索引字段的情况
  • 对于分片的集合,唯一性约束必须匹配分片规则。换句话说,为了保证全局的唯一性,分片键必须作为唯一性索引的前缀字段
# 创建唯一索引
db.values.createIndex({title:1},{unique:true})
# 复合索引支持唯一性约束
db.values.createIndex({title:1,type:1},{unique:true})
#多键索引支持唯一性约束
db.inventory.createIndex( { ratings: 1 },{unique:true} )

部分索引

  • 部分索引仅对满足指定过滤器表达式的文档进行索引。通过在一个集合中为文档的一个子集建立索引,部分索引具有更低的存储需求和更低的索引创建和维护的性能成本。3.2新版功能。
  • 如果同时指定了partialFilterExpression和唯一约束,那么唯一约束只适用于满足筛选器表达式的文档。
# 为rating>5的文档建立复合索引{ cuisine: 1, name: 1 }
db.restaurants.createIndex(
{ cuisine: 1, name: 1 },
{ partialFilterExpression: { rating: { $gt: 5 } } }
)

# 符合条件,使用索引
db.restaurants.find( { cuisine: "Italian", rating: { $gte: 8 } } )
# 不符合条件,不能使用索引
db.restaurants.find( { cuisine: "Italian" } )

稀疏索引

  • 索引的稀疏属性确保索引只包含具有索引字段的文档的条目,索引将跳过没有索引字段的文档
  • 只对存在字段的文档进行索引(包括字段值为null的文档)
  • 如果稀疏索引会导致查询和排序操作的结果集不完整,MongoDB将不会使用该索引,除非hint()明确指定索引
#创建稀疏索引,不包含score字段的文档不索引
db.scores.createIndex( { score: 1 } , { sparse: true } )

# 使用稀疏索引
db.scores.find( { score: { $lt: 90 } } )

# 即使排序是通过索引字段,MongoDB也不会选择稀疏索引来完成查询,以返回完整的结果
db.scores.find().sort( { score: ‐1 } )

# 要使用稀疏索引,使用hint()显式指定索引
db.scores.find().sort( { score: ‐1 } ).hint( { score: 1 } )

TTL索引

  • 历史数据删除方式
    • 为每个数据记录一个时间戳,应用侧开启一个定时器,按时间戳定期删除过期的数据。删除索引时候空间不会立刻被回收。
    • 数据按日期进行分表,同一天的数据归档到同一张表,同样使用定时器删除过期的表。
    • TTL索引需要声明在一个日期类型的字段中,TTL 索引是特殊的单字段索引,MongoDB 可以使用它在一定时间或特定时钟时间后自动从集合中删除文档。
  • 对集合创建TTL索引之后,MongoDB会在周期性运行的后台线程中对该集合进行检查及数据清理工作。除了数据老化功能,TTL索引具有普通索引的功能,同样可以用于加速数据的查询。
  • TTL 索引不保证过期数据会在过期后立即被删除。文档过期和 MongoDB 从数据库中删除文档的时间之间可能存在延迟。删除过期文档的后台任务每 60 秒运行一次。因此,在文档到期和后台任务运行之间的时间段内,文档可能会保留在集合中。
# 创建 TTL 索引,TTL 值为3600秒
db.eventlog.createIndex( { "lastModifiedDate": 1 }, { expireAfterSeconds:3600 } )

#TTL索引在创建之后,仍然可以对过期时间进行修改。这需要使用collMod命令对索引的定义进行变更
db.runCommand({collMod:"eventlog",index:{keyPattern:{lastModifiedDate:1},expireAfterSeconds:600}})
  • TTL索引使用约束
    • TTL索引只能支持单个字段,并且必须是非_id字段
    • TTL索引不能用于固定集合。
    • TTL索引无法保证及时的数据老化
    • TTL索引对于数据的清理仅仅使用了remove命令,这种方式并不是很高效。因此TTL Monitor在运行期间对系统CPU、磁盘都会造成一定的压力。相比之下,按日期分表的方式操作会更加高效。

隐藏索引

  • 隐藏索引对查询规划器不可见,不能用于支持查询。通过对规划器隐藏索引,用户可以在不实际删除索引的情况下评估删除索引的潜在影响。如果影响是负面的,用户可以取消隐藏索引,而不必重新创建已删除的索引。4.4新版功能。
#创建隐藏索引
db.restaurants.createIndex({ borough: 1 },{ hidden: true });
# 隐藏现有索引
db.restaurants.hideIndex( { borough: 1} );
db.restaurants.hideIndex( "索引名称" )
# 取消隐藏索引
db.restaurants.unhideIndex( { borough: 1} );
db.restaurants.unhideIndex( "索引名称" );

索引使用建议

  • 为每一个查询建立合适的索引
  • 创建合适的复合索引,不要依赖于交叉索引
  • 复合索引字段顺序:匹配条件在前,范围条件在后
  • 尽可能使用覆盖索引
  • .建索引要在后台运行 {background: true}
  • 避免设计过长的数组索引

explain执行计划详解

  • db.collection.find().explain(<verbose>),verbose的可选值
    • queryPlanner:执行计划的详细信息,包括查询计划、集合信息、查询条件、最佳执行计划、查询方式和 MongoDB 服务信息等
    • exectionStats:最佳执行计划的执行情况和被拒绝的计划等信息
    • allPlansExecution:选择并执行最佳执行计划,并返回最佳执行计划和其他执行计划的执行情况

stage状态
执行计划的返回结果中尽量不要出现以下stage:

  • COLLSCAN(全表扫描)
  • SORT(使用sort但是无index)
  • 不合理的SKIP
  • SUBPLA(未用到index的$or)
  • COUNTSCAN(不使用index进行count)
COLLSCAN 	全表扫描
IXSCAN 	索引扫描
FETCH 	根据索引检索指定文档
SHARD_MERGE 	将各个分片返回数据进行合并
SORT	 在内存中进行了排序
LIMIT 	使用limit限制返回数
SKIP 	使用skip进行跳过
IDHACK 	对_id进行查询
SHARDING_FILTER 	通过mongos对分片数据进行查询
COUNTSCAN 	count不使用Index进行count时的stage返回
COUNT_SCAN 	count使用了Index进行count时的stage返回
SUBPLA 	未使用到索引的$or查询的stage返回
TEXT 	使用全文索引进行查询时候的stage返回
PROJECTION 	限定返回字段时候stage的返回

7.MongoDB复制集

  • MongoDB复制集由一组MongoDB实例(进程)组成。包含一个Primary节点和多个Secondary节点,Mongodb Driver(客户端)的所有数据都写入Primary,Secondary从Primary同步写入的数据,以保持复制集内所有成员存储相同的数据集,提供数据的高可用。
  • 复制集的作用
    • 高可用
    • 数据分发,从节点可以承担读压力
    • 读写分离
    • 异地容灾

复制集的模式

PSS模式(官方推荐模式)

  • PSS模式由一个主节点和两个备节点所组成,即Primary+Secondary+Secondary。

PSA模式

  • PSA模式由一个主节点、一个备节点和一个仲裁者节点组成,即Primary+Secondary+Arbiter
  • Arbiter仲裁节点,只做投票,不存数据,不提供读写操作。
    在这里插入图片描述

复制集环境搭建

  • 即使暂时只有一台服务器,也要以单节点模式启动复制集,这样方便后期拓展。
  • 主节点和从节点配置需要一致,为保证高可用从节点可能会升级为主节点
  • 增加复制集节点不提升系统写性能

单机PSS三节点配置文件

#节点1配置
systemLog:
	destination: file
	path: /data/db1/mongod.log # log path
	logAppend: true
storage:
	storage: /data/db1 # data directory
net:
	bindIp: 0.0.0.0
	port: 28017 # port
replication:
	replSetName: rs0	#复制集名称
processManagement:
	fork: true

#另外两个节点也同样配置,修改对应的systemLog.path、storage.storage地址,修改ip和端口

复制集启动

#启动方式1
mongo ‐‐port 28017	#连接mongodb
# 初始化复制集
rs.initiate()
# 将其余成员添加到复制集
rs.add("192.168.65.174:28018")
rs.add("192.168.65.174:28019")

#启动方式2
mongo ‐‐port 28017
# 初始化复制集
rs.initiate({
_id: "rs0",
members: [{
_id: 0,
host: "192.168.65.174:28017"
},{
_id: 1,
host: "192.168.65.174:28018"
},{
_id: 2,
host: "192.168.65.174:28019"
}]
})

Mongo Shell复制集命令

命令描述
rs.add()为复制集新增节点
rs.addArb()为复制集新增一个 arbiter
rs.conf()返回复制集配置信息
rs.freeze()防止当前节点在一段时间内选举成为主节点
rs.help()返回 replica set 的命令帮助
rs.initiate()初始化一个新的复制集
rs.printReplicationInfo()以主节点的视角返回复制的状态报告
rs.printSecondaryReplicationInfo()以从节点的视角返回复制状态报告
rs.reconfig()通过重新应用复制集配置来为复制集更新配置
rs.remove()从复制集中移除一个节点
rs.secondaryOk()为当前的连接设置 从节点可读
rs.status()返回复制集状态信息。
rs.stepDown()让当前的 primary 变为从节点并触发 election
rs.syncFrom()设置复制集节点从哪个节点处同步数据,将会覆盖默认选取逻辑

安全认证
创建用户:在主节点服务器上,启动mongo

use admin
#创建用户
db.createUser( {
user: "fox",
pwd: "fox",
roles: [ { role: "clusterAdmin", db: "admin" } ,
{ role: "userAdminAnyDatabase", db: "admin"},
{ role: "userAdminAnyDatabase", db: "admin"},
{ role: "readWriteAnyDatabase", db: "admin"}]
})

创建keyFile文件,用于集群之间的安全认证。创建keyFile前,需要先停掉复制集中所有主从节点的mongod服务,然后再创建,
否则有可能出现服务启动不了的情况。

#mongo.key采用随机算法生成,用作节点内部通信的密钥文件。
openssl rand ‐base64 756 > /data/mongo.key
#权限必须是600
chmod 600 /data/mongo.key

启动mongod

# 启动mongod
mongod ‐f /data/db1/mongod.conf ‐‐keyFile /data/mongo.key
mongod ‐f /data/db2/mongod.conf ‐‐keyFile /data/mongo.key
mongod ‐f /data/db3/mongod.conf ‐‐keyFile /data/mongo.key

复制集连接方式
通过高可用 Uri 的方式连接 MongoDB,当 Primary 故障切换后,MongoDB Driver 可自动感知并把流量路由到新的 Primary 节点。

#springboot连接配置
spring:
	data:
	mongodb:
	uri:mongodb://fox:fox@192.168.65.174:28017,192.168.65.174:28018,192.168.65.174:2
8019/test?authSource=admin&replicaSet=rs0

复制集成员角色

  • Priority属性:当 Priority 等于 0 时,它不可以被复制集选举为主,Priority 的值越高,则被选举为主的概率更大。可以考虑给性能好的机器配置大一点数值,更有机会被选主。
  • Vote=0属性:不可以参与选举投票,此时该节点的 Priority 也必须为 0,即它也不能被选举为主。由于一个复制集中最多只有7个投票成员,因此多出来的成员则必须将其vote属性值设置为0,即这些成员将无法参与投票。
  • 成员角色
    • Primary:主节点,其接收所有的写请求,然后把修改同步到所有备节点。
    • Secondary:备节点,与主节点保持同样的数据集。当主节点“挂掉”时,参与竞选主节点。分为以下三个不同类型:
      • Hidden = false:正常的只读节点,是否可选为主,是否可投票,取决于 Priority,Vote 的值;
      • Hidden = true:隐藏节点,对客户端不可见, 可以参与选举,但是 Priority 必须为 0,即不能被提升为主。
      • Delayed :延迟节点,必须同时具备隐藏节点和Priority0的特性,会延迟一定的时间(SlaveDelay 配置决定)从上游复制增量,常用于快速回滚场景。
    • Arbiter:仲裁节点,只用于参与选举投票,本身不承载任何数据,只作为投票角色。
#配置隐藏节点
cfg = rs.conf()
cfg.members[1].priority = 0
cfg.members[1].hidden = true
rs.reconfig(cfg)

#配置延时节点,当我们配置一个延时节点的时候,复制过程与该节点的 oplog 都将延时
cfg = rs.conf()
cfg.members[1].priority = 0
cfg.members[1].hidden = true
#延迟1分钟
cfg.members[1].slaveDelay = 60
rs.reconfig(cfg)
#通过rs.printSecondaryReplicationInfo()命令,可以查看到备节点的同步延迟情况

添加投票节点

# 为仲裁节点创建数据目录,存放配置数据。该目录将不保存数据集
mkdir /data/arb
# 启动仲裁节点,指定数据目录和复制集名称
mongod ‐‐port 30000 ‐‐dbpath /data/arb ‐‐replSet rs0
# 进入mongo shell,添加仲裁节点到复制集
rs.addArb("ip:30000")

复制集高可用

  • MongoDB的复制集选举使用Raft算法,选举成功的必要条件是大多数投票节点存活。在具体的实现中,MongoDB对raft协议添加了一些自己的扩展:
    • 支持chainingAllowed链式复制,即备节点不只是从主节点上同步数据,还可以选择一个离自己最近(心跳延时最小)的节点来复制数据。
    • 增加了预投票阶段,即preVote,这主要是用来避免网络分区时产生Term(任期)值激增的问题
    • 支持投票优先级,如果备节点发现自己的优先级比主节点高,则会主动发起投票并尝试成为新的主节点
  • 一个复制集最多可以有50 个成员,但只有 7 个投票成员

自动故障转移

  • 一个影响检测机制的因素是心跳,在复制集组建完成之后,各成员节点会开启定时器,持续向其他成员发起心跳,这里涉及的参数为heartbeatIntervalMillis,即心跳间隔时间,默认值是2s。
  • 另一个重要的因素是选举超时检测,一次心跳检测失败并不会立即触发重新选举。实际上除了心跳,成员节点还会启动一个选举超时检测定时器,该定时器默认以10s的间隔执行,具体可以通过electionTimeoutMillis参数指定
  • 在electionTimeout任务中触发选举必须要满足以下条件:
    • 当前节点是备节点。
    • 当前节点具备选举权限。
    • 在检测周期内仍然没有与主节点心跳成功。

思考:如何优雅的重启复制集?
如果想不丢数据重启复制集,更优雅的打开方式应该是这样的:

  • 逐个重启复制集里所有的Secondary节点
  • 对Primary发送rs.stepDown()命令,等待primary降级为Secondary
  • 重启降级后的Primary

复制集数据同步机制

  • 在复制集架构中,主节点与备节点之间是通过oplog来同步数据的,这里的oplog是一个特殊的固定集合,当主节点上的一个写操作完成后,会向oplog集合写入一条对应的日志,而备节点则通过这个oplog不断拉取到新的日志,在本地进行回放以达到数据同步的目的
  • 什么是oplog
    • MongoDB oplog 是 Local 库下的一个集合,用来保存写操作所产生的增量日志(类似于 MySQL 中 的 Binlog)。
    • 它是一个 Capped Collection(固定集合),即超出配置的最大值后,会自动删除最老的历史数据,MongoDB 针对 oplog 的删除有特殊优化,以提升删除效率。
    • 主节点产生新的 oplog Entry,从节点通过复制 oplog 并应用来保持和主节点的状态一致;

oplog
查看oplog

use local
db.oplog.rs.find().sort({$natural:‐1}).pretty()
  • 每个备节点都分别维护了自己的一个offset,也就是从主节点拉取的最后一条日志的optime,在执行同步时就通过这个optime向主节点的oplog集合发起查询。
  • oplog集合的大小可以通过参数replication.oplogSizeMB设置
  • 幂等性:每一条oplog记录都描述了一次数据的原子性变更,对于oplog来说,必须保证是幂等性的。oplog会将$inc指令转化为$set来保证原子性
  • 幂等性的代价:在执行数组的push操作时,oplog会将整个数组进行$set操作,本来只是插入1个元素,变成整个数组重新全量设置了,导致oplog的写入被放大。
  • 复制延迟:由于oplog集合是有固定大小的,因此存放在里面的oplog随时可能会被新的记录冲掉。如果备节点的复制不够快,就无法跟上主节点的步伐,从而产生复制延迟(replication lag)问题。
  • 数据回滚:由于复制延迟是不可避免的,这意味着主备节点之间的数据无法保持绝对的同步。当复制集中的主节点宕机时,备节点会重新选举成为新的主节点。那么,当旧的主节点重新加入时,必须回滚掉之前的一些“脏日志数据”,以保证数据集与新的主节点一致。
    • 对于写入的业务数据来说,如果已经被复制到了复制集的大多数节点,则可以避免被回滚的风险。
    • 当rollback发生时,MongoDB将把rollback的数据以BSON格式存放到dbpath路径下rollback文件夹中。可以考虑重新写入进去。
  • 同步源选择:MongoDB是允许通过备节点进行复制的,这会发生在以下的情况中:
    • 在settings.chainingAllowed开启的情况下,备节点自动选择一个最近的节点(ping命令时延最小)进行同步。
    cfg = rs.config()
    cfg.settings.chainingAllowed = false
    rs.reconfig(cfg)
    
    • 使用replSetSyncFrom命令临时更改当前节点的同步源,比如在初始化同步时将同步源指向备节点来降低对主节点的影响。
    db.adminCommand( { replSetSyncFrom: "hostname:port" })
    

7.分片集群架构

  • 分片shard是将数据进行水平切分后,存储到不同的服务器节点上
  • 提高集群的存储容量,毕竟单机服务容量比较有限
  • 提高MongoDB服务的写入能立

分片集群组成

在这里插入图片描述

  • 数据分片节点:将数据均衡分配到不同服务器节点,并且采用复制集保证数据节点的高可用
  • 配置服务器Config:配置复制集中保存了整个分片集群中的元数据,其中包含各个集合的分片策略,以及分片的路由表等。也可以做复制集保证高可用。
  • 查询路由(mongos):mongos是分片集群的访问入口,其本身并不持久化数据。在分片集群中可以部署多个mongos以分担客户端请求的压力。
  • 分片集群搭建比较复杂,除非大数据量集项目,很少使用分片集群,一般都是采用复制集就可以。

环境搭建

暂不讲解这块

分片集群的使用

  • 使用分片集群,要先对database开启分片功能
sh.enableSharding("shop")
  • 执行shardCollection命令,对集合执行分片初始化。指定分片键
对shop数据库的product集合,指定productId为分片键,hashed的分片算法,指定chunk数为4
sh.shardCollection("shop.product",{productId:"hashed"},false,{numInitialChunks:4})
  • 查看数据分布
db.product.getShardDistribution()

分片的原理详解

chunk的介绍

  • 数据根据分片键分配到chunk上,而chunk分配到每个分片节点上,每个节点上的chunk数量可能不相同。
  • 集群在操作分片集合时,会根据分片键找到对应的chunk,并向该chunk所在的分片发起操作请求

分片的策略

范围分片

  • 根据范围区间分配到chunk上,范围分片对范围查询比较好,但对数据分布均衡不友好。例如按照日期分片,可能出现某些时间断的数据比较多。

哈希分片

  • 根据分片键计算出hash值,然后按照chunk进行划分。哈希算法保证了随机性,所以文档可以更加离散地分布到多个chunk上。但在执行一些范围查询时,哈希分片并不是高效的。
  • 4.4 以后的版本,可以将单个字段的哈希分片和一个到多个的范围分片键字段来进行组合。之前的版本只支持单个字段hash分片

分片标签

  • MongoDB允许通过为分片添加标签(tag)的方式来控制数据分发。
#指定分片标签,给每个分片指定标签
sh.addShardTag("shard01","oltp")
sh.addShardTag("shard02","oltp")
sh.addShardTag("shard03","olap")

#为集合指定分片标签,指定分片区间范围
sh.addTagRange("main.devices",{shardKey:MinKey},{shardKey:MaxKey},"oltp")
sh.addTagRange("other.systemLogs",{shardKey:MinKey},{shardKey:MaxKey},"olap")

分片键的选择

  • 分片键的基数(cardinality),取值基数越大越有利于扩展。
  • 分片键的取值分布应该尽可能均匀。
  • 业务读写模式,尽可能分散写压力,而读操作尽可能来自一个或少量的分片。
  • 分片键应该能适应大部分的业务操作

分片键(ShardKey)的约束

  • ShardKey 必须是一个索引。非空集合须在 ShardCollection 前创建索引;空集合ShardCollection 自动创建索引
  • 4.4 版本之前:
    • ShardKey 大小不能超过 512 Bytes;
    • 仅支持单字段的哈希分片键;
    • Document 中必须包含 ShardKey;
    • ShardKey 包含的 Field 不可以修改。
  • 4.4 版本之后:
    • ShardKey 大小无限制;
    • 支持复合哈希分片键;
    • Document 中可以不包含 ShardKey,插入时被当 做 Null 处理;
    • 为 ShardKey 添加后缀 refineCollectionShardKey 命令,可以修改 ShardKey包含的 Field;
  • 而在 4.2 版本之前,ShardKey 对应的值不可以修改;4.2 版本之后,如果 ShardKey 为非_ID 字段, 那么可以修改 ShardKey 对应的值。

数据均衡

  • 所有的数据应均匀地分布于不同的chunk上。
  • 每个分片上的chunk数量尽可能是相近的。
  • 在初始化集合时预分配一定数量的chunk(仅适用于哈希分片),可以通过splitAt、moveChunk命令进行手动切分、迁移。
  • 自动均衡:开启MongoDB集群的自动均衡功能。均衡器会在后台对各分片的chunk进行监控,一旦发现了不均衡状态就会自动进行chunk的搬迁以达到均衡。
    • 在没有人工干预的情况下,chunk会持续增长并产生分裂(split),而不断分裂的结果就会出现数量上的不均衡;
    • 在动态增加分片服务器时,也会出现不均衡的情况。自动均衡是开箱即用的,可以极大简化集群的管理工作。

chunk分裂

  • 在默认情况下,一个chunk的大小为64MB,该参数由配置的chunksize参数指定。如果持续地向该chunk写入数据,并导致数据量超过了chunk大小,则MongoDB会自动进行分裂,将该chunk切分为两个相同大小的chunk。
  • chunk分裂是基于分片键进行的,如果分片键的基数太小,则可能因为无法分裂而会出现jumbo chunk(超大块)的问题。
  • jumbo chunk对水平扩展有负面作用,该情况不利于数据的均衡,业务上应尽可能避免。

自动均衡

  • MongoDB的数据均衡器运行于Primary Config Server(配置服务器的主节点)上,而该节点也同时会控制chunk数据的搬迁流程。
  • 均衡器对于数据的“不均衡状态”判定是根据两个分片上的chunk个数差异来进行的。
    • 少于20个,迁移阈值2
    • 20到79个,迁移阈值4
    • 80及以上,迁移阈值8
  • 影响MongoDB均衡速度的配置
    • _secondaryThrottle:用于调整迁移数据写到目标分片的安全级别。如果没有设定,则会使用w:2选项,即至少一个备节点确认写入迁移数据后才算成功。从MongoDB 3.4版本开始,_secondaryThrottle被默认设定为false, chunk迁移不再等待备节点写入确认。
    • _waitForDelete:在chunk迁移完成后,源分片会将不再使用的chunk删除。如果_waitForDelete是true,那么均衡器需要等待chunk同步删除后才进行下一次迁移。该选项默认为false,这意味着对于旧chunk的清理是异步进行的。
    • 并行迁移数量:在早期版本的实现中,均衡器在同一时刻只能有一个chunk迁移任务。从MongoDB 3.4版本开始,允许n个分片的集群同时执行n/2个并发任务。

数据均衡带来的问题

  • 数据均衡会影响性能,在分片间进行数据块的迁移是一个“繁重”的工作,很容易带来磁盘I/O使用率飙升,或业务时延陡增等一些问题。
  • 利用低峰期迁移数据。启用了自动均衡器,同时在每天的凌晨2点到4点运行数据均衡操作
use config
sh.setBalancerState(true)
db.settings.update(
{_id:"balancer"},
{$set:{activeWindow:{start:"02:00",stop:"04:00"}}},
{upsert:true}
)
  • 对分片集合中执行count命令可能会产生不准确的结果。迁移过程中,mongos对所以分片请求,可能出现重复值。
  • 在执行数据库备份的期间,不能进行数据均衡操作,否则会产生不一致的备份数据。在备份操作之前,可以通过如下命令确认均衡器的状态:
    • sh.getBalancerState():查看均衡器是否开启
    • sh.isBalancerRunning():查看均衡器是否正在运行
    • sh.getBalancerWindow():查看当前均衡的窗口设定

8.事务操作

  • 在MongoDB中,对单个文档的操作是原子的,也可以通过内嵌文档来解决事务操作问题
  • MongoDB 虽然已经在 4.2 开始全面支持了多文档事务。对事务的使用原则应该是:能不用尽量不用。

使用方法

try (ClientSession clientSession = client.startSession()) {
clientSession.startTransaction();
collection.insertOne(clientSession, docOne);
collection.insertOne(clientSession, docTwo);
clientSession.commitTransaction();
}

writeConcern

{ w: <value>, j: <boolean>, wtimeout: <number> }
  • writeConcern 决定一个写操作落到多少个节点上才算成功,一般来说配置majority写入大多数节点就可以。
  • 应对重要数据应用 {w: “majority”},普通数据可以应用 {w: 1} 以确保最佳性能。
  • journal=true持久化机制是要求Primary写入持久化了才向客户端确认
  • wtimeout: 写入超时时间,仅w的值大于1时有效。

数据的读取

数据该从哪里读,由readPreference配置来控制

  • primary: 只选择主节点,默认模式;
  • primaryPreferred:优先选择主节点,如果主节点不可用则选择从节点;
  • secondary:只选择从节点;
  • secondaryPreferred:优先选择从节点, 如果从节点不可用则选择主节点;
  • nearest:根据客户端对节点的 Ping 值判断节点的远近,选择从最近的节点读取。

readPreference 配置

#通过 MongoDB 的连接串参数
mongodb://host1:27107,host2:27107,host3:27017/?replicaSet=rs0&readPreference=secondary

#通过 MongoDB 驱动程序 API
MongoCollection.withReadPreference(ReadPreference readPref)

#Mongo Shell
db.collection.find().readPref( "secondary" )

哪些数据是可以读取的,由readConcern配置控制

  • 在 readPreference 选择了指定的节点后,readConcern 决定这个节点上的数据哪些是可读的,类似于关系数据库的隔离级别
    • available:读取所有可用的数据;
    • local:读取所有可用且属于当前分片的数据
    • majority:读取在大多数节点上提交完成的数据;
    • linearizable:可线性化读取文档,仅支持从主节点读;
    • snapshot:读取最近快照中的数据,仅可用于多文档事务
  • 在复制集中 local 和 available 是没有区别的,两者的区别主要体现在分片集上。在数据均衡情况下,数据已经迁移完成,但config的记录信息还未变更,此时available级别下,可以在新节点读取到迁移的数据;local级别读取不到。
  • 从主节点读取数据时默认 readConcern 是 local,从从节点读取数据时默认readConcern 是 available(因为MongoDB <=3.6 不支持对从节点使用 {readConcern: “local”},向前兼容)

readConcern: majority

  • 只读取大多数据节点上都提交了的数据。
  • 使用 {readConcern: “majority”} 可以有效避免脏读

事务隔离级别

  • 事务完成前,事务外的操作对该事务所做的修改不可访问
  • 如果事务内使用 {readConcern: “snapshot”},则可以达到可重复读Repeatable Read
  • 默认情况下MongoDB会为每个事务设置1分钟的超时时间,如果在该时间内没有提交,就会强制将其终止。该超时时间可以通过transactionLifetimeLimitSecond变量设定。

事务写机制

  • MongoDB 的事务错误处理机制不同于关系数据库:
  • 当一个事务开始后,如果事务要修改的文档在事务外部被修改过,则事务修改这个 文档时会触发 Abort 错误,因为此时的修改冲突了。 这种情况下,只需要简单地重做事务就可以了;
  • 如果一个事务已经开始修改一个文档,在事务以外尝试修改同一个文档,则事务以外的修改会等待事务完成才能继续进行。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值