Mongodb
一、介绍
MongoDB是一个基于分布式文件存储的非关系型数据库,由C++语言编写,存储bson格式数据,方便WEB应用扩展开发。
bson是一种类似于json的二进制形式的存储格式,简称 binary json,支持内嵌的文档对象和数组对象,但是bson相对于json增加了Date和BinData类型。
官网:https://www.mongodb.com/
核心概念:
- 库(database):用于隔离不同应用数据,默认数据库为test
- 集合(collection):存在于库中,没有固定的结构,可以插入不同格式类型的数据
- 文档(document):集合的一条条记录,是一组K-V键值对,每一条文档不需要设置相同的字段并且相同字段数据类型也可以不一样
RDBMS | MongoDB |
---|---|
数据库 | 库 |
表 | 集合 |
行 | 文档 |
列 | 字段 |
二、docker安装
docker pull mongo:5.0.5 //拉取mongodb镜像
docker run -d -p 27017:27017 --name mongo --privileged=true
-v /root/mongodb/data:/data/db //挂载数据卷
-e MONGO_INITDB_ROOT_USERNAME=用户名
-e MONGO_INITDB_ROOT_PASSWORD=密码
mongo:5.0.5 //运行镜像
docker exec -it mongodb bash //进入容器
mongo //进入客户端
或者
docker exec -it mongodb mongo
use admin
db.auth('用户名', '密码') //登录认证
root用户需要切换到admin库进行认证,认证后才能进行操作:
安装可视化工具Navicat for MongoDB,测试加密:
显示隐藏的库:
三、基本命令
参考文档:https://www.mongodb.com/docs/manual/
1.库
查询所有库:
show databases
#或者
show dbs
- admin:将一个用户添加到这个数据库,将获得所有数据库的权限
- local:存储限于单台服务器的数据,这个数据库数据不会被复制
- config:用于保存mongo分片相关信息
显示当前库:
db
创建库:
use 库名
注意:当数据库中没有数据时默认不显示
插入一条文档后显示
删除库:
db.dropDatabase()
2.集合
查看库中所有集合:
show collections;
#或者
show tables;
创建集合:
db.createCollection('集合名称', [options])
options:
字段 | 解释 |
---|---|
capped | 如果为true,创建固定大小集合,必须指定size值 (当集合达到最大值时会覆盖最早的文档) |
size | 指定集合存储的最大值字节数 |
max | 指定集合能够存放文档的最大数量 |
删除集合:
db.集合名称.drop()
3.文档
插入文档:
db.集合名称.insert({})
db.集合名称.insert([{},{}]) #插入多条文档
或者
db.集合名称.insertMany([{},{}])
# js脚本方式
for(let i = 0; i < 10; i++)
{
db.集合名称.insert({});
}
注意:每一个文档都有一个_id
作为唯一标识,如果没有指定的话会自动生成
删除文档:
db.集合名称.remove({},[option])
db.集合名称.remove({}) #填空 删除全部文档
option
justOne: 如果为true或1,则只删除一个文档,默认为false删除所有匹配的文档
注意:如果通过_id
匹配删除文档,对于自动生成的id需要加上ObjectId()函数才能删除
更新文档:
db.集合名称.update({},{},[option])
option
upsert: 默认为false 如果设置为true表示不存在更新的文档时插入新的文档
multi: 默认为false 只更新一条文档 如果设置为true表示更新所有匹配的文档
注意:更新操作相当于先删除匹配的文档再插入新的文档,如果要保存原来的字段需要加上$set:
更新所有匹配的文档:
不存在更新的文档时插入新的文档:
4.查询
文档查询:
db.集合名称.find({}, {projection})
projection: 指定返回的字段 1 返回 0 不返回
db.集合名称.find({}, {projection}).pretty() #以格式化的方式显示所有文档
运算符 | 格式 |
---|---|
等于 | {key:value} |
小于 | {key:{$lt:value}} |
小于等于 | {key:{$lte:value}} |
大于 | {key:{$gt:value}} |
大于等于 | {key:{$gte:value}} |
不等于 | {key:{$ne:value}} |
查询年龄等于19:
查询年龄小于或大于19:
AND与连接:
db.集合名称.find({ key:value, key:value})
OR或连接:
db.集合名称.find(
{
$or:[
{ key:value}, { key:value}
]
}
)
模糊查询(正则表达式):
db.集合名称.find({key:/value/})
按数组长度查询:
db.集合名称.find({key:{$size:value}})
排序:
db.集合名称.find().sort({key:1/-1}) # 1 升序 -1 降序
分页:
db.集合名称.find().skip(start).limit(rows) #start 起始文档 rows 查询几条 类似 limit start rows
注意:find()不是查询全部文档,默认只显示20条,输入it
查看更多
查询文档总条数:
db.集合名称.find().count()
去重:
db.集合名称.distinct('字段')
设置projection
不显示age字段(1 返回 0 不返回):
按字段类型查询:
db.集合名称.find({key:{$type:value}})
聚合查询:
db.集合名称.aggregate(
[
{
$group:{_id:"$分组字段名称", '聚合名称':{$聚合操作}}
}
]
)
聚合操作 | 描述 |
---|---|
$sum | 计算总和 |
$avg | 计算平均值 |
$min | 计算最小值 |
$max | 计算最大值 |
$push | 将值加入一个数组(可重复) |
$addToSet | 将值加入一个数组(不可重复) |
$first | 获取第一个文档 |
$last | 获取最后一个文档 |
原始数据:
按照性别分组查询最大年龄、最小年龄和平均年龄:
按照性别分类将爱好加入一个数组返回、获取第一个和最后一个爱好
5.索引
索引用来提高查询的效率,和关系型数据库类似,遵循最左前缀法则
创建索引:
db.集合名称.createIndex(keys, option) #keys 指定建立索引的字段 1 字段升序 -1 字段降序
option
background: 由于建立索引会阻塞其他数据库操作,background指定以后台方式创建索,默认为false
unique: 默认为false,为true时建立唯一索引
name: 指定索引名称,如果没有指定会自动生成
expireAfterSeconds: 对于索引的数据设置过期时间,过期后数据自动删除
weights: 指定索引的权值,在1~99999之间,表示索引之间的优先
查看索引的大小:
db.集合名称.totalIndexSize()
查看索引:
db.集合名称.getIndexes()
删除索引:
db.集合名称.dropIndexes("索引名称") #删除指定索引
db.集合名称.dropIndexes() #删除所有索引
四、SpringBoot整合
导入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
application.yaml:
spring:
data:
mongodb:
# uri: mongodb://ip地址:27017/库名 #未设置账号密码时
host: ip地址
port: 27017
database: 库名
username: 用户名
password: 密码
注意:uri和host、port、username、password、replicaSetName配置冲突,只能使用其中的一种
集合操作:
@SpringBootTest
class MongoDbApplicationTests {
@Autowired
private MongoTemplate mongoTemplate;
@Test
void createCollection() {
boolean exists = mongoTemplate.collectionExists("b");
if(!exists) {
mongoTemplate.createCollection("b");
}
}
@Test
void dropCollection() {
mongoTemplate.dropCollection("b");
}
}
文档操作:
@Document("users") //User类的对象相当于一条记录
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
@Id //映射文档中的_id
private Integer id;
@Field //映射文档中的字段
private String name;
@Field
private Integer age;
}
相关注解:
注解 | 解释 |
---|---|
@Document | 标注在类上,表示该类的对象相当于一条文档,value属性指明集合的名称 |
@Id | 标注在成员变量上,表示文档的_id值 |
@Field | 标注在成员变量上,表示文档的字段名称 |
@Transient | 标注在成员变量上,不参与文档的序列化 |
@SpringBootTest
class MongoDbApplicationTests {
@Autowired
private MongoTemplate mongoTemplate;
@Test
void insertDocument() {
User user1 = new User(1, "张三", 18);
User user2 = new User(1, "李四", 19);
//mongoTemplate.insert(user1); //当_id存在时报错 DuplicateKeyException
//mongoTemplate.save(user2); //当_id存在时则会更新数据
ArrayList<User> users = new ArrayList<>();
users.add(new User(2, "阿四", 25));
users.add(new User(3, "小陈", 35));
mongoTemplate.insert(users, User.class); //insert可以插入多条文档
//mongoTemplate.insert(users, "users");
}
@Test
void queryDocument() {
//根据id查询
System.out.println("_id=1的文档:" + mongoTemplate.findById(1, User.class));
System.out.println("-----------------------------------");
//查询所有
System.out.println("所有文档:" + mongoTemplate.findAll(User.class));
System.out.println("所有文档:" + mongoTemplate.find(new Query(), User.class)); //query 查询对象为null
System.out.println("-----------------------------------");
//条件查询
System.out.println("名字是张三的文档:" + mongoTemplate.find(Query.query(Criteria.where("name").is("张三")), User.class));
System.out.println("年龄小于30的文档:" + mongoTemplate.find(Query.query(Criteria.where("age").lt(30)), User.class));
System.out.println("-----------------------------------");
//and or
System.out.println("年龄为30并且id为3的文档:" + mongoTemplate.find(Query.query(Criteria.where("age").gt(30).and("id").is(3)), User.class));
Criteria criteria = new Criteria();
criteria.orOperator(Criteria.where("age").gt(30), Criteria.where("age").lt(20));
System.out.println("年龄大于30或小于20的文档:" + mongoTemplate.find(Query.query(criteria), User.class));
System.out.println("-----------------------------------");
//排序
System.out.println("按年龄降序:" + mongoTemplate.find(new Query().with(Sort.by(Sort.Order.desc("age"))), User.class));
System.out.println("-----------------------------------");
//分页
System.out.println("分页:" + mongoTemplate.find(new Query().skip(0).limit(2), User.class));
System.out.println("-----------------------------------");
//总条数
System.out.println("文档总条数:" + mongoTemplate.count(new Query(), User.class));
System.out.println("-----------------------------------");
System.out.println("对于年龄去重:" + mongoTemplate.findDistinct(new Query(), "age", User.class, Integer.class));
}
@Test
void updateDocument() {
//更新符合条件的第一条文档
mongoTemplate.updateFirst(Query.query(Criteria.where("id").is(1)), new Update().set("age", 20), User.class);
//更新符合条件的所有文档
mongoTemplate.updateMulti(new Query(), new Update().set("age", 18),User.class);
//不符合条件插入新的文档
mongoTemplate.upsert(Query.query(Criteria.where("id").is(4)), new Update().setOnInsert("name", "小四").setOnInsert("age", 1), User.class);
}
@Test
void deleteDocument() {
//删除所有文档
mongoTemplate.remove(new Query(), User.class);
//删除年龄为18的所有文档
mongoTemplate.remove(Query.query(Criteria.where("age").is(18)), User.class);
}
}
五、集群
1.副本集群
MongoDB副本集是可以自动故障恢复的主从集群,由一个主节点和多个从节点组成,当主节点故障时,会从从节点中选举出一个新的主节点,保证系统的高可用
选举示意图:
搭建一主两从副本集群:
节点 | 端口 |
---|---|
主节点 | 27017 |
从节点1 | 27018 |
从节点1 | 27019 |
创建对应的目录存放数据、配置文件和日志信息
tree -d //创建对应的目录存放数据、配置文件和日志信息
master主节点mongo.conf:
storage:
dbPath: /data/db # 存储数据目录
journal:
enabled: true
systemLog:
destination: file
logAppend: true
path: /var/log/mongodb/mongod.log # 存储日志目录
net:
port: 27017 # 端口号
bindIp: 0.0.0.0 # 允许外部访问
processManagement:
timeZoneInfo: /usr/share/zoneinfo
security:
keyFile: /opt/keyfile # 内部认证文件路径
authorization: enabled # 开启认证
replication:
replSetName: "rs0" # 副本集名称
slave从节点配置文件类似,仅修改端口号为27018、27019即可
生成keyfile文件用于节点之间的内部认证:
openssl rand -base64 756 > keyfile
chmod 400 keyfile //提供读取权限
chown 999 keyfile //!设置文件所有者(不然后面会报错)
运行容器:
//运行主节点
docker run -d -p 27017:27017 --name mongoMaster --privileged=true
-v /root/mongoReplication/master/data:/data/db //挂载数据目录
-v /root/mongoReplication/keyfile:/opt/keyfile //挂载keyfile文件
-v /root/mongoReplication/master/configdb/:/data/configdb //挂载配置目录
-v /root/mongoReplication/master/logs/:/var/log/mongodb/ //挂载日志目录
mongo:5.0.5 mongod --config /data/configdb/mongod.conf //通过配置文件运行
//运行从节点
docker run -d -p 27018:27018 --name mongoSlave1 --privileged=true
-v /root/mongoReplication/slave1/data:/data/db
-v /root/mongoReplication/keyfile:/opt/keyfile
-v /root/mongoReplication/slave1/configdb/:/data/configdb
-v /root/mongoReplication/slave1/logs/:/var/log/mongodb/
mongo:5.0.5 mongod --config /data/configdb/mongod.conf
docker run -d -p 27019:27019 --name mongoSlave2 --privileged=true
-v /root/mongoReplication/slave2/data:/data/db
-v /root/mongoReplication/keyfile:/opt/keyfile
-v /root/mongoReplication/slave2/configdb/:/data/configdb
-v /root/mongoReplication/slave2/logs/:/var/log/mongodb/
mongo:5.0.5 mongod --config /data/configdb/mongod.conf
进入主节点容器内部:
docker exec -it mongoMaster mongosh
var config = {
_id:"rs0",
members:[
{_id:0, host:"外网IP:27017"},
{_id:1, host:"外网IP:27018"},
{_id:2, host:"外网IP:27019"}
]
}
rs.initiate(config) //启动副本集
//创建用户用于认证
db.createUser(
{
user:"用户名",
pwd:"密码",
roles:[
{role: "root", db:"admin" }
]
}
)
//从节点暂时可读
rs.secondaryOk()
db.getMongo().setReadPref("primaryPreferred")
rs.conf() //显示副本集配置
测试:
主节点执行写操作,从节点同步
主节点可读写,从节点只读
当主节点宕机后,从节点会选举出一个新的主节点
当主节点恢复后会变成从节点
副本集扩缩容
rs.add({host:"IP:端口号"}) # 添加新的节点到副本集
rs.remove("IP:端口号") # 从副本集中删除节点(一次只能删除一个节点)
rs.reconfig(config) # 重新加载配置(一次可删除多个节点)
创建一个新的节点:
docker run -d -p 27020:27020 --name mongoSlave3 --privileged=true
-v /root/mongoReplication/newNode/data:/data/db
-v /root/mongoReplication/keyfile:/opt/keyfile
-v /root/mongoReplication/newNode/configdb/:/data/configdb
-v /root/mongoReplication/newNode/logs/:/var/log/mongodb/
mongo:5.0.5 mongod --config /data/configdb/mongod.conf
添加到副本集:
删除节点:
使用splice删除节点,重新加载配置:
SpringBoot连接副本集日志:
spring:
data:
mongodb:
host: 外网IP # 主节点
port: 端口 # 主节点
database: test # 操作数据库test
authentication-database: admin # 认证数据库admin
replica-set-name: rs0 # 副本集名称
username: 用户名
password: 密码
2.分片集群
当客户端访问Router执行读写操作时,Router会去访问Config Servers(配置服务器)获取分片集群的信息,进而执行相应的操作
- Router:充当查询路由器,提供客户端和分片集群之间的服务,对外透明像单节点一样
- Shard:每个分片都包含分片数据的子集,可以部署为副本集,保证高可用
- Config Servers:配置服务器存储集群的元数据和配置设置,必须部署为副本集
结构图:
节点 | 端口 |
---|---|
路由节点 | 27017 |
分片1主节点 | 27018 |
分片1从节点 | 27019 |
分片1从节点 | 27020 |
分片2主节点 | 27021 |
分片2从节点 | 27022 |
分片2从节点 | 27023 |
分片3主节点 | 27024 |
分片3从节点 | 27025 |
分片3从节点 | 27026 |
配置主节点 | 27027 |
配置从节点 | 27028 |
配置从节点 | 27029 |
分片服务器配置文件(其他分片类似):
storage:
dbPath: /data/db
journal:
enabled: true
systemLog:
destination: file
logAppend: true
path: /var/log/mongodb/mongod.log
net:
port: 27018
bindIp: 0.0.0.0
processManagement:
timeZoneInfo: /usr/share/zoneinfo
security:
keyFile: /opt/keyfile
authorization: enabled
replication:
replSetName: "s0" # 分片1
sharding:
clusterRole: shardsvr # 作为分片
# 分片服务器
var config = {
_id:"s0", # 分片1
members:[
{_id:0, host:"外网IP:27018"},
{_id:1, host:"外网IP:27019"},
{_id:2, host:"外网IP:27020"}
]
}
rs.initiate(config) # 启动副本集
var config = {
_id:"s1", # 分片2
members:[
{_id:0, host:"外网IP:27021"},
{_id:1, host:"外网IP:27022"},
{_id:2, host:"外网IP:27023"}
]
}
rs.initiate(config) # 启动副本集
var config = {
_id:"s2", # 分片3
members:[
{_id:0, host:"外网IP:27024"},
{_id:1, host:"外网IP:27025"},
{_id:2, host:"外网IP:27026"}
]
}
rs.initiate(config) # 启动副本集
# 配置服务器
var config = {
_id:"c0",
configsvr: true, # 指定副本集用于配置服务器
members:[
{_id:0, host:"外网IP:27027"},
{_id:1, host:"外网IP:27028"},
{_id:2, host:"外网IP:27029"}
]
}
rs.initiate(config) # 启动副本集
db.createUser(
{
user:"用户名",
pwd:"密码",
roles:[
{role: "root", db:"admin" }
]
}
)
分片服务器副本集:
配置服务器配置文件:
storage:
dbPath: /data/db
journal:
enabled: true
systemLog:
destination: file
logAppend: true
path: /var/log/mongodb/mongod.log
net:
port: 27027
bindIp: 0.0.0.0
processManagement:
timeZoneInfo: /usr/share/zoneinfo
security:
keyFile: /opt/keyfile
authorization: enabled
replication:
replSetName: "c0" # 配置服务器副本集名称
sharding:
clusterRole: configsvr # 作为配置服务器
docker run -d -p 27017:27017 --name router --privileged=true
-v /root/cluster/keyfile:/opt/keyfile
-v /root/cluster/router/logs:/var/log/mongodb/
-e MONGO_INITDB_ROOT_USERNAME=用户名
-e MONGO_INITDB_ROOT_PASSWORD=密码
mongo:5.0.5 mongos --keyFile /opt/keyfile # 这里使用mongos
--bind_ip=0.0.0.0,:: # 允许外部访问
--configdb 副本集名称/各个节点的IP:端口,... # 配置服务器副本集
# 添加三个分片
sh.addShard("s0/外网IP:27018,外网IP:27019,外网IP:27020")
sh.addShard("s1/外网IP:27021,外网IP:27022,外网IP:27023")
sh.addShard("s2/外网IP:27024,外网IP:27025,外网IP:27026")
sh.status() # 查看分片集群信息
sh.enableSharding("库名") # 数据库开启分片
sh.shardCollection("库名.集合名", { _id: "hashed"}) # 指定分片键按_id哈希散列对集合进行分片
sh.shardCollection("库名.集合名", { _id: 1}) # 指定分片键按_id范围对集合进行分片
插入数据测试分片:
# 插入1000数据 对于不同的分片键查看分片情况
for(let i = 0; i < 1000; i++){
db.users.insert({_id:i, name:"王" + i})
}
分片键_id单调递增,使用范围分片将导致大量数据存入同一个分片,分布不均匀
使用哈希分片,将数据尽量平均地分布在每一个分片