Java-MongoDB详细记录
⼀、MongoDb的体系结构
NoSql的概念
NoSQL(NoSQL = Not Only SQL ),意即“不仅仅是 SQL ”,关系型数据库特点是规范的数据结构,表与表之间通过外键进⾏关联,这些特征使我们对数据的管理更加清晰和严谨,但随着互联网的发展数据成爆炸式的增⻓我们对数据库需要更好的灵活性和更快的速度。这就是NoSql可以做到 的。它不需要预先定义模式,没有主外键关联、支持分片、支持复本。
NoSql的分类
键值(Key-Value)存储数据库
这⼀类数据库主要会使⽤到⼀个哈希表,这个表中有⼀个特定的键和⼀个指针指向特定的 数据。Key/value模型对于IT系统来说的优势在于简单、易部署。但是如果DBA只对部分值进行查询或更新的时候,Key/value就显得效率低下了。举例如:Redis.
列存储数据库
这部分数据库通常是⽤来应对分布式存储的海量数据。键仍然存在,但是它们的特点是指 向了多个列。这些列是由列家族来安排的。如:Cassandra, HBase, Riak.
⽂档型数据库
⽂档型数据库的灵感是来⾃于Lotus Notes办公软件的,⽽且它同第⼀种键值存储相类似。该类型的数据模型是版本化的⽂档,半结构化的⽂档以特定的格式存储,⽐如JSON。⽂档型数据库可以看作是键值数据库的升级版,允许之间嵌套键值。⽽且⽂档型数据库⽐键值 数据库的查询效率更⾼。如:CouchDB, MongoDb。
图形(Graph)数据库
图形结构的数据库同其他⾏列以及刚性结构的SQL数据库不同,它是使⽤灵活的图形模 型,并且能够扩展到多个服务器上。NoSQL数据库没有标准的查询语⾔(SQL),因此进⾏数据 库查询需要制定数据模型。许多NoSQL数据库都有REST式的数据接⼝或者查询API。
MongoDb的逻辑组成
image.png逻辑结构与关系数据库的对比
关系型数据库 | MongoDb |
---|---|
database(数据库) | database(数据库) |
table (表) | collection( 集合) |
row( 行) | document( BSON 文档) |
column (列) | field (字段) |
index(唯一索引、主键索引) | index (全文索引) |
join (主外键关联) | embedded Document (嵌套文档) |
primary key(指定1至N个列做主键) | primary key (指定_id field做为主键) |
aggreation(groupy) | aggreation (pipeline mapReduce) |
二、MongoDb使用
mongoDb 由C++编写,下载下来的包可以直接启动。
参数 | 说明 |
---|---|
dbpath | 数据库目录,默认/data/db |
bind_ip | 监听IP地址,默认全部可以访问 |
port | 监听的端口,默认27017 |
logpath | 日志路径 |
logappend | 是否追加日志 |
auth | 是开启用户密码登陆 |
fork | 是否已后台启动的方式登陆 |
config | 指定配置文件 |
mongo shell 是一个js 控台,可以执行js 相关运算
数据库与集合的基础操作
#查看数据库
show dbs;
#切换数据库
use luban;
#创建数据库与集合,在插入数据时会自动 创建数据库与集和
db.friend.insertOne({name:"wukong",sex:"man"});
#查看集合
show tables;
show collections;
#删除集合
db.friend.drop();
#删除数据库
db.dropDatabase();
MongoDB CRUD
数据插入
//插入单条
db.friend.insertOne({name:"wukong",sex:"man"});
// 插入多条
db.friend.insertMany([
{name:"wukong",sex:"man"},
{name:"diaocan",sex:"woman",age:18,birthday:new Date("1995-11-02")},
{name:"zixiao",sex:"woman"}]);
// 指定ID
db.friend.insert([
{_id:1,name:"wokong",sex:"man",age:1},
{_id:2,name:"diaocan",sex:"women",birthday:new Date("1988-11-11")}])
注意:数据插入,一个集合内的数据格式可以完全不相同,但不建议这样,每条数据都有唯一id,id默认自动生成,但也可以指定,前提是不能重复,否则报错。
数据查询
a.基于条件的基础查询
b.$and、$or、$in、$gt、$gte、$lt、$lte 运算符
c.基于 sort skip limit 方法实现排序与分页
d.嵌套查询
e.数组查询
f.数组嵌套查询
值运算:$in、$gt、$gte、$lt、$lte、$all
$in:值在范围内存在。
$gt:大于值。
$gte:大于等于值。
$lt:小于值。
$lte:小于等于值。
$all:值在范围内都存在。
逻辑运算:$and、$or
其值为多个逻辑的组合运算,后面跟中括号,中括号包括多个大括号。基于具体的值进行运算
#基于ID查找
db.emp.find({_id:1101})
#基于属性查找
db.emp.find({"name":"鲁班"})
# && 运算 与大于 运算
db.emp.find({"job":"讲师","salary":{$gt:8000}})
# in 运算 工作为"讲师"或者"客服部"
db.emp.find({"job":{$in:["讲师","客服部"]}})
# or 运算 工作为"讲师"或者"客服部" || 名字是貂蝉 或者 年龄18
db.emp.find({$or:[{job:"讲师" },{job:"客服部"}] })
db.friend.find({$or:[{"name":"貂蝉"},{age:{$gte:18}}]});
- 排序与分页
# 首先按dep升序salary降序排列,然后从第5条开始,最后选择2条数据。
db.emp.find().sort({dep:1,salary:-1}).skip(5).limit(2)
- 嵌套查询
# 数据
{_id:"001",name:"陈霸天",grade:{redis:87,dubbo:90,zookeper:85}}
# 错误示例:无结果
db.student.find({grade:{redis:87,dubbo:90 });
# 错误示例:无结果
db.student.find({grade:{redis:87,dubbo:90,zookeper:85} })
# 正确示例:基于复合属性查找 时必须包含其所有的值 并且顺序一至
db.student.find({grade:{redis:87,zookeper:85,dubbo:90} })
#基于复合属性当中的指定值 查找。注:名称必须用双引号
db.student.find({"grade.redis":87});
db.student.find({"grade.redis":{"$gt":80}});
- 数组查询
# 数据
{_id:"001",name:"陈霸天",subjects:["redis","zookeper","dubbo"]}
# 无结果
db.subject.find({subjects:["redis","zookeper"]})
# 无结果
db.subject.find({subjects:["zookeper","redis","dubbo"]})
# 与嵌套查询一样,必须是所有的值 并且顺序一至
db.subject.find({subjects:["redis","zookeper","dubbo"]})
# $all 匹配数组中包含该两项的值。注:顺序不作要求
db.subject.find({subjects:{"$all": ["redis","zookeper"]}})
# 简化数组查询
db.subject.find({subjects:"redis"})
# 简化数组查询 ,匹配数组中存在任意一值。与$all相对应
db.subject.find({subjects:{$in: ["redis","zookeper"]}})
- 数组嵌套查询
# 数据
{_id:"001",name:"陈霸天",subjects:[{name:"redis",hour:12},{name:"java",hour:12}]}
# 基础查询 ,必须查询全部,且顺序一至
db.subject2.find({subjects:{name:"redis",hour:12} })
# 指定查询第一个数组 课时大于12
db.subject2.find({"subjects.0.hour":{$gt:12}})
# 查询任科目 课时大于12
db.subject2.find({"subjects.hour":{$gt:12}})
# $elemMatch 元素匹配,指定属性满足,且不要求顺序一至
db.subject2.find({subjects:{$elemMatch:{name:"redis",hour:12}}})
# 数组中任意元素匹配 不限定在同一个对象当中
db.subject2.find({"subjects.name":"mysql","subjects.hour":120})
- 数据的修改与删除
db.表.update( 条件,{设置值},false,false )
$set :设置值
$unset :删除指定字段
$inc:自增
# 设置值
db.emp.update({_id:1101} ,{ $set:{salary:10300} })
# 自增
db.emp.update({_id:1101} ,{ $inc:{salary:200}})
# 基于条件 更新多条数据
# 只会更新第一条
db.emp.update({"dep":"客服部"},{$inc:{salary:100}})
# 更新所有 匹配的条件
db.emp.updateMany({"dep":"客服部"},{$inc:{salary:100}})
# $unset删除字段
db.subject2.update({"_id":"001"},{$unset:{"subjects":1}})
- 嵌套数组修改
$addToSet : 添加至集合 ( 命令区分大小写)
$push: 推送至集合
$pull:从集合中删除
$pop:删除集合中的 两端未尾元素。
# $addToSet:添加至集合,多次执行不会重复插入
db.subject2.update({"_id":"001"},{$addToSet:{"subjects":{"name":"mongodb","hour":20}}})
# $push:推送至集合,不判断重复
db.subject2.update({"_id":"001"},{$push:{"subjects":{"name":"mongodb","hour":20}}})
# $pull:将匹配的元素项,从集合中全部删除
db.subject2.update({"_id":"001"},{$pull:{"subjects":{"name":"mongodb","hour":20}}})
# 仅匹配一个字段
db.subject2.update({"_id":"001"},{$pull:{"subjects":{"name":"mongodb"}}})
# $addToSet、$push、$pull 均可操作数组,效果等同于执行多次数组中元素。
db.subject2.update({"_id":"001"},{$addToSet:{"subjects":[{"name":"mongodb","hour":20},{"name":"mongodb2","hour":22}]}})
# $pop:删除集合中的 两端未尾元素。
# -1 删除队首 元素
db.subject2.update({"_id":"001"},{$pop:{"subjects":-1}})
# 1 删除队尾元素
db.subject2.update({"_id":"001"},{$pop:{"subjects":-1}})
注:值仅能是 1 或 -1, 不能是其它值
# upsert 更新时如果记录不存在,将会进行插入操作
db.subject2.update({"_id":"009"},{$set:{"name":"陈陈"}},true)
删除
// 基于查找删除
db.emp.deleteOne({_id:1101})
// 删除整个集合
db.project.drop()
// 删除库
db.dropDatabase()
三、mongoDB的聚合操作
mongoDB的聚合操作主要分为pipeline和mapRedurce。pipeline 速度快,但只能运行在单机上,适合数据量小的实时聚合操作。mapRedurce 可以运行在分布式节点,适适大数量并且复杂的聚合分析操作。
Pipeline 聚合
pipeline 聚合其特性是运行速度快,只能运行在单机上,并且对资源的使用有一定限制如下:
- 单个的聚合操作耗费的内存不能超过20%
- 返回的结果集大小在16M以内
语法说明
aggredate 方法接收任意多个参数,每个参数都是一个具体类别的聚合操作,通过参数的顺序组成一个执行链。每个操作执行完后将返回结果交给下一个操作。值到最后产出结果。
Pipeline相关运算符
$match :匹配过滤聚合的数据
$project:返回需要聚合的字段
$group:统计聚合数据 ,必须指定_id 列.
- $max:求出最大值
- $sum:求和
- $avg:求平均值
- $push: 将结果插入至一个数组当中
- $addToSet:将结果插入至一个数组当中,并去重
- $first:取第一个值
- $last:取最后一个值
$limit:用来限制MongoDB聚合管道返回的文档数。
$skip:在聚合管道中跳过指定数量的文档,并返回余下的文档。
$unwind:(flatmap)将文档中的某一个数组类型字段拆分成多条,每条包含数组中的一个值。
$sort:将输入文档排序后输出。
示例:
# $match 条件过滤
db.emp.aggregate({$match:{"job":"讲师"}})
# $project 指定列返回
#返回指定例,_id 自动带上
db.emp.aggregate({$match:{"job":"讲师"}},{$project:{"job":1,"salary":1}})
#返回指定列,并修改列名
db.emp.aggregate({$match:{"job":"讲师"}},{$project:{"工作":"$job","薪水":"$salary"}})
# $group 操作 ( 必须指定_id 列)
# 基于工作分组,并求出薪水总和
db.emp.aggregate({$group:{_id:"$job",total:{$sum:"$salary"}}})
# 求出薪水最大值
db.emp.aggregate({$group:{_id:"$job",total:{$max:"$salary"}}})
# 将所有薪水添加列表
db.emp.aggregate({$group:{_id:"$job",total:{$push:"$salary"}}})
# 将所有薪水添加列表 并去重
db.emp.aggregate({$group:{_id:"$job",total:{$addToSet:"$salary"}}})
# 聚合操作可以任意个数和顺序的组合
# 二次过滤
db.emp.aggregate({$match:{"job":"讲师"}},{$project:{"工作":"$job","薪水":"$salary"}},{$match:{"薪水":{$gt:8000}}})
# $skip 与 $limit 跳转 并限制返回数量
db.emp.aggregate({$group:{_id:"$job",total:{$push:"$salary"}}},{$limit:4},{$skip:2});
# sort 排序
db.emp.aggregate({$project:{"工作":"$job","salary":1}},{$sort:{"salary":1,"工作":1}});
# unwind 操作,将数组拆分成多条记录
db.emp.aggregate({$group:{_id:"$job",total:{$push:"$salary"}}},{$unwind:"$total"});
MapRedurce 聚合
mapRedurce 非常适合实现非常复杂 并且数量大的聚合计算,其可运行在多台节点上实行分布式计算。
MapReduce 现大量运用于hadoop大数据计算当中,其最早来自于google 的一遍论文,解决大PageRank搜索结果排序的问题。其大至原理如下:
- mongodb中mapRedurce的使用流程
1.创建Map函数,
2.创建Redurce函数
3.将map、Redurce 函数添加至集合中,并返回新的结果集
4.查询新的结果集
// 创建map 对象 ,group key,value以数组形式展示
var map1=function (){
emit(this.job,1); // 内置函数 key,value
}
// 创建reduce 对象 ,接收key,以及values[value,value,value,value]
var reduce1=function(key,values){
return Array.sum(values);
}
// 执行mapReduce 任务 并将结果放到新的集合 result 当中
db.emp.mapReduce(map1,reduce1,{out:"result"});
// 查询新的集合
db.result.find()
# 使用复合对象作为key
var map2=function (){
emit({"job":this.job,"dep":this.dep},{"name":this.name,"dep":this.dep});
}
var reduce2=function(key,values){
return values.length;
}
db.emp.mapReduce(map2,reduce2,{out:"result2"}).find()
# 调式 mapReduce 执行
var emit=function(key,value){
print(key+":"+value);
}
四、mongoDB的索引特性
- 索引的基础概念
- 单键索引
- 多键索引
- 复合索引
- 过期索引
- 全文索引
1. 索引的基础概念
每个集合(表)默认情况下都会有一个索引(_id)。
# 查看执行计划:
db.emp.find({"salary":{$gt:500}}).explain()
#创建索引
db.emp.createIndex({salary:1})
#查看索引
db.emp.getIndexes()
#查看执行计划
db.emp.find({"salary":{$gt:500}}).explain()
单键索引
# 单个例上创建索引:
db.subject.createIndex({"name":1})
# 嵌套文档中的列创建索引
db.subject.createIndex({"grade.redis":1})
# 整个嵌套文档创建索引
db.subject.createIndex({"grade":1})
复合索引
# 建复合索引
db.emp.createIndex( { "job":1,"salary":-1} )
# 查看执行计划:
db.emp.find({"job":"讲师", "salary":{$gt:500}}).explain()
db.emp.find({"job":"讲师"}).explain()
db.emp.find({"salary":{$gt:500}}).explain()
# 复合索引如下
job_1_salary_-1,复合索引遵循最左原则。
db.emp.find({"job":"讲师","salary":{$gt:5000}}).explain() // 走索引
db.emp.find({"salary":{$gt:5000},"job":"讲师"}).explain() // 走索引
db.emp.find({"job":"讲师"}).explain() // 走索引
db.emp.find({"salary":{$gt:5000}}).explain() //不走索引
# 排序 场景
db.emp.find({}).sort({"job":1,"salary":-1}).explain()// 完全匹配 ==>走索引
db.emp.find({}).sort({"job":-1,"salary":1}).explain()//完全不匹配 ==>走索引
db.emp.find({}).sort({"job":1,"salary":1}).explain()// 一半匹配 ==>不走索引
db.emp.find({}).sort({"job":-1,"salary":-1}).explain()// 一半匹配 ==>不走索引
db.emp.find({}).sort({"job":-1}).explain() // ==>走索引
db.emp.find({}).sort({"salary":-1}).explain() // ==>不走索引
过期索引
过期索引存在一个过期的时间,如果时间过期,相应的数据会被自动删除,注意这边删除的是数据,而不是索引。
# 示例
# 插入数据
db.log.insert({"title":"this is logger info","createTime":new Date()})
# 创建过期索引
db.log.createIndex({"createTime":1},{expireAfterSeconds:10})
全文索引
# 创建全文索引
db.project.createIndex( {"name":"text","description":"text"})
# 使用全文索引进行查询,查找的是包含"java"或者"dubbo"
db.project.find({$text:{$search:"java dubbo"}})
# 用于屏蔽关键字,查找的是包含"java"但屏蔽"dubbo"
db.project.find({$text:{$search:"java -dubbo"}})
# 短语查询,\" 包含即可,查找的是"Apache Dubbo"
db.project.find({$text:{$search:"\"Apache Dubbo\""}})
# 中文查询,查找的是包含"阿里"或者"开源"
db.project.find({$text:{$search:"阿里 开源"}})
五、MongoDB 集群
MongoDB 复制集群
image.pngPRIMARY 节点: 可以查询和新增数据
SECONDARY 节点:只能查询 不能新增 基于priority 权重可以被选为主节点
RBITER 节点: 不能查询数据 和新增数据 ,不能变成主节点
注意1:MongoDB主从间的数据同步是通过日志文件oplog进行同步的,这与mysql的同步机制类似。
注意2:MongoDB选举流程比较简单,通过比较节点中设置的权重,权重大的节点会成为新的主节点。
基础示例
主节点配置
dbpath=/data/mongo/master
port=27017
fork=true
logpath=master.log
replSet=tulingCluster
从节点配置
- 其中replSet相同代表着是同一个集群下的节点。
#子节点配置1
dbpath=/data/mongo/slave
port=27018
fork=true
logpath=slave.log
replSet=tulingCluster
#子节点配置2
dbpath=/data/mongo/slave2
port=27019
fork=true
logpath=slave2.log
replSet=tulingCluster
分别启动三个节点
进入其中一个节点
集群复制配置管理
# 查看复制集群的帮助方法
rs.help()
# 添加配置
# 声明配置变量
var cfg ={"_id":"tuling",
"members":[
{"_id":1,"host":"127.0.0.1:27017"},
{"_id":2,"host":"127.0.0.1:27018"}
]
}
# 初始化配置
rs.initiate(cfg)
# 查看集群状态
rs.status()
# 变更节点示例
# 插入新的复制节点
rs.add("127.0.0.1:27019")
# 删除slave 节点
rs.remove("127.0.0.1:27019")
注:默认节点下从节点不能读取数据。调用 rs.slaveOk() 解决。
MongoDB 分片集群
随着数据的增长,单机实例的瓶颈是很明显的。可以通过复制的机制应对压力,但mongodb中单个集群的 节点数量限制到了12个以内,所以需要通过分片进一步横向扩展。此外分片也可节约磁盘的存储。
image.png分片示例
配置 并启动config 节点集群
# 节点1 config1-37017.conf
dbpath=/data/mongo/config1
port=37017
fork=true
logpath=logs/config1.log
replSet=configCluster
configsvr=true
# 节点2 config2-37018.conf
dbpath=/data/mongo/config2
port=37018
fork=true
logpath=logs/config2.log
replSet=configCluster
configsvr=true
# 进入shell 并添加 config 集群配置:
var cfg ={"_id":"configCluster",
"protocolVersion" : 1,
"members":[
{"_id":0,"host":"127.0.0.1:37017"},
{"_id":1,"host":"127.0.0.1:37018"}
]
}
# 重新装载配置,并重新生成集群。
rs.initiate(cfg)
配置 shard 节点集群
# 节点1 shard1-47017.conf
dbpath=/data/mongo/shard1
port=47017
fork=true
logpath=logs/shard1.log
shardsvr=true
# 节点2 shard2-47018.conf
dbpath=/data/mongo/shard2
port=47018
fork=true
logpath=logs/shard2.log
shardsvr=true
配置 路由节点 mongos
# 节点 route-27017.conf
port=27017
bind_ip=0.0.0.0
fork=true
logpath=logs/route.log
configdb=conf/127.0.0.1:37017,127.0.0.1:37018
# 添加分片节点
sh.status()
sh.addShard("127.0.0.1:47017");
sh.addShard("127.0.0.1:47018");
# 为数据库开启分片功能
sh.enableSharding("tuling")
# 为指定集合开启分片功能
sh.shardCollection("tuling.emp",{"_id":1})
# 修改分片大小
use config
db.settings.find()
db.settings.save({_id:"chunksize",value:1})
# 尝试插入1万条数据:
for(var i=1;i<=100000;i++){
db.emp.insert({"_id":i,"name":"copy"+i});
}
db.emp.createIndex({_id: 'hashed'})
注意:添加分片的时候,若分片为一个集群,那么路由对分片的添加要注明集群名称,例如:
sh.addShard("tulingCluster/127.0.0.1:47017,127.0.0.1:47018")