MongoDB 基础操作
1. MongoDB 相关概念
1.1 简介
1、MongoDB是一个开源、 高性能、 无模式(没有具体的列,不需要像关系型数据库一样创建表的时候将列创建出来)的文档型数据库,是NoSQL数据库。
2、它使用 bson 格式存储数据(二进制的 json),类似于 json 格式,它既可以存储比较复杂的数据类型,又相当的灵活。
3、MongoDB中的记录是一个文档,它由键值对存储数据,一个文档可以认为是一个对象,键的数据类型为字符型。
1.2 业务场景
1、传统的关系型数据库 (比如 MySQL), 在数据操作的 ”三高” 需求以及对应的 Web 2.0 网站需求面前, 会有”力不从心”的感觉。
2、在互联网快速发展的阶段要求我们的数据库满足以下几点需求:
- High Performance: 高并发,数据库要能够抗住高并发的读写需求。(双十一淘宝网上同一时刻有上亿个并发,对数据库的压力很大)
- High Storage: 海量数据,要能够对海量数据进行高效存储和访问。(朋友圈、微信,一天产生的数据是pb甚至是tb级别的)
- High Scalability:高可扩展性。(mysql使用时数据库的表与列固定,难以扩展。MongoDB 结构松散,不需要提前确定列,扩展性好)
- High Available: 高可用性。(搭建集群,一个节点宕机了,其他节点能够立刻替补上来提供正常的服务)
3、MongoDB 的适用场景如下:
1)社交场景, 使用 MongoDB 存储存储用户信息, 以及用户发表的朋友圈信息, 通过地理位置索引实现附近的人, 地点等功能.
2)游戏场景, 使用 MongoDB 存储游戏用户信息, 用户的装备, 积分等直接以内嵌文档的形式存储, 方便查询, 高效率存储和访问.
3)物流场景, 使用 MongoDB 存储订单信息, 订单状态在运送过程中会不断更新, 以 MongoDB 内嵌数组的形式来存储, 一次查询就能将订单所有的变更读取出来.
4)物联网场景, 使用 MongoDB 存储所有接入的智能设备信息, 以及设备汇报的日志信息, 并对这些信息进行多维度的分析.
5)视频直播, 使用 MongoDB 存储用户信息, 点赞互动信息等.
- 我们发现,这些场景中对于数据操作方面的特点为:
1)数据量大。
2)写入操作频繁。
3)对事务性支持不好,像银行转账这种强事务性的业务场景不能使用。
- 所以,MongoDB 适用于:
1)应用不需要事务及复杂 JOIN 支持(对事务性要求不高)
2)新应用, 需求会变, 数据模型无法确定, 想快速迭代开发(高可扩)
3)应用需要 2000 - 3000 以上的读写QPS(高性能)
4)应用需要 TB 甚至 PB 级别数据存储(海量存储)
5)应用发展迅速, 需要能快速水平扩展(高可扩)
6)应用要求存储的数据不丢失
7)应用需要 99.999% 高可用(高可用)
8)应用需要大量的地理位置查询, 文本查询(高性能)
1.3 体系结构
1、将 MongoDB 与 Mysql 进行对比:
MySQL 概念 | MongoDB 概念 | 解释 |
---|---|---|
database | database | 数据库 |
table | collection | 数据库表 |
row | document | 数据库表中的数据 |
column | field | 数据库表的字段 |
index | index | 数据库索引 |
table joins | - | 表连接,MongoDB 不支持 |
- | 嵌入文档 | MongoDB 使用嵌入文档来替代多表连接 |
primary key | primary key | 主键,MongoDB 会自动生成 _id 字段作为主键 |
2、bson 支持的数据类型:
字符串、对象id(文档的12字节的唯一ID)、布尔值、数组、64位浮点数(是唯一支持的数字类型,整数会默认转成64位浮点数)、null、undefined、正则、javascript 代码。
小提示:对于整数型,可以使用 NumberInt 函数或者 NumberIong 函数进行定义。
{"x": NumberInt("3")}
2. 安装部署 MongoDB
1、官方下载 mongodb 安装包,下载地址: https://www.mongodb.com/try/download
注意:MongoDB分社区版和企业版,社区版在所有环境下都免费,企业版在开发环境免费在生产环境收费。
- 我们选择社区版本,下载对应的版本。平台:RedHat / CentOS 7.0 , 安装包:tgz
- 在MongoDB版本中,第二位是偶数表示正式版,如3.2.x、3.4.x、3.6.x 稳定适合生产环境,第二位是奇数表示为开发版,如 3.1.x、3.3.x、3.5.x 不稳定
2、上传到服务器的 opt 目录下,解压,移动文件到 /usr/local 目录下
tar -zxvf mongodb-linux-x86_64-rhel70-5.0.2.tgz
mv mongodb-linux-x86_64-rhel70-5.0.2 /usr/local/mongodb-5.0.2
3、在解压目录下手动建立一个目录用于存放数据文件,例如 data/db
mkdir -p data/db
mkdir -p data/log
4、启动方式一:进入bin目录,使用命令行参数方式启动服务,默认端口27017。
./mongod --dbpath=/usr/local/mongodb-5.0.2/data/db --logpath=/usr/local/mongodb-5.0.2/data/log/mongodb.log --logappend --port=27017 --fork
5、启动方式二:配置文件方式启动服务
在解压目录下新建 config 文件夹,创建配置文件 mongod.conf,配置内容如下:
#日志文件位置
logpath=/usr/local/mongodb-5.0.2/data/log/mongodb.log
# 以追加方式写入日志
logappend=true
# 是否以守护进程方式运行
fork = true
# 默认27017
port = 27017
# 数据库文件位置
dbpath=/usr/local/mongodb-5.0.2/data/db
# 开启远程连接,所有主机都可以访问
bind_ip=0.0.0.0
启动方式:
./mongod -f ../config/mongod.conf 或者 ./mongod --config ../config/mongod.conf
停止方式:
./mongod -f ../config/mongod.conf --shutdown
6、使用 navicat 连接测试
若连接不成功,报超时的错误
1、检查服务器的防火墙是否开放端口 。
2、检查配置文件中是否开启远程连接 bind_ip=0.0.0.0
3. 基本常用命令
3.1 数据库的操作
3.1.1 选择和创建数据库
1、语法:use 数据库名称
,如果数据库不存在就自动创建,存在就选中。
2、执行语句:use articledb
,创建并选择 articledb 数据库。
3、我们可以使用 show dbs
或者 show databases
查看所有的数据库。但我们发现 articledb 数据库不存在!
注意:在 MongoDB 中,数据库只有在创建集合后才会被持久化到磁盘,不然只会存在于内存中。
MongoDB 安装完后会自带三个数据库:
1)admin:从权限的角度来看,这是 “root” 库。如果将一个用户添加到这个数据库,那么这个用户会自动获得所有数据库的权限。
2)local:部署集群时其他库会相互复制,但如果数据存放在local里面,数据永远不会被复制。
3)config:当MongoDB 用于分片设置时,config数据库在内部使用,用于保存分片相关的信息。
4、使用 db
命令可以查看当前选择的数据库。
3.1.2 数据库的删除
1、语法:db.dropDatabase()
,发现是一个 js 的语法,db为当前库对象,dropDatabase为删除方法。
主要用于删除已经持久化的数据库,临时存储在内存中的数据库不需要删除。
3.2 集合的操作
3.2.1 集合的显式创建
1、语法:db.createCollection(name)
,name为要创建的集合的名称。
2、执行语句:db.createCollection("comment")
,创建 comment 文章评论集合。
此时再次执行 show dbs 就可以发现 articledb 创建了,持久化到了磁盘。
3、我们可以用 show collections
或者 show tables
查看数据库中的所有集合。
3.2.2 集合的隐式创建
1、当向一个集合中插入一个文档,如何集合不存在,就会自动创建集合。文档的插入之后会介绍。
通常我们使用隐式创建集合。
3.2.3 集合的删除
1、语法:db.集合名.drop()
。
2、执行语句:db.comment.drop()
,可以删除 comment 文章评论集合。
删完集合后,如果之前持久化到磁盘的数据库没有一个集合,那么这个数据库也会不存在。
3.3 文档的操作
3.3.1 文档的插入
1)单条插入
1、语法:db.集合名.insert( {"xx": ... } )
,集合若没有会隐式创建集合。
2、执行语句,创建文档:
db.comment.insert({
"articleid": "100",
"content": "今天天气真好!",
"userid": "1001",
"nickname": "zhangsan",
"createdatetime": new Date(),
"likenum": NumberInt(10),
"state": null
})
提示:
1)comment集合如果不存在,就会隐式创建集合。
2)mongo中的数字,默认情况下是double类型的,要转存整型,需要使用函数NumberInt()。
3)插入当前日期使用 new Date()。
4)插入的数据没有指定 _id ,会自动生成主键值。
5)如果字段没值,可以赋值为 null 或者不写该字段。
2)批量插入
1、语法:db.集合名.insertMany( [{"xx": ... }] )
或者 db.集合名.insert( [{"xx": ... }] )
。
2、执行语句,创建多条文档:
db.comment.insertMany([{
"articleid": "100",
"content": "我也觉得天气不错。",
"userid": "1002",
"nickname": "lisi",
"createdatetime": new Date("2021-7-21T17:40:42.485Z"),
"likenum": NumberInt(20),
"state": 1
},{
"articleid": "100",
"content": "天气晴朗。",
"userid": "1003",
"nickname": "wangwu",
"createdatetime": new Date("2021-7-21T17:42:23.321Z"),
"likenum": NumberInt(30),
"state": 1
}])
注意:
1、如果某条数据插入失败时,将会终止插入,之前插入成功的数据不会回滚。
2、因为批量插入由于数据较多,很容易出现失败,因此可以使用 try catch 进行异常捕获处理。
3.3.2 文档的查询
1)查询所有
1、语法:db.集合名.find()
2、执行语句:db.comment.find()
,查看 comment 集合下的所有文档。
2)根据条件查询
1、语法:db.集合名.find(query)
2、执行语句:db.comment.find({"userid": "1003"})
,查询 userid 为 1003 的文档。
3)只返回第一条数据
1、语法:db.集合名.findOne()
2、执行语句:db.comment.findOne()
,只返回一条文档。
4)投影查询,只显示部分字段
1、语法:db.集合名.find(query,mapping)
,1为显示 ,0为不显示。_id默认始终显示,设置为0后不显示。
2、执行语句:db.comment.find({},{"content": 1, "_id":0})
,查询所有文档,只显示文档的 content 字段内容。
3.3.3 文档的更新
语法:db.集合名.update(query,update,options)
1)覆盖修改
1、其他未修改字段会被删除!
2、执行语句:db.comment.update({"_id": ObjectId("613ac289ae690000600000be")}, {"likenum": NumberInt(12)})
报错:Invalid key 'likenum': update only works with $ operators and pipelines。更新仅适用于$运算符和管道。不支持覆盖修改,想要修改使用 $set。
可能现在文档的更新不支持这样直接更新,需要添加 $ 操作符。
2)局部修改
1、在更新的对象前添加 $set
2、执行语句:db.comment.update({"_id": ObjectId("613ac289ae690000600000be")},{$set:{"likenum": NumberInt(12)}})
,将指定 id 的点赞数修改为12,不会删除其他字段。
注意:
1、每个对象的键对应的值的数据类型要和存储时的数据类型一致才能查询到。
2、默认自动生成的 "_id" 的数据类型是 ObjectId,所以要加上 ObjectId 函数进行转换。
3、当更新的是一个不存在的字段时,会自动添加这一列字段。
3)批量修改
1、我们发现局部修改只会修改第一个匹配的数据,无法批量修改。
2、想要批量修改需要添加第三个参数 {"multi:true"}
3、执行语句:db.comment.update({"articleid": "100"},{$set:{"state": NumberInt(1)}},{"multi": true})
,将 articleid = 100 匹配的所有文档的状态都改为1,若不加 multi:true 则只会修改第一个匹配的文档。
4)列值增长修改
1、适用于某个列的值想要在原来的基础上增加或减少,可以使用$inc
。
2、执行语句:db.comment.update({"_id": ObjectId("613ac289ae690000600000be")},{$inc:{"likenum": NumberInt(-1)}})
, 将指定 id 点赞数减1。
3.3.4 删除文档
1、语法:db.集合名.remove(query)
2、执行语句:db.comment.remove({"_id": ObjectId("613ac289ae690000600000be")})
,将指定 id 的数据删除。
注意:db.comment.remove({}),会将整张表的数据删除!类似与 db.comment.drop(),但 db.comment.remove({}) 还会保存集合,db.comment.drop() 集合也删除了。
3.3.5 文档的分页查询
1)统计文档的个数
1、语法:db.集合名.count(query)
。
2、执行语句:db.comment.count()
,查询所有文档数量。
2)限制查询条数
1、语法:db.集合名.find().limit(num)
。
2、执行语句:db.comment.find().limit(2)
,只查询前两条文档数据。
3)跳过文档
1、语法:db.集合名.find().skip(num)
。
2、执行语句:db.comment.find().skip(2)
,跳过前两条文档查全部文档。
4)分页查询
1、语法:db.集合名.find().skip(num).limit(num)
。
2、每页查询5条,查询第3页数据。执行语句:db.comment.find().skip(5*(3-1)).limit(5)
。
3.3.6 文档的排序查询
1、语法:db.集合名.find().sort({key:1})
。1升序,-1降序。
2、执行语句:db.comment.find().sort({"articleid": 1,"likenum": -1})
,articleid字段升序排序(从小到大),likenum字段降序排序(从大到小),优先对 articleid 字段排序 。
4. MongoDB 索引
1、如果没有索引,MongoDB 必须执行全集合扫描,即扫描集合中的每个文档,这种扫描全集合的查询效率是非常低的,特别在处理大量的数据时,查询可以要花费几十秒甚至几分钟,,这对网站的性能是非常致命的。
2、索引是特殊的数据结构,它以易于遍历的形式存储集合数据集的一小部分。
3、如果添加了索引会先从索引库查找数据,提高查询效率。
4、MongoDB 使用的是 B Tree,MySQL 使用的是 B+ Tree。
4.1 索引的类型
4.1.1 单字段索引
1、MongoDB 支持在文档的单个字段上创建用户定义的升序(1)或者降序(-1)索引, 称为单字段索引。(Single Field Index)
2、对于单个字段索引和排序操作, 索引键的排序顺序(即升序或降序)并不重要, 因为 MongoDB 可以在任何方向上遍历索引。
4.1.2 复合索引
1、MongoDB 还支持多个字段的用户定义索引, 即复合索引。( Compound Index)
2、复合索引中列出的字段顺序具有重要意义,例如, 如果复合索引由 { userid: 1, score: -1 }
组成, 则索引首先按 userid
正序排序, 然后在每个 userid
的值内, 再在按 score
倒序排序。
4.1.3 其他索引
1、地理空间索引(Geospatial Index)
为了支持对地理空间坐标数据的有效查询, MongoDB 提供了两种特殊的索引: 返回结果时使用平面几何的二维索引和返回结果时使用球面几何的二维球面索引.
2、文本索引(Text Indexes)
MongoDB 提供了一种文本索引类型, 支持在集合中搜索字符串内容.这些文本索引不存储特定于语言的停止词(例如 “the”, “a”, “or”), 而将集合中的词作为词干, 只存储根词.
3、哈希索引(Hashed Indexes)
为了支持基于散列的分片, MongoDB 提供了散列索引类型, 它对字段值的散列进行索引.这些索引在其范围内的值分布更加随机, 但只支持相等匹配, 不支持基于范围的查询.
4.2 索引的操作
4.2.1 索引的查看
1、语法:db.collection.getIndexes()
2、执行语句,查看 comment 表中的索引 :db.comment.getIndexes()
> db.comment.getIndexes()
[ { "v" : 2, "key" : { "_id" : 1 }, "name" : "_id_" } ]
3、发现主键 _id 默认就加了索引。v 是索引引擎的版本号,key 是哪个字段加了索引,"_id" 为加索引的字段名,1 表示升序排序,name 为索引的名称。
MongoDB 在创建集合的过程中, 在
_id
字段上创建一个唯一的索引,默认名字为_id_
, 该索引可防止客户端插入两个具有相同值的文档, 不能在_id
字段上删除此索引。注意:该索引是唯一索引, 因此值不能重复, 即
_id
值不能重复的。在分片集群中, 通常使用
_id
作为片键.
4.2.2 索引的创建
1、语法:db.collection.createIndex(keys, options)
2、keys 表示要在哪些字段上添加索引。options 为可选项,常用的有
-
unique:true
表示索引字段的值是否唯一,默认为 false。 -
name:xxx
索引的名称,默认为索引的字段名+上下划线+排序方式(1、-1)。
3、执行语句,给 comment 表中的 userid 加上升序索引 :db.comment.createIndex({userid:1})
{
"v" : 2,
"key" : {
"userid" : 1
},
"name" : "userid_1"
}
4、执行语句,给 comment 表中的 userid 加上升序索引,给 nickname 加上降序索引:db.comment.createIndex({userid:1,nickname:-1})
{
"v" : 2,
"key" : {
"userid" : 1,
"nickname" : -1
},
"name" : "userid_1_nickname_-1"
}
4.2.3 索引的删除
1、语法:
# 删除某一个索引
db.collection.dropIndex(indexName/document)
# 删除全部索引
db.collection.dropIndexes()
2、执行语句,删除 comment 表中的 userid 升序索引:db.comment.dropIndex({userid:1})
或者 db.comment.dropIndex("userid_1")
3、执行语句,删除 comment 表中的所有索引db.comment.dropIndexes()
注意:
_id
的字段的索引是无法删除的, 只能删除非_id
字段的索引
4.3 索引的使用
4.3.1 执行计划
1、执行计划通常是用于分析查询的性能 。我们可以通过执行计划查看添加的索引是否有效,效果如何。
2、语法:db.collection.find(query,option).explain(options)
3、执行语句查看根据 user_id
查询数据的情况:db.comment.find({userid:"1003"}).explain()
{
...
"winningPlan" : {
"stage" : "COLLSCAN",
"filter" : {
"userid" : {
"$eq" : "1003"
}
},
"direction" : "forward"
},
...
}
-
发现,当未添加索引时,查询的策略为 "COLLSCAN" 全集合扫描,未用上索引。
-
添加索引:
db.comment.createIndex({userid:1})
-
再次执行:
db.comment.find({userid:"1003"}).explain()
"winningPlan" : {
"stage" : "FETCH",
"inputStage" : {
"stage" : "IXSCAN",
"keyPattern" : {
"userid" : 1
},
"indexName" : "userid_1",
"isMultiKey" : false,
"multiKeyPaths" : {
"userid" : [ ]
},
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 2,
"direction" : "forward",
"indexBounds" : {
"userid" : [
"[\"1003\", \"1003\"]"
]
}
}
}
- 发现,当添加索引时,查询的策略为 "FETCH" 基于索引的扫描,先通过 "IXSCAN" 查找索引的集合,再抓取到对应的文档。
4.3.2 索引覆盖
当查询条件和查询的投影正好是索引的字段时,MongoDB 会直接从索引返回结果,而不再去找集合中的文档了(没有 FETCH 的操作了),效率特别高。
5. MongoDB 实战
5.1 案例需求
1、根据头条的一篇文章,实现文章评论的业务,头条地址:https://www.toutiao.com/a6721476546088927748/
2、需要实现以下功能:
1)基本增删改查 API
2)根据文章 id 查询评论
3)评论点赞
3、使用 MongoDB 存放文章评论的数据,数据结构参考如下:
数据库:articledb
文章评论集合:comment
字段名称 | 字段含义 | 字段类型 | 备注 |
---|---|---|---|
_id | ID | ObjectId | Mongo的主键字段 |
articleid | 文章ID | String | |
content | 评论内容 | String | |
userid | 评论人ID | String | |
nickname | 评论人昵称 | String | |
createdatetime | 评论的日期时间 | Date | |
likenum | 点赞数 | Int32 | |
replynum | 回复数 | Int32 | |
state | 状态 | String | 0:不可见,1:可见 |
parentid | 上级ID | String | 0表示文章的指定评论 |
5.2 技术选型
5.2.1 mongodb-driver
1、mongodb-driver 是 mongo 官方推出的 java 连接 mongoDB 的驱动包,相当于 JDBC 驱动。
2、官方驱动说明和下载:http://mongodb.github.io/mongo-java-driver/
3、官方驱动示例文档:http://mongodb.github.io/mongo-java-driver/3.8/driver/getting-started/quick-start/
5.2.2 SpringDataMongoDB
1、SpringData家族成员之一,用于操作MongoDB的持久层框架,封装了底层的mongodb-driver。
2、官网主页: https://projects.spring.io/spring-data-mongodb/
5.3 基本项目搭建
5.3.1 引入 MongoDB 依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
5.3.2 修改配置文件
# 主机地址
spring.data.mongodb.host=192.168.200.105
# 端口号
spring.data.mongodb.port=27017
# 数据库名称
spring.data.mongodb.database=articledb
# 也可以通过 url 来链接
# spring.data.mongodb.uri=mongodb://192.168.200.105:27017/articledb
5.3.3 创建实体类
实体类的字段和 mongodb 中的字段相对应
@Document(collection = "comment")
@Data
@Accessors(chain = true)
public class Comment implements Serializable {
@Id
private String id; // 主键
@Field("content")
private String content; // 吐槽内容
private Date publishtime; // 发布日期
@Indexed
private String userid; // 发布人ID
private String nickname; // 昵称
private LocalDateTime createdatetime; // 评论的日期时间
private Integer likenum; // 点赞数
private Integer replynum; // 回复数
private String state; // 状态
private String parentid; // 上级ID
private String articleid; // 文章ID
}
@Document(collection = "comment"):
声明这是一个mongodb的文档,对应的集合名为"comment"。
collection = "comment" 可以省略,如果省略,则默认使用类名小写映射集合
@Id:
主键标识,该属性的值会自动对应mongodb的主键字段"_id",如果该属性名就叫"id",则该注解可以省略,否则必须写
@Field("content"):
如果实体的字段名和mongodb的字段名不一致,可以使用该注解声明mongodb的字段名,实现映射
@Indexed:
可以添加一个单字段的索引
@CompoundIndex( def = "{'userid': 1, 'nickname': -1}"):
可以添加一个复合索引
单字段的索引和复合索引建议在命令行创建!
5.3.4 编写持久层接口
public interface CommentRepository extends MongoRepository<Comment,String> {
}
MongoRepository<T, ID>:T 为实体类,ID 为主键类型。
5.3.5 编写服务层接口
1、服务层注入持久层,使用持久层实现的方法完成基本的增删改查API
@Service
public class CommentService {
@Autowired
private CommentRepository commentRepository;
public void saveComment(Comment comment){
commentRepository.save(comment);
}
public void updateComment(Comment comment){
commentRepository.save(comment);
}
public void deleteCommentById(String id){
commentRepository.deleteById(id);
}
public List<Comment> findCommentList(){
return commentRepository.findAll();
}
public Comment findCommentById(String id){
return commentRepository.findById(id).get();
}
}
2、编写测试类测试
@RunWith(SpringRunner.class)
@SpringBootTest
public class CommentServiceTest {
@Autowired
CommentService commentService;
@Test
public void testsaveComment() {
LocalDateTime createDateTime = LocalDateTime.of(2019,8,5,9,30,20);
Comment comment = new Comment();
comment.setArticleid("1001").setContent("不能空腹吃早餐").setLikenum(11).setReplynum(4)
.setUserid("2001").setNickname("相忘于江湖33594983")
.setCreatedatetime(createDateTime).setState("1").setParentid(null);
commentService.saveComment(comment);
}
@Test
public void testFindCommentList(){
List<Comment> commentList = commentService.findCommentList();
System.out.println(commentList);
}
}
5.4 根据上级ID查询文章评论的分页列表
5.4.1 修改持久层接口
public interface CommentRepository extends MongoRepository<Comment,String> {
Page<Comment> findByParentid(String parentid, Pageable pageable);
}
5.4.2 修改服务层接口
1、修改服务层接口
public Page<Comment> findCommentListByParentId(String parentId,int page,int size){
return commentRepository.findByParentid(parentId, PageRequest.of(page-1, size));
}
2、编写测试类测试
@Test
public void testFindCommentListByParentId(){
Page<Comment> commentPage = commentService.findCommentListByParentId("613ec7e519b642261cc94e48", 1, 2);
long total = commentPage.getTotalElements();
List<Comment> content = commentPage.getContent();
System.out.println("总条数:"+total);
System.out.println("评论信息:"+content);
}
5.5 MongoTemplate 实现评论点赞
5.5.1 使用 MongoRepository
使用 MongoRepository 虽然也可以实现点赞数加一,但是执行效率低,因为我只需要将点赞数加1就可以了,没必要查询出所有字段修改后再更新所有字段,两次 IO 操作
public void updateCommentLikeNumById(String id){
Comment comment = commentRepository.findById(id).get();
comment.setLikenum(comment.getLikenum()+1);
commentRepository.save(comment);
}
5.5.2 使用 MongoTemplate
1、使用 MongoTemplate 可以实现较复杂的查询与更新操作。
public void updateCommentLikeNumById(String id){
// 查询条件,Criteria 条件,addCriteria 可以拼接多个条件
Query query = Query.query(Criteria.where("_id").is(id));
// 更新条件
Update update = new Update();
update.inc("likenum");
mongoTemplate.updateFirst(query,update,Comment.class);
}
2、编写测试类测试
@Test
public void testUpdateCommentLikeNumById(){
commentService.updateCommentLikeNumById("613eecb219b6423e8450e0f2");
}
6. 副本集 Replica Sets
6.1 副本集集群搭建
6.1.1 简介
1、MongoDB中的副本集(Replica Set)是一组维护相同数据集的mongod服务。 (所有的副本集存储相同的数据)
2、副本集可提供冗余和高可用性,是所有生产部署的基础。(当主节点服务不可用时,可以使用从节点提供服务,保证系统的高可用)
3、还可以利用副本服务器做只读服务器,实现读写分离,提高负载。
4、类似于 Mysql 的主从复制,区别是主从复制是指定主、从节点的,副本集是通过选举选出一个主节点。
6.1.2 副本集角色
1、副本集有两种类型和三种角色。
2、两种类型:
- 主节点(Primary)类型:数据操作的主要连接点,可读写。
- 从节点(Secondaries)类型:数据冗余备份节点,可以读或选举。
3、三种角色:
-
主要成员(Primary):主要接收所有写操作。就是主节点。
-
副本成员(Replicate):可以备份数据,不可写操作,但可以读。是默认的一种从节点类型。
-
仲裁者(Arbiter):不保存任何数据,只具有投票选举作用。
仲裁者的补充说明:
1、仲裁者的目的是通过响应其他副本集成员的心跳和选举请求来维护副本集中的仲裁。 因为仲裁者不存储数据集,所以仲裁者可以是提供副本集仲裁功能的好方法,其资源成本比具有数据集的全功能副本集成员更便宜。
2、仲裁者将永远是仲裁者,而主要人员可能会退出并成为次要人员,而次要人员可能成为选举期间的主要人员。
3、如果你的副本+主节点的个数是偶数,建议加一个仲裁者,形成奇数,容易满足大多数的投票。
4、如果你的副本+主节点的个数是奇数,可以不加仲裁者。
6.1.3 副本集的创建
1、本系统架构采用一主一副本一仲裁的设计。为了节约成本,在同一台服务上跑三个MongoDB服务器,占用不同的端口号。
2、复制三个解压包文件,放到 /usr/local 目录下,取名为 mongodb-27017、mongodb-27018、mongodb-27019
3、在每个mongodb目录下创建 data/log、data/db 目录
4、在每个mongodb目录下创建 config 目录,编写 mongod.conf 配置文件
systemLog:
# MongoDB发送所有日志输出的目标指定为文件
destination: file
# mongod或mongos应向其发送所有诊断日志记录信息的日志文件的路径
path: "/usr/local/mongodb-27017/data/log/mongodb.log"
# 当mongos或mongod实例重新启动时,mongos或mongod会将新条目附加到现有日志文件的末尾。
logAppend: true
storage:
# mongod实例存储其数据的目录。storage.dbPath设置仅适用于mongod。
dbPath: "/usr/local/mongodb-27017/data/db"
journal:
#启用或禁用持久性日志以确保数据文件保持有效和可恢复。
enabled: true
processManagement:
# 启用在后台运行mongos或mongod进程的守护进程模式。
fork: true
# 指定用于保存mongos或mongod进程的进程ID的文件位置,其中mongos或mongod将写入其PID
pidFilePath: "/usr/local/mongodb-27017/data/log/mongodb.pid"
net:
# 服务实例绑定所有IP,有副作用,副本集初始化的时候,节点名字会自动设置为本地域名,而不是ip
#bindIpAll: true
# 服务实例绑定的IP
bindIp: 0.0.0.0
# bindIp
#绑定的端口
port: 27017
replication:
# 副本集的名称
replSetName: myrs
5、分别启动这三个mongodb
./mongod -f ../config/mongod.conf
[root@localhost bin]# ps -aux | grep mongod
root 85852 2.0 4.0 1719332 82460 ? Sl 15:48 0:01 ./mongod -f ../config/mongod.conf
root 85925 2.4 4.1 1720360 84332 ? Sl 15:48 0:00 ./mongod -f ../config/mongod.conf
root 85993 3.7 4.4 1719332 89832 ? Sl 15:49 0:00 ./mongod -f ../config/mongod.conf
6、此时登录27017的mongodb,发现许多命令无法使用,例如:show dbs
./mongo --port=27017
> show dbs
uncaught exception: Error: listDatabases failed:{
"topologyVersion" : {
"processId" : ObjectId("613f0263a5bc84436152edb4"),
"counter" : NumberLong(0)
},
"ok" : 0,
"errmsg" : "not master and slaveOk=false",
"code" : 13435,
"codeName" : "NotPrimaryNoSecondaryOk"
}
7、需要初始化配置副本集和主节点
语法:rs.initiate()
> rs.initiate()
{
"info2" : "no configuration specified. Using a default configuration for the set",
"me" : "localhost.localdomain:27017",
"ok" : 1
}
myrs:SECONDARY>
myrs:PRIMARY>
8、查看 rs 的配置:rs.conf()
myrs:PRIMARY> rs.conf()
{
"_id" : "myrs",
"version" : 1,
"term" : 1,
"members" : [
{
"_id" : 0,
"host" : "localhost.localdomain:27017",
"arbiterOnly" : false,
"buildIndexes" : true,
"hidden" : false,
"priority" : 1,
"tags" : {
},
"secondaryDelaySecs" : NumberLong(0),
"votes" : 1
}
],
"protocolVersion" : NumberLong(1),
"writeConcernMajorityJournalDefault" : true,
"settings" : {
"chainingAllowed" : true,
"heartbeatIntervalMillis" : 2000,
"heartbeatTimeoutSecs" : 10,
"electionTimeoutMillis" : 10000,
"catchUpTimeoutMillis" : -1,
"catchUpTakeoverDelayMillis" : 30000,
"getLastErrorModes" : {
},
"getLastErrorDefaults" : {
"w" : 1,
"wtimeout" : 0
},
"replicaSetId" : ObjectId("613f04daa5bc84436152ee5d")
}
}
1) "_id" : "myrs" :副本集的名字
2) "members" :副本集成员数组,此时只有一个,该成员不是仲裁节点: "arbiterOnly" : false ,优先级(权重值): "priority" : 1
3) "settings" :副本集的参数配置
9、查看副本集节点运行的状态:rs.status()
{
"set" : "myrs",
"date" : ISODate("2021-09-13T08:05:49.902Z"),
"myState" : 1,
"term" : NumberLong(1),
"syncSourceHost" : "",
"syncSourceId" : -1,
"heartbeatIntervalMillis" : NumberLong(2000),
"majorityVoteCount" : 1,
"writeMajorityCount" : 1,
"votingMembersCount" : 1,
"writableVotingMembersCount" : 1,
"optimes" : {
"lastCommittedOpTime" : {
"ts" : Timestamp(1631520342, 1),
"t" : NumberLong(1)
},
"lastCommittedWallTime" : ISODate("2021-09-13T08:05:42.568Z"),
"readConcernMajorityOpTime" : {
"ts" : Timestamp(1631520342, 1),
"t" : NumberLong(1)
},
"appliedOpTime" : {
"ts" : Timestamp(1631520342, 1),
"t" : NumberLong(1)
},
"durableOpTime" : {
"ts" : Timestamp(1631520342, 1),
"t" : NumberLong(1)
},
"lastAppliedWallTime" : ISODate("2021-09-13T08:05:42.568Z"),
"lastDurableWallTime" : ISODate("2021-09-13T08:05:42.568Z")
},
"lastStableRecoveryTimestamp" : Timestamp(1631520312, 1),
"electionCandidateMetrics" : {
"lastElectionReason" : "electionTimeout",
"lastElectionDate" : ISODate("2021-09-13T07:59:22.471Z"),
"electionTerm" : NumberLong(1),
"lastCommittedOpTimeAtElection" : {
"ts" : Timestamp(0, 0),
"t" : NumberLong(-1)
},
"lastSeenOpTimeAtElection" : {
"ts" : Timestamp(1631519962, 1),
"t" : NumberLong(-1)
},
"numVotesNeeded" : 1,
"priorityAtElection" : 1,
"electionTimeoutMillis" : NumberLong(10000),
"newTermStartDate" : ISODate("2021-09-13T07:59:22.477Z"),
"wMajorityWriteAvailabilityDate" : ISODate("2021-09-13T07:59:22.485Z")
},
"members" : [
{
"_id" : 0,
"name" : "localhost.localdomain:27017",
"health" : 1,
"state" : 1,
"stateStr" : "PRIMARY",
"uptime" : 1018,
"optime" : {
"ts" : Timestamp(1631520342, 1),
"t" : NumberLong(1)
},
"optimeDate" : ISODate("2021-09-13T08:05:42Z"),
"syncSourceHost" : "",
"syncSourceId" : -1,
"infoMessage" : "",
"electionTime" : Timestamp(1631519962, 2),
"electionDate" : ISODate("2021-09-13T07:59:22Z"),
"configVersion" : 1,
"configTerm" : 1,
"self" : true,
"lastHeartbeatMessage" : ""
}
],
"ok" : 1,
"$clusterTime" : {
"clusterTime" : Timestamp(1631520342, 1),
"signature" : {
"hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
"keyId" : NumberLong(0)
}
},
"operationTime" : Timestamp(1631520342, 1)
}
1) "set" : "myrs" :副本集的名字
2) "myState" : 1:说明状态正常
3) "members" :副本集成员数组,此时只有一个,该成员的角色是 "stateStr" : "PRIMARY", 该节点是健康的: "health" : 1
10、将27018加入为从节点:rs.add(host)
myrs:PRIMARY> rs.add("192.168.200.105:27018")
{
"ok" : 1,
"$clusterTime" : {
"clusterTime" : Timestamp(1631520526, 1),
"signature" : {
"hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
"keyId" : NumberLong(0)
}
},
"operationTime" : Timestamp(1631520526, 1)
}
11、将27019加入为仲裁节点:rs.addArb(host)
myrs:PRIMARY> rs.addArb("192.168.200.105:27019")
{
"ok" : 1,
"operationTime" : Timestamp(1631520527, 1),
"$clusterTime" : {
"clusterTime" : Timestamp(1631520527, 1),
"signature" : {
"hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
"keyId" : NumberLong(0)
}
}
}
12、rs.status()
可以查看 rs 副本集中有一主节点、一从节点、一仲裁节点。
6.1.4 副本集的数据读写操作
1、目标:测试三个不同角色的节点的数据读写情况。
2、主节点可以读、写集合的数据。
3、从节点不能读、写集合的数据,当前从节点只是一个备份。
4、默认情况下,从节点是没有读写权限的,可以增加读的权限,但需要进行设置。
5、设置从节点有读操作权限:rs.slaveOk()
# 登录从节点
./mongo --host=27018
myrs:SECONDARY> rs.slaveOk()
WARNING: slaveOk() is deprecated and may be removed in the next major release. Please use secondaryOk() instead.
myrs:SECONDARY> show dbs
admin 0.000GB
config 0.000GB
local 0.000GB
6、现在可实现了读写分离,让主插入数据,让从来读取数据。
7、如果要取消作为奴隶节点的读权限:rs.slaveOk(false)
myrs:SECONDARY> rs.slaveOk(false)
WARNING: slaveOk() is deprecated and may be removed in the next major release. Please use secondaryOk() instead.
8、仲裁者节点,不存储任何业务数据,只有一个local数据库。
6.2 主节点的选举原则
1、MongoDB在副本集中,会自动进行主节点的选举,主节点选举的触发条件:
-
主节点故障
-
主节点网络不可达(默认心跳信息为10秒)
-
人工干预(rs.stepDown(600))
2、选举规则是根据票数来决定谁获胜:
-
票数最高,且获得了“大多数”成员的投票支持的节点获胜。
“大多数”的定义为:假设复制集内投票成员数量为N,则大多数为 N/2 + 1。例如:3个投票成员, 则大多数的值是2。当复制集内存活成员数量不足大多数时,整个复制集将无法选举出Primary, 复制集将无法提供写服务,处于只读状态。
-
若票数相同,且都获得了“大多数”成员的投票支持的,数据新的节点获胜。
数据的新旧是通过操作日志oplog来对比的。
3、在获得票数的时候,优先级(priority)参数影响重大。
-
可以通过设置优先级(priority)来设置额外票数。
-
优先级即权重,取值为0-1000,相当于可额外增加 0-1000的票数,优先级的值越大,就越可能获得多数成员的投票(votes)数。
-
指定较高的值可使成员 更有资格成为主要成员,更低的值可使成员更不符合条件。
-
默认情况下,主节点和从节点优先级的值是1;仲裁节点的优先级是0,即不具备选举权。可以通过
rs.conf()
命令查看。
修改优先级:
1)先将配置导入cfg变量
rs.conf()
2)然后修改值(ID号默认从0开始)
cfg.members[1].priority=2
3)重新加载配置
rs.reconfig(cfg)
4)等待一会,发现原来的27018从节点重新选举成为了主节点
6.3 故障测试
6.3.1 从节点故障测试
1、关闭27017副本节点,发现,27018主节点和27019仲裁节点对27017的心跳失败。
2、但是主节点还在,因此,没有触发投票选举。
3、 在27017副本节点关闭后,在主节点写入数据。
db.comment.insert({"_id":"1","articleid":"100001","content":"我们不应该把清晨浪费在手机上,健康很重要,一杯温水幸福你我他。","userid":"1002","nickname":"相忘于江湖","createdatetime":new Date("2019-08-05T22:08:15.522Z"),"likenum":NumberInt(1000),"state":"1"})
4、重新启动27017副本节点,查看comment集合是否有插入的数据。发现,主节点写入的数据,会自动同步给从节点。
6.3.2 主节点故障测试
1、关闭27018主节点,发现从节点和仲裁节点对27018的心跳失败,当失败超过10秒,此时认为主节点挂了,会自动发起投票。
2、而副本节点此时只有27017,因此,候选人只有一个就是27017,开始投票。 27019向27017投了一票,27017本身自带一票,因此共两票,超过了“大多数”。
3、27019是仲裁节点,没有选举权,27017不向其投票,其票数是0。
4、最终结果,27017成为主节点,具备读写功能。
5、在27017写入数据。
db.comment.insert({"_id":"2","articleid":"100001","content":"我夏天空腹喝凉开水,冬天喝温开水","userid":"1005","nickname":"伊人憔悴","createdatetime":new Date("2019-08-05T23:58:51.485Z"),"likenum":NumberInt(888),"state":"1"})
6、再启动27018节点,发现27018变成了从节点,27017仍保持主节点。27018数据自动从27017同步。
6.3.3 仲裁节点和主节点故障
1、先关掉仲裁节点27019, 关掉现在的主节点27018 。
2、登录27017后,发现,27017仍然是从节点,副本集中没有主节点了,导致此时,副本集是只读状态,无法写入。
为啥不选举了?
因为27017的票数,没有获得大多数,即没有大于等于2,它只有默认的一票(优先级是1) 如果要触发选举,需要随便加入一个成员即可。
3、如果只加入27019仲裁节点成员,则主节点一定是27017,因为没得选了,仲裁节点不参与选举, 但参与投票。
4、如果只加入27018节点,会发起选举。因为27017和27018都是两票,则按照谁数据新,谁当主节点。
6.3.4 仲裁节点和从节点故障
1、先关掉仲裁节点27019, 关掉现在的副本节点27017
2、10秒后,27018主节点自动降级为副本节点。(服务降级)
3、主节点无法自己完成副本集的服务,副本集不可写数据了,已经故障了。
6.4 navicat 连接副本集
1、需要修改第一个主机的ip,"host" : "localhost.localdomain:27017"
修改为"host" : "192.168.200.105:27017"
var config = rs.config();
config.members[0].host="192.168.200.105:27017";
rs.reconfig(config);
2、navicat 连接副本集
6.5 SpringDataMongoDB 连接副本集
1、不能像之前一样使用 host+ip+数据库 连接,需要使用 uri 链接。
2、副本集 uri 语法
mongodb://host1,host2,host3/articledb?connect=replicaSet&slaveOk=true&replicaSet=myrs
# host1,host2,host3:副本集主机和ip地址
# articledb:数据库名称
# connect=replicaSet:自动到副本集中选择读写的主机
# slaveOk=true:开启副本节点读的功能,可实现读写分离。
# replicaSet:副本集名字
3、application.properties 中配置:
spring.data.mongodb.uri=mongodb://192.168.200.105:27017,192.168.200.105:27018,192.168.200.105:27019/articledb?connect=replicaSet&slaveOk=true&replicaSet=myrs
4、按照之前写的测试类进行测试。
6. 分片集群 Sharded Cluster
6.1 分片集群简介
1、之前的副本集我们可以发现所有的节点存储的数据都是一样的,数据最终还是存在一台服务器上,当数据量非常大的时候一台服务器可能存储不下。
2、分片集群就是将数据进行拆分,存储在不同的机器上,这样不需要功能强大的大型计算机就可以储存更多的数据,处理更多的负载。
3、有两种解决数据增长的方法:垂直扩展和水平扩展。
- 垂直扩展:增加服务器的配置,加内存加硬盘......有上限,并且成本太高。
- 水平扩展:分散的存在一些小服务器上。
6.2 分片集群包含的组件
MongoDB分片群集包含以下组件:
- 分片(存储):数据拆分存储在不同的分片上。 每个分片都可以部署为副本集。
- mongos(路由):可以知道要操作的数据在哪一个分片上,在客户端应用程序和分片集群之间提供接口。
- config servers(“调度”的配置):存放配置信息,哪个数据存在哪个分片,路由如何去调用等等。 从MongoDB 3.4开始,必须将配置服务器部署为副本集(CSRS)。
6.3 分片集群架构目标
-
2个路由节点
-
2个分片节点搭建副本集,每个副本集包含一主一从一仲裁
-
1个配置节点副本集,包含一主二从
6.4 副本集搭建
1、按上图拷贝11个解压包。
2、每个解压包中添加 data/log 、data/db 文件夹。
3、每个解压包中添加 config/mongod.conf 或者 config/mongos.conf 配置文件。
1)分片服务配置文件 mongod.conf:
systemLog:
# MongoDB发送所有日志输出的目标指定为文件
destination: file
# mongod或mongos应向其发送所有诊断日志记录信息的日志文件的路径
path: "/usr/local/mongodb-sharded/shard01-27018/data/log/mongodb.log"
# 当mongos或mongod实例重新启动时,mongos或mongod会将新条目附加到现有日志文件的末尾。
logAppend: true
storage:
# mongod实例存储其数据的目录。storage.dbPath设置仅适用于mongod。
dbPath: "/usr/local/mongodb-sharded/shard01-27018/data/db"
journal:
#启用或禁用持久性日志以确保数据文件保持有效和可恢复。
enabled: true
processManagement:
# 启用在后台运行mongos或mongod进程的守护进程模式。
fork: true
# 指定用于保存mongos或mongod进程的进程ID的文件位置,其中mongos或mongod将写入其PID
pidFilePath: "/usr/local/mongodb-sharded/shard01-27018/data/log/mongodb.pid"
net:
# 服务实例绑定所有IP,有副作用,副本集初始化的时候,节点名字会自动设置为本地域名,而不是ip
#bindIpAll: true
# 服务实例绑定的IP
bindIp: 0.0.0.0
# bindIp
#绑定的端口
port: 27018
replication:
# 副本集的名称
replSetName: shard01
sharding:
# 分片角色
clusterRole: shardsvr
-
sharding.clusterRole: shardsvr
:分片服务 -
sharding.clusterRole: configsvr
:配置服务
2)配置服务配置文件 mongod.conf:
systemLog:
# MongoDB发送所有日志输出的目标指定为文件
destination: file
# mongod或mongos应向其发送所有诊断日志记录信息的日志文件的路径
path: "/usr/local/mongodb-sharded/configserver-27019/data/log/mongodb.log"
# 当mongos或mongod实例重新启动时,mongos或mongod会将新条目附加到现有日志文件的末尾。
logAppend: true
storage:
# mongod实例存储其数据的目录。storage.dbPath设置仅适用于mongod。
dbPath: "/usr/local/mongodb-sharded/configserver-27019/data/db"
journal:
#启用或禁用持久性日志以确保数据文件保持有效和可恢复。
enabled: true
processManagement:
# 启用在后台运行mongos或mongod进程的守护进程模式。
fork: true
# 指定用于保存mongos或mongod进程的进程ID的文件位置,其中mongos或mongod将写入其PID
pidFilePath: "/usr/local/mongodb-sharded/configserver-27019/data/log/mongodb.pid"
net:
# 服务实例绑定所有IP,有副作用,副本集初始化的时候,节点名字会自动设置为本地域名,而不是ip
#bindIpAll: true
# 服务实例绑定的IP
bindIp: 0.0.0.0
# bindIp
#绑定的端口
port: 27019
replication:
# 副本集的名称
replSetName: configserver
sharding:
# 配置角色
clusterRole: configsvr
3)路由服务配置文件 mongos.conf:
systemLog:
# MongoDB发送所有日志输出的目标指定为文件
destination: file
# mongod或mongos应向其发送所有诊断日志记录信息的日志文件的路径
path: "/usr/local/mongodb-sharded/router-27017/data/log/mongodb.log"
# 当mongos或mongod实例重新启动时,mongos或mongod会将新条目附加到现有日志文件的末尾。
logAppend: true
processManagement:
# 启用在后台运行mongos或mongod进程的守护进程模式。
fork: true
# 指定用于保存mongos或mongod进程的进程ID的文件位置,其中mongos或mongod将写入其PID
pidFilePath: "/usr/local/mongodb-sharded/router-27017/data/log/mongodb.pid"
net:
# 服务实例绑定所有IP,有副作用,副本集初始化的时候,节点名字会自动设置为本地域名,而不是ip
#bindIpAll: true
# 服务实例绑定的IP
bindIp: 0.0.0.0
# bindIp
#绑定的端口
port: 27017
sharding:
# 指定配置节点副本集
configDB: configserver/192.168.200.105:27019,192.168.200.105:27119,192.168.200.105:27219
4、搭建 shard0、shard1、configserver 副本集
# 启动 mongod 服务
./mongod -f ../config/mongod.conf
# 启动客户端连接 mongodb
./mongo --port=27018
# 初始化副本集
rs.initiate()
# 加入从节点
rs.add("192.168.200.105:27118")
# 加入仲裁节点
rs.addArb("192.168.200.105:27218")
6.5 路由节点的搭建
1、启动 mongos 服务
# 启动 mongos 服务
./mongos -f ../config/mongos.conf
# 此时路由节点只和配置服务连通,还未指定真正存储的分片服务,所以此时在路由节点存数据将失败。
2、添加分片
# 添加分片,单个分片
sh.addShard("IP:Port")
# 添加分片,分片副本集
sh.addShard("rsName/IP:Port,IP:Port,IP:Port")
sh.addShard("shard01/192.168.200.105:27018,192.168.200.105:27118,192.168.200.105:27218")
sh.addShard("shard02/192.168.200.105:27318,192.168.200.105:27418,192.168.200.105:27518")
如果出现:"errmsg" : "in seed list shard02/192.168.200.105:27318,192.168.200.105:27418,192.168.200.105:27518, host 192.168.200.105:27318 does not belong to replica set shard02;
var config = rs.config();
config.members[0].host="192.168.200.105:27318";
rs.reconfig(config);
# 查看 shard 状态
sh.status()
# 发现有2个 shard 副本集,没有将仲裁者加入分片
shards:
{ "_id" : "shard01", "host" : "shard01/192.168.200.105:27018,192.168.200.105:27118", "state" : 1, "topologyTime" : Timestamp(1631585930, 1) }
{ "_id" : "shard02", "host" : "shard02/192.168.200.105:27318,192.168.200.105:27418", "state" : 1, "topologyTime" : Timestamp(1631586171, 2) }
3、移除分片
# 移除分片
use admin
db.runCommand( { removeShard: "myshardrs02" } )
# 如果只剩下最后一个shard,是无法删除的。移除时会自动转移分片数据,需要一个时间过程。完成后,再次执行删除分片命令才能真正删除。
4、对数据库的集合开启分片功能
# 要对数据库的集合开启分片功能
# 先对数据库开启分片功能
sh.enableSharding("articledb")
# 再对此数据库的集合开启分片功能
sh.shardCollection(namespace, key, unique)
# namespace:命名空间,数据库.集合名
# key:根据哪个字段分片,指定分片的策略,分片字段一定不能为空
# unique:指定的分片字段如果是唯一索引,可以设置为true,hash策略片键不支持唯一索引,默认为false
sh.shardCollection("articledb.comment",{"nickname":"hashed"})
5、再添加一个路由,由于分片的配置信息已经存入configserver,所以第二个路由只需要连接configserver就行了,不需要再配置分片。配置文件和第一个路由类似,只需要修改端口号。
6.6 两种分片策略
1、hashed 分片策略
会将指定分片字段的值算出hash值,根据算出的hash值的不同范围加入不同的分片。由于hash值较为随机,所以会将数据平均分配到不同的分片服务器中。
如无特殊情况,一般推荐使用 hashed 分片策略。
而使用 _id 作为片键是一个不错的选择,因为它是必有的。
sh.shardCollection("articledb.comment",{"nickname":"hashed"})
# 查看 shard 状态
sh.status()
articledb.comment
shard key: { "nickname" : "hashed" }
unique: false
balancing: true
chunks:
shard01 4
{ "nickname" : { "$minKey" : 1 } } -->> { "nickname" : NumberLong("-4611686018427387902") } on : shard01 Timestamp(1, 0)
{ "nickname" : NumberLong("-4611686018427387902") } -->> { "nickname" : NumberLong(0) } on : shard01 Timestamp(1, 1)
{ "nickname" : NumberLong(0) } -->> { "nickname" : NumberLong("4611686018427387902") } on : shard01 Timestamp(2, 0)
{ "nickname" : NumberLong("4611686018427387902") } -->> { "nickname" : { "$maxKey" : 1 } } on : shard01 Timestamp(3, 0)
# 发现默认将存储的数据放入shard01的四个分片
# 根据nickname的值算出hash值,如果hash值小于-4611686018427387902 ,就放入shard01的第一个分片
# 根据nickname的值算出hash值,如果hash值大于-4611686018427387902,小于0 ,就放入shard01的第二个分片
# 根据nickname的值算出hash值,如果hash值大于0,小于4611686018427387902,就放入shard01的第三个分片
# 根据nickname的值算出hash值,如果hash值大于4611686018427387902,就放入shard01的第四个分片
2、范围策略
一个集合只能存在一种分片策略和分片字段,所以我们要换一个集合设置分片。
会在插入大量数据的时候才开始自动分片,分片字段值接近的大概率会存在同一个分片服务器中,所以适用于范围查询,例如:查询15-20岁的人,数据会大概率存在同一个分片服务器中,不用去两个分片服务器中去找了。
sh.shardCollection("articledb.author",{"age":1})
# 查看 shard 状态
sh.status()
articledb.author
shard key: { "age" : 1 }
unique: false
balancing: true
chunks:
shard02 1
{ "age" : { "$minKey" : 1 } } -->> { "age" : { "$maxKey" : 1 } } on : shard02 Timestamp(1, 0)
# 发现范围策略不会像hash策略一样帮我们自动分好片。而是在插入大量数据的时候,才自动分片。
# 由于分片字段没有进行编码,而是使用真实的值进行范围分片,所以分片字段值接近的大概率会存在同一个分片服务器中,这样根据这个分片字段进行范围查询时效率高。
6.7 navicat 连接分片集群
1、只需要输入任意一个路由的地址即可,192.168.200.105:27017、192.168.200.105:27117
6.8 SpringDataMongoDB 连接分片集群
1、可以通过 uri 连接分片集群。
spring.data.mongodb.uri=mongodb://192.168.200.105:27017,192.168.200.105:27117/articledb
2、SpringData默认会有负载均衡的策略,当配置两台路由节点时,会选择其中一台进行访问。
3、跑测试程序,执行成功。
7. 安全认证
7.1 安全认证简介
1、默认情况下,MongoDB实例启动运行时是没有启用用户访问权限控制的,也就是说,连接MongoDB服务后用户可以对所有资源进行任意操作,MongoDB不会对连接客户端进行用户验证,这是非常危险的。
2、mongodb官网上说,为了能保障mongodb的安全可以做以下几个步骤:
-
使用新的端口,默认的27017端口如果一旦知道了ip就能连接上,不太安全。
-
设置mongodb的网络环境,最好将mongodb部署到公司服务器内网,这样外网是访问不到的。公司内部访问使用vpn等。
-
开启安全认证。认证要同时设置服务器之间的内部认证方式,同时要设置客户端连接到集群的账号密码认证方式。
3、MongoDB 采用的是基于角色的访问控制(RBAC),给用户授予角色,给角色授予访问资源的权限。
4、常用的内置角色:
-
数据库用户角色:read、readWrite(指定某个数据库的增删改查权限);
-
所有数据库用户角色:readAnyDatabase、readWriteAnyDatabase、 userAdminAnyDatabase、dbAdminAnyDatabase
-
数据库管理角色:dbAdmin、dbOwner、userAdmin;
-
集群管理角色:clusterAdmin、clusterManager、clusterMonitor、hostManager;
-
备份恢复角色:backup、restore;
-
超级用户角色:root(超级权限)
-
内部角色:system
7.2 单实例环境
7.2.1 创建用户,分配角色
1、启动单机的 mongod,并登录。
2、创建两个管理员用户,一个是系统的超级管理员 root,一个是admin库的管理用户admin
# 切换到 admin 库
use admin
# 创建超级管理员root,密码root,角色root,验证数据库为当前的db数据库admin
db.createUser({user:"root",pwd:"root",roles:["root"]})
# 创建专门用来管理admin库的账号admin,只用来作为用户权限的管理
db.createUser({user:"admin",pwd:"admin",roles:[{role:"userAdminAnyDatabase",db:"admin"}]})
# 查看已经创建了的用户的情况:
db.system.users.find()
3、删除admin用户
db.dropUser("admin")
4、修改用户密码
db.changeUserPassword("root","root")
5、验证用户账号密码
use admin
# 密码输错
> db.auth("root","123456")
Error: Authentication failed.
0
# 密码正确
> db.auth("root","root")
1
6、创建普通用户
创建普通用户可以在没有开启认证的时候添加,也可以在开启认证之后添加,但开启认证之后,必须使用有操作admin库的用户登录认证后才能操作。底层都是将用户信息保存在了admin数据库的集合 system.users 中。
# 创建(切换)要操作的数据库articledb
use articledb
# 创建普通用户,验证数据库为当前的db数据库articledb,db:"articledb"可以省略不写
db.createUser({user: "yinrz", pwd: "123456", roles: [{ role: "readWrite", db:"articledb" }]})
7.2.2 服务端开启认证和客户端连接登录
1、先关闭之前的mongod服务
# 可以在mongo客户端中使用shutdownServer命令来关闭
use admin
db.shutdownServer()
# 也可以操作mongod停止
./mongod -f ../config/mongod.conf --shutdown
2、参数方式开启认证的方式启动服务
在启动时指定参数 --auth
./mongod -f ../config/mongod.conf --auth
3、配置文件方式
在 mongod 配置文件中添加
security:
#开启授权认证
authorization: enabled
4、此时用户想要访问mongod服务,如果未登录,啥数据库都看不见也操作不了
5、先连接mongod再认证
./mongo --port=27017
# 超级管理员用户
# 切换到 admin 库,要切换到之前赋角色的数据库下
use admin
db.auth("root","root")
# 普通用户
use articledb
db.auth("yinrz","123456")
6、 连接时认证
# 超级管理员用户
# --authenticationDatabase:指定认证的哪个库。 -u:账号。 -p:密码。
./mongo --host 192.168.200.105 --port 27017 --authenticationDatabase admin -u root -p root
# 普通用户
./mongo --host 192.168.200.105 --port 27017 --authenticationDatabase articledb -u yinrz -p 123456
7.2.3 SpringDataMongoDB 连接认证
1、可以通过username、password连接
spring.data.mongodb.host=192.168.200.105
spring.data.mongodb.port=27017
spring.data.mongodb.database=articledb
spring.data.mongodb.username=yinrz
spring.data.mongodb.password=123456
2、建议使用 uri 来连接,username:password@hostname/dbname
spring.data.mongodb.uri=mongodb://yinrz:123456@192.168.200.105:27017/articledb
7.3 副本集环境
1、先创建超级管理员用户root
use admin
db.createUser({user:"root",pwd:"root",roles:["root"]})
2、生成加密文件
# 生成90位加密文件
openssl rand -base64 90 -out ./mongo.keyfile
# 仅为文件所有者提供读取权限
chmod 400 ./mongo.keyfile
3、拷贝加密文件到不同的节点中
cp mongo.keyfile /usr/local/mongodb-27017/
cp mongo.keyfile /usr/local/mongodb-27018/
cp mongo.keyfile /usr/local/mongodb-27019/
4、修改配置文件,指定keyfile加密文件
security:
# KeyFile 鉴权文件
keyFile: /usr/local/mongodb-27017/mongo.keyfile
# 开启认证方式运行
authorization: enabled
5、启动副本集,发现如果不登录,什么数据库都查看操作不了
6、用超级管理员用户登录
use admin
db.auth("root","root")
7、添加普通用户
use articledb
db.createUser({user: "yinrz", pwd: "123456", roles: [{ role: "readWrite", db:"articledb" }]})
8、SpringDataMongoDB 连接认证
在主机地址前添加 用户名:密码@
spring.data.mongodb.uri=mongodb://yinrz:123456@192.168.200.105:27017,192.168.200.105:27018,192.168.200.105:27019/articledb?connect=replicaSet&slaveOk=true&replicaSet=myrs
7.4 分片环境
1、和副本集环境类型,需要生成keyfile加密文件,复制到所有的mongod和mongos服务下
2、mongod 服务配置文件添加
security:
#KeyFile鉴权文件
keyFile: /usr/local/mongodb-sharded/shard01-27018/mongo.keyfile
#开启认证方式运行
authorization: enabled
3、mongos 服务配置文件添加
security:
#KeyFile鉴权文件
keyFile: /usr/local/mongodb-sharded/router-27017/mongo.keyfile
只有相同 keyfile 才能相互通信,所以需要配置 keyFile
所有的数据访问需要开启 authorization: enabled,但 mongos 只做路由,不保存数据,所以不需要添加 authorization: enabled。
4、如果之前没创建超级管理员用户,可以通过 localhost 登录任意一个 mongos 路由,进行超级管理员用户创建。
use admin
db.createUser({user:"root",pwd:"root",roles:["root"]})
5、创建一个普通权限帐号
use articledb
db.createUser({user: "yinrz", pwd: "123456", roles: [{ role: "readWrite", db:"articledb" }]})
6、SpringDataMongoDB 连接认证
在主机地址前添加 用户名:密码@
spring.data.mongodb.uri=mongodb://yinrz:123456@192.168.200.105:27017,192.168.200.105:27117/articledb