MongoDB索引,事务和安全
文章目录
一:事务和锁
1:MongoDB事务机制:不建议使用
MongoDB
作为数据库家族的一员,自然也支持事务机制,只不过相较于InnoDB
的事务机制而言,MongoDB
事务方面并没有那么强大
这倒不是因为官方技术欠缺,而是由于MongoDB
的定位是:大数据、高拓展、高可用、分布式
因此在实现事务时,不仅仅要考虑单机事务,而且需要考虑分布式事务,复杂度上来之后,自然无法做到MySQL-InnoDB
那种单机事务的强大性。
这里也列出MongoDB
事务方面的改进过程,如下:
3.0
版本中,引入WiredTiger
存储引擎,开始支持单文档事务;4.0
版本中,开始支持多文档事务,以及副本集(主从复制)架构下的事务;4.2
版本中,开始支持分片集群、分片式多副本集架构下的事务。
// 开启一个会话
var session = db.getMongo().startSession({
// readPreference:定义读操作的节点优先级和模式
// mode:指定读取模式
// -> primary:只从主节点读取数据;
// -> secondary:只在从节点上读取数据;
// -> primaryPreferred:优先从主节点读取,主节点不可用,转到从节点读取;
// -> secondaryPreferred:优先在从节点读取,从节点不可用,转到主节点读取;
// -> nearest:从可用节点中选择最近的节点进行读取;
readPreference:{mode: "primary"}
});
// 开启事务
session.startTransaction(
{
// readConcern:指定事务的读取模式
// level:指定一致性级别
// --> available:读取已提交的数据,可能包含尚未持久化的事务更改;
// --> snapshot:读取事务开始时的一致快照,不包含未提交的事务更改;
readConcern: {level:"snapshot"}, // 指定读模式为快照读
// writeConcern:指定事务的写入模式
// w:指定写操作的确认级别(同步模式)
// --> [number]:写操作在写入指定数量的节点后,返回写入成功;
// --> majority: 写操作在写入大多数节点(半数以上)后,返回写入成功;
// --> tagSetName:写操作在写入指定标签的节点后,返回写入成功
// j:写入是否应被持久化到磁盘
// --> wtimeout:指定写入确认的超时时间;
writeConcern:{w: "majority"} // 写模式的同步模式级别为半同步,即写入半数以上节点后再返回成功
}
);
// 获取要操作的集合对象
var trx_coll = session.getDatabase("库名").getCollection("集合名");
// 要在事务里执行的CRUD操作
// ......
// 回滚事务命令
session.abortTransaction();
// 提交事务命令
session.commitTransaction();
// 关闭会话命令
session.endSession();
上述命令了解即可,毕竟MongoDB
本身就不适用于强事务的场景,原因如下:
MongoDB
的事务必须在60s
内完成,超时将自动取消(因为要考虑分布式环境);- 涉及到事务的分片集群中,不能有仲裁节点;
- 事务会影响集群数据同步效率、节点数据迁移效率;
- 多文档事务的所有操作,必须在主节点上完成,包括读操作;
综上所述,就算MongoDB
支持事务,可实际使用起来也会有诸多限制,因此在不必要的情况下,不建议使用其事务机制。
2:MongoDB的锁机制
MongoDB 锁机制是其并发控制的重要组成部分,目的是为了确保多线程多用户访问下数据的完整性和一致性,主要分类两大类:MMAPv1 引擎的锁机制和 WiredTiger 引擎的锁机制。
在使用 MMAPv1 存储引擎的 MongoDB 版本中,全局锁时其主要的并发控制手段。全局锁有两种模式:
- 读锁:允许多个读操作共享,但组织任何写操作
- 写锁:独占锁,一旦获取将组织其他读写操作,直至锁释放
WiredTiger 引擎使用了更细粒度的锁机制,主要为:
- 文档锁:锁定单个文档,允许多个并发读操作,但写操作会互斥。这大大减少了锁竞争,提高了并发写入能力,从而使得在高并发场景下也能保持较好的性能。
- 多版本并发控制(MVCC):WiredTiger 实现了一种 MVCC 机制,为每个事务创建数据的多个版本。这样,读操作可以不受写操作的影响,看到事务开始时的一致性视图,而写操作则在新版本上进行,直到事务提交后才会对外可见。这增强了系统的并发能力,同时保证了事务的隔离性。
- 范围锁:在某些情况下,为了保持数据一致性,WiredTiger可能会锁定一个文档范围,防止其他操作修改该范围内的数据。
- 乐观锁:除了传统的锁机制,WiredTiger还采用了乐观锁策略,尤其在处理读写操作时。乐观锁依赖于文档版本控制,每个文档都有一个内部版本号。写操作前先读取版本号,写入时检查版本号是否改变,若未变则成功,否则重试。这种方式减少了锁的使用,提高了并发效率
非常复杂:有时间研究下大佬的笔记
// 这里简单的列出手动操作锁的命令:
// 获取锁
db.collection.fsyncLock();
// 释放锁
db.collection.fsyncUnlock();
二:MongoDB的索引机制
任何数据库都有索引这一核心功能,MongoDB
自然不例外,而且MongoDB
在索引方面特别完善,毕竟是新的数据库,肯定汇集百家之长
Mongo索引官方文档
早版本的MongoDB
中,索引底层默认使用B-Tree
结构
而4.x
版本后,MongoDB
推出了V2
版索引,默认使用变种B+Tree
来作为索引的数据结构(和MySQL
索引的数据结构相同)
1:初始索引
MongoDB
会为每个集合生成一个默认的_id
字段,该字段在每个文档中必须存在,可以手动赋值
如果不赋值则会默认生成一个ObjectId
, 该字段则是集合的主键,MongoDB
会基于该字段创建一个默认的主键索引
后续基于_id
字段查询数据时,会走索引来提升查询效率。
当咱们基于其他字段查询时,由于未使用_id
作为条件,这会导致find
语句走全表查询,即从第一条数据开始,遍历完整个集合,从而检索到目标数据。
当集合中的数据量,达到百万、千万、甚至更高时,意味着效率会直线下滑,在这种情况下,必须得由我们手动为频繁作为查询条件的字段建立索引。
Mongo中的索引分类
- 从字段数量的维度划分:单列,组合,多键,部分
- 从排序的维度划分:升序,降序,多序
- 从功能的维度划分:主键,普通,唯一,全文,空间
- 从数据结构的维度划分:B+Tree,Hash
- 从存储方式的维度划分:聚簇,非聚簇
- 从索引性质的维度划分:稀疏,TTL,隐藏,通配符
2:索引详解
MongoDB
中创建索引的命令:
db.collection.createIndex(<key and index type specification>, <options>);
前面提到的所有索引,都是通过这一个方法创建,不同类型的索引,通过里面的参数和选项来区分,下面说明一下参数和可选项。
第一个参数主要是传字段,以及索引类型,这里可以传一或多个字段,用于表示单列/复合索引。
第二个参数表示可选项,如下:
background
:是否以后台形式创建索引,因为创建索引会导致其他操作阻塞;unique
:是否创建成唯一索引;name
:指定索引的名称;sparse
:是否对集合中不存在的索引字段的文档不启用索引;expireAfterSeconds
:指定存活时间,超时后会自动删除文档;v
:指定索引的版本号;weights
:指定索引的权重值,权值范围是1~99999
,当一条语句命中多个索引时,会根据该值来选择;
以下述集合为例,演示各种索引的创建过程
db.animals.insert([
{_id:1, name:"肥肥", age:3, hobby:"竹子", color:"黑白色"},
{_id:2, name:"花花", color:"黑白色"},
{_id:4, name:"黑熊", age:3, food:{name:"黄金竹", grade:"S"}},
{_id:5, name:"白熊", age:4, food:{name:"翠绿竹", grade:"B"}},
{_id:6, name:"棕熊", age:3, food:{name:"明月竹", grade:"A"}},
{_id:7, name:"红熊", age:2, food:{name:"白玉竹", grade:"S"}},
{_id:8, name:"粉熊", age:6, food:{name:"翡翠竹", grade:"A"}},
{_id:9, name:"紫熊", age:3, food:{name:"烈日竹", grade:"S"}},
{_id:10, name:"金熊", age:6, food:{name:"黄金竹", grade:"S"}}
]);
2.1:单列索引
// 基于name字段,创建一个名为idx_name的单列普通索引,排序方式为降序
db.animals.createIndex(
{name: -1},
{name: "idx_name"}
);
🎉 对于单字段的索引而言,排序方式并不重要,因为索引底层默认是B+Tree
,每个文档之间会有双向指针,为此,MongoDB
基于单字段索引查询时,既可以向前、也可以向后查找数据
创建完成后,可以通过db.animals.getIndexes()
命令查询索引
多键索引
例如现在将集合中的爱好字段,变为一个数组:
{
_id:1,
name:"肥肥",
age:3,
hobby:[
"竹子", "睡觉"
],
color:"黑白色"
}
现在给hobby
字段创建一个索引,这时叫啥索引?多键索引!
因为这里是基于单个数组类型的字段在建立索引,所以MongoDB
会为数组中的每个元素,都生成索引的条目(即索引键)
由于一个文档的数组字段,拥有多个元素,因此会创建多个索引键,这也是“多键索引”的名字由来。
2.2:复合索引
复合索引是指基于多个字段创建的索引,例如:
db.animals.createIndex(
{name:-1, age:1}, // 依据name降序,age升序创建一个聚合索引
{name:"idx_name_age"} // 复合索引的名称
);
🎉 这个排序就有意义了,MongoDB
生成索引键时,会按照指定的顺序,来将索引键插入到树中。
索引键=索引字段的值,比如现在一个文档的
name=张三、age=3
,索引键为张三3
。注意:由于这里的顺序是
{name:-1, age:1}
,所以当排序查询时,支持sort({name:-1,age:1})、sort({name:1,age:-1})
,因为这两个顺序和树的组成顺序要么完全相同、相反而当执行
sort({name:-1,age:-1})、sort({name:1,age:1})
排序查询时,将不会使用索引,因为这时和树的顺序冲突。
2.3:唯一索引
必须创建在不会出现重复值的字段上,基于唯一索引查找数据时,找到第一个满足条件的数据,就会立马停止匹配,毕竟该字段的值在集合中是唯一的
db.animals.createIndex(
{name:1}, // 在name字段上正序创建索引
{unique: true} // 声明是唯一索引
);
只需要将unique
设置为true
即可,如果尝试插入已有的name,将会触发报错
两个都是空也认为是冲突
db.animals.insertOne(
{_id: 66, age: 12}
)
db.animals.insertOne(
{_id: 77, age: 13}
)
这种情况怎么解决呢,声明name索引是稀疏索引即可
// 将刚才的给删除了
db.animals.dropIndex("name_1");
// 在创建一个,这次指明这个索引不但是唯一的,还是稀疏的,允许重复的null值
db.animals.createIndex(
{name: 1},
{unique: true, sparse: true}
)
2.4:部分索引
部分索引即使用字段的一部分开创建索引,但必须要结合partialFilterExpression
选项来实现,
db.animals.createIndex(
{hobby: 1}, // 给 hobby 字段创建索引
{partialFilterExpression: {
hobby: {
// 只为存在hobby字段的文档创建索引
$exists: true,
// 通过$substr操作符,截取前3个字节作为索引键
$expr: {$eq: [{$substr:["$hobby", 0, 3] },"prefix"]}
}
}}
);
其实这就类似于MySQL
中的前缀索引,不过MongoDB
的中的部分索引功能更强大,还可以只为集合中的一部分文档创建索引
db.animal.createIndex(
{age: -1},
// 只为集合中年龄大于2岁的文档创建索引
{partialFilterExpression: {
age: {$gt: 2}
}}
);
2.5:TTL索引
可以基于它实现过期自动删除的效果,主要依靠expireAfterSeconds
选项来创建
只能在Date、ISODate
类型的字段上建立TTL
索引,在其他类型的字段上建立TTL
索引,文档永远不会过期。
db.test_ttl.insertMany([
// new Date()表示插入当前时间
{_id:1, time:new Date()},
{_id:2, time:new Date()},
{_id:3, time:new Date()},
{_id:4, time:new Date()},
{_id:5, notes:"这条数据用于观察TTL删除特性"} // 10s之后,将只剩这条数据
]);
db.test_ttl.find();
db.test_ttl.createIndex(
{time: 1}, // 依据时间字段创建索引
{expireAfterSeconds: 10} // 给定的过期时间为10s
);
2.6:全文索引
在MySQL
中想实现模糊查询,一般会采用like
关键字;而在MongoDB
中想实现模糊查询,官方并没有提供相关方法与操作符,只能通过自己写正则的形式,实现模糊查找的功能
那有没有更好的方法呢?
答案是有,为相应字段创建全文索引即可。
在数据量不大不小(几百万左右)、查询又不是特别复杂的情况下,直接上ElasticSearch、Solr
等中间件,显得有点大材小用
此时全文索引就是这类搜索引擎的平替。相较于MySQL
,MongoDB
提供的全文索引,功能方面会更加强大。
db.animals.createIndex(
{name: "text"},
{name: "full_text_index"}
)
这里对name
字段建立了一个全文索引,和创建普通索引的区别在于:在字段后面加了一个text
。
不过要注意,MongoDB
全文索引停用词、词干和词器的规则,默认为英语,想要更改,这里涉及到创建索引时的两个可选项:
default_language
:指定全文索引停用词、词干和词器的规则,默认为english
;language_override
:指定全文索引语言覆盖的范围,默认为language
;
不过注意,不管任何技术栈的全文索引,对中文的支持都不太友好,分词方面总会有点不完善
所以MongoDB
全文索引直接不支持中文,当你试图通过default_language:"chinese"
时,会直接给你返回报错
当然,正是由于MongoDB
的全文索引不支持中文,因此就算你给一个字符串字段,建立了全文索引后,也无法实现全文搜索,如下:
db.animals.find(
{$text: {$search: "熊"}}
);
⚠️ 在前面给出的集合数据中,name
包含“熊”的数据有好几条,但这条语句执行之后的结果为null
。想要解决这个问题,必须要手动安装第三方的中文分词插件,如mmseg、jieba
等。当然,如果你字段中的值是英文,这自然是支持的,什么都不需要。
2.7:通配符索引
在前面提到过“内嵌文档”这个概念,这是指将另一个文档,以字段值的形式嵌入到一个文档中。
结合MongoDB
可以动态插入各种字段的特性,每个内嵌文档的字段,也可以灵活变化,例如前面给出的数据:
{_id:4, name:"黑熊", age:3, food:{name:"黄金竹", grade:"S"}},
{_id:5, name:"白熊", age:4, food:{name:"翠绿竹", grade:"B"}},
......
这些数据中都内嵌了一个food
文档,虽然现在插入的都是固定的name、grade
字段,但我们可以随时插入新的字段,例如:
db.animals.insertOne(
{
_id:99,
name:"星熊",
age: 1,
food: {
name:"星光竹",
grade:"S",
quality_inspector: ["竹大","竹二"]
}
}
);
这时新插入的文档,其food
字段又多了一个quality_inspector
质检员的属性
对于这种动态变化的字段,可不可以建立索引呢?MongoDB4.2中引入了“通配符索引”来支持对未知或任意字段的查询操作
创建的语法如下:
db.animals.createIndex({"food.$**": 1});
3:explain执行计划
通过该命令,能有效帮咱们分析语句的执行情况,和MySQL的explain作用一致
db.<collection>.find().explain(<verbose>);
explain
方法同样有三个模式可选,这里简单列出来:
queryPlanner
:返回执行计划的详细信息,包括查询计划、集合信息、查询条件、最佳计划、查询方式、服务信息等(默认模式);exectionStats
:列出最佳执行计划的执行情况和被拒绝的计划等信息(即语句最终执行的方案);allPlansExecution
:选择并执行最佳执行计划,同时输出其他所有执行计划的信息;
一般排查find()
查询缓慢问题时,可以先指定第二个模式,查看最佳执行计划的信息;
如果怀疑MongoDB
没选择好索引,则可以再指定第三个模式,查看其他执行计划
如果的确是因为走错了索引,这时你可以通过hint
强制指定要使用的索引,如下:
db.collection_name.find(查询条件).hint(索引名);
执行explain之后,可以发现输出了很多信息,主要关注的就是这个stage这个值:是最重要的字段,相当于mysql explain中的type
带包这本次查询的类型,该字段可能出现的值以及含义如下:
值 | 含义 |
---|---|
COLLSCAN | 扫描整个集合进行查询; |
IXSCAN | 通过索引进行查询; |
COUNT_SCAN | 使用索引在进行count 操作; |
COUNTSCAN | 没使用索引在进行count 操作; |
FETCH | 根据索引键去磁盘拿具体的数据 |
SORT | 执行了sort 排序查询; |
LIMIT | 使用了limit 限制返回行数; |
SKIP | 使用了skip 跳过了某些数据; |
IDHACK | 通过_id 主键查询数据; |
SHARD_MERGE | 从多个分片中查询、合并数据; |
SHARDING_FILTER | 通过mongos 对分片集群执行查询操作; |
SUBPLA | 未使用索引的$or 查询; |
TEXT | 使用全文索引进行查询; |
PROJECTION | 本次查询指定了返回的结果集字段(投影查询) |
// 在age上创建一个索引
db.animals.createIndex({age: -1}, {name: "idx_age"})
// 假设执行的语句如下
db.animals.find({age: {$gt: 6}}).limit(1).skip(1).explain();
这里咱们只需要带SCAN
后缀的,因为其他都属于命令执行的“阶段”,并不属于具体的类型
explain
会将一条语句执行的每个阶段,都详细列出来,每个阶段都会有stage
字段
我们要做的,就是确保每个阶段都能用上索引即可
如果某一阶段出现COLLSCAN
,在数据量较大的情况下,都有可能导致查询缓慢。
其次,咱们需要关心keysExamined、docsExamined
两个字段的值(exectionStats
模式下才能看到)
- 前者代表扫描的索引键数量,后者代表扫描的文档数量,前者越大,代表索引字段值的离散性太差
- 后者的值越大,一般代表着没建立索引。
三:安全与权限管理
1:简单的用户创建和使用
通常为了保证数据安全性,Redis、MySQL、MQ、ES……
,通常都会配置账号/密码,一来可以提高安全等级,二来还可以针对不同库、操作设置权限,极大程度上降低了数据的安全风险。
同样,在MongoDB
中也支持创建账号、密码,以及分配权限,并且还支持角色的概念,可以先为角色分配权限,再为用户绑定角色,从而节省大量重复的权限分配工作。
同时,MongoDB
中还内置了大量常用角色,方便于咱们快速分配权限,不过并没有默认的账号
所以想要启用MongoDB
的访问控制,还需要先创建一个账号:
// 1:切换到admin
use admin;
// 2:创建一个用户,并且赋予root角色(root角色只能分配给admin库)
db.createUser(
{"user": "cui", "pwd": "123456", "roles": ["root"]}
)
// 3:退出客户端
quit;
接着再关闭MongoDB
服务,重新启动时开启访问控制,必须使用账号密码连接才允许操作:
[root@~]# /soft/mongodb/bin/mongod -shutdown -f /soft/mongodb/conf/standalone/mongodb.conf
[root@~]# /soft/mongodb/bin/mongod -auth -f /soft/mongodb/conf/standalone/mongodb.conf
[root@~]# /soft/mongodb/mongosh/bin/mongosh 192.168.229.135:27017
或者这里也可以直接修改配置文件:
[root@~]# vi /soft/mongodb/conf/standalone/mongodb.conf
# 在配置文件结尾加上这两行
security:
authorization: enabled
然后通过不带-auth
的命令启动,效果同样是相同的。
接着先切换到咱们前面创建的cui
库,查询一下animals
集合试试看:
use cui;
db.animals.find({_id:1});
MongoServerError: command find requires authentication
此时就会看到对应报错,提示目前未授权,所以无法执行命令,因此这里需要登录一下,不过登录必须要切换到admin
库下才可以,否则会提示认证失败:
db.auth("cui", "123456");
{ ok: 1 }
use cui;
db.animals.find({_id:1});
[ { _id: 1, name: '肥肥', age: 3, hobby: '竹子', color: '黑白色' } ]
认证成功后,再次切回cui
库查询,此时会发现数据依旧可以查询出来。
当然,如果不想每次连接时都切换到admin
库下登录,然后再切换回来,此时可以在cui
库下再创建一个用户,如下:
// 先切换到cui库
use cui;
// 再在cui库下创建一个cui用户,并分配dbOwner角色
db.createUser(
{
"user":"cui",
"pwd":"123456",
// 这个dbOwner是啥,会在下面的内置角色中说明
"roles":[{"role":"dbOwner", "db":"cui"}]
}
);
// 退出连接
quit;
然后可以再次连接MongoDB
服务,这时直接切换到cui
库下登录后,也照样可以读写数据
2:Mongo中的内置角色
可以通过下述命令来查询MongoDB
所有内置角色:
use admin;
db.runCommand({rolesInfo: 1, showBuiltinRoles: true});
角色 | 含义 |
---|---|
root | 超级管理员权限,可以执行任何操作; |
read | 只读用户,不允许对数据库执行写入操作; |
readWrite | 读写用户,允许对数据执行读写操作; |
dbAdmin | 数据库管理员(如创建和删除数据库),不允许读写数据; |
userAdmin | 用户管理员(如创建和删除用户),不允许读写数据; |
dbOwner | 同时拥有dbAdmin、userAdmin 两个角色的权限,且允许读写数据; |
backup | 具有备份和恢复权限,不允许读写数据; |
restore | 只具有数据恢复权限,不允许读写数据; |
clusterAdmin | 集群超级管理员,可以执行集群中任意操作,允许读写数据; |
clusterManager | 集群管理员,只可以管理集群节点、配置等; |
clusterMonitor | 集群监视员,允许监控集群的状态和性能,不允许读写数据; |
这些内置角色,可以在创建用户的时候分配,一个用户同时可以绑定多个角色。但如果你想要的权限,内置角色并不提供,也可以自定义角色
3:Mongo自定义角色
自定义角色的语法
use cui;
db.createRole(
{
// 自定义角色的名称
role:"xxxRole",
// 自定义角色拥有的权限集
privileges: [
{
// 自定义角色可操作的资源
resource:
{
// 当前角色可操作cui库
db:"cui",
// 当前角色具体可操作的集合(多个传数组,所有写"")
collection:""
},
// 当前角色拥有的权限
actions: ["find", "update", "insert", "remove"]
}
],
// 当前角色是否继承其他角色,如果指定了其他角色,当前角色自动继承父亲的所有权限
roles: []
}
);
通过该方式,诸位可以灵活的创建出各种适用于业务的角色,最后再附上一些相关命令:
// 给指定角色增加权限 ---------> grantPrivilegesToRole
db.grantPrivilegesToRole(
"角色名称",
[{
resource: {
db: "库名",
collection: ""
},
actions: ["权限1","……"]
}]
);
// 回收指定角色的权限 ----------> revokePrivilegesFromRole
db.revokePrivilegesFromRole(
"角色名称",
[{
resource: {
db: "库名",
collection: ""
},
actions: ["权限1","……"]
}]
);
// 删除角色(要先进入角色所在的库)
use cui;
db.dropRole("角色名称");
// 查看当前库的所有角色
show roles;
// 查看当前库中所有用户
show users;