目录
MongoDB 介绍
MongoDB 是一个基于 分布式 文件存储 的数据库。由 C++ 语言编写。旨在为 WEB 应用提供可扩展的高性能数据存储解决方案。在高负载的情况下,添加更多的节点,可以保证服务器性能。
文档存储一般用类似json的格式存储,存储的内容是文档型的。这样也就有机会对某些字段建立索引,实现关系数据库的某些功能。
MongoDB 是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富,最像关系数据库的。是一个 非关系型数据库。
关系型数据库的事务遵循ACID特性
事务 具有四大特性:
- 原子性 Atomicity:一系列操作,要么全部做完,要么全都不做。若一个操作失败,全部事务回滚。
- 隔离性 Isolation:并发执行的事务,相互之间不会影响,
- 持久性 Durability:事务一旦提交后,修改的内容会永久保存。
- 一致性 Consistency:如果事务启动时数据是一致的,那么当这个事务执行成功后,数据库也应该是一致的。事务的最终目的,原子性、隔离性、持久性 都是为了约束事务,实现数据的一致性。
MongoDB是NoSQL,不具有ACID特性。
NoSQL
Not Only SQL,即非关系型数据库,适用于超大规模数据的存储。
特性:
- 没有声明性查询语言
- 非结构化和不可预知的数据
- 最终一致性,而非ACID属性
- 键 - 值对存储,列存储,文档存储,图形数据库
- 高性能,高可用性和可伸缩性
- CAP定理
CAP定理
即布鲁尔定理,它指出对于一个分布式计算系统来说,不可能同时满足以下三点:
- 一致性(Consistency) (所有节点在同一时间具有相同的数据)
- 可用性(Availability) (保证每个请求不管成功或者失败都有响应)
- 分隔容忍(Partition tolerance) (系统中任意信息的丢失或失败不会影响系统的继续运作)
CAP理论的核心是:一个分布式系统不可能同时很好的满足一致性,可用性和分区容错性这三个需求,最多只能同时较好的满足两个。
因此,根据 CAP 原理将 NoSQL 数据库分成了满足 CA 原则、满足 CP 原则和满足 AP 原则三 大类:
CA - 单点集群,满足一致性,可用性的系统,通常在可扩展性上不太强大。
CP - 满足一致性,分区容忍性的系统,通常性能不是特别高。
AP - 满足可用性,分区容忍性的系统,通常可能对一致性要求低一些。
分布式系统
由多台计算机和通信的软件组件通过计算机网络连接(本地网络或广域网)组成,具有高度的内聚性和透明性。
优点
- 可靠性(容错) :分布式计算系统中的一个重要的优点是可靠性。一台服务器的系统崩溃并不影响到其余的服务器。
- 可扩展性:在分布式计算系统可以根据需要增加更多的机器。
- 资源共享:共享数据是必不可少的应用,如银行,预订系统。
- 灵活性:由于该系统是非常灵活的,它很容易安装,实施和调试新的服务。
- 更快的速度:分布式计算系统可以有多台计算机的计算能力,使得它比其他系统有更快的处理速度。
- 开放系统:由于它是开放的系统,本地或者远程都可以访问到该服务。
- 更高的性能:相较于集中式计算机网络集群可以提供更高的性能(及更好的性价比)。
缺点
- 故障排除:故障排除和诊断问题。
- 软件:更少的软件支持是分布式计算系统的主要缺点。
- 网络:网络基础设施的问题,包括:传输问题,高负载,信息丢失等。
- 安全性:开放系统的特性让分布式计算系统存在着数据的安全性和共享的风险等问题。
MongoDB
将数据存储为一个文档,数据结构由键值(key=>value)对组成。MongoDB 文档类似于 JSON 对象。字段值可以包含其他文档,数组及文档数组。
特点:
- 高性能:提供高性能的数据持久性。
- 高可用性:MongoDB的复制工具成为副本集,提供自动故障转移和数据冗余。
- 高扩展性:提供了水平可扩展
应用需求与应用场景
需求:高并发读写需求;海量数据的高效率存储和访问需求;高扩展和高可用性的需求;
应用场景
- 社交场景,存储用户信息及用户发表的朋友圈信息,通过地理位置索引附近的人、地点等功能。
- 游戏场景,存储游戏用户信息,用户装备、几分等直接以内嵌文档的形式存储,方便查询、高效率存储和访问。
- 物流场景,存储订单信息,订单状态会不断更新,以MongoDB内嵌数组的形式存储,一次查询就能将订单的所有变更读取出来。
- 物联网场景,存储所有接入的智能设备信息,以及设备回报的日志信息,并对这些信息进行多维度分析。
- 视频直播,存储用户信息、点赞互动信息等。
数据操作方面的特点:数据量大;读写很频繁;价值较低的数据,对事务性要求不高。
MongoDB 基础概念
默认数据库介绍:
- admin:从权限的角度来看,这是"root"数据库。要是将一个用户添加到这个数据库,这个用户自动继承所有数据库的权限。一些特定的服务器端命令也只能从这个数据库运行,比如列出所有的数据库或者关闭服务器。
- local:这个数据永远不会被复制,可以用来存储限于本地单台服务器的任意集合。
- config:当Mongo用于分片设置时,config数据库在内部使用,用于保存分片的相关信息。
基础术语
SQL术语/概念 | MongoDB术语/概念 | 解释/说明 |
database | database | 数据库 |
table | collection | 数据库表/集合 |
row | document | 数据记录行/文档 |
column | field | 数据字段/域 |
index | index | 索引 |
table joins | 表连接,MongoDB不支持 | |
嵌入文档 | MongoDB通过嵌入文档代替多表连接 | |
primary key | primary key | 主键,MongoDB自动将_id字段设置为主键 |
关系型数据库 与 MongoDB 对应的术语:数据库——数据库;表格——集合;行——文档;列——字段;表联合——嵌入文档;主键——主键(MongoDB中主键为_id)。
注意:文档中的键值对是有序的;文档中的值不仅可以是在双引号里面的字符串,还可以是其他几种数据类型(甚至可以是整个嵌入的文档);MongoDB区分类型和大小写;MongoDB的文档不能有重复的键;文档的键是字符串。除了少数例外情况,键可以使用任意UTF-8字符。
- 集合:MongoDB的文档组。类似关系型数据库的 数据表。但是没有固定结构。可以对集合插入不同格式和类型的数据。
- 文档:一组键值(key-value)对(即 BSON)。MongoDB 的文档不需要设置相同的字段,并且相同的字段不需要相同的数据类型,这与关系型数据库有很大的区别,也是 MongoDB 非常突出的特点。
{"site":"www.baidu.com", "name":"百度"}
函数命令
# 默认ID对象
ObjectId()
ObjectId().getTimestamp()
ObjectId().str
# 日期-格林尼治时间
new Date() 或者直接 Date()
typeof new Date() # 查看日期对象的类型 Object类型
new Date().toString() # 时间 由Object类型转字符串
常用查询命令
连接
# 在本机使用shell连接
mongo
# 连接 远程服务器的 MongoDB.默认Port=27017
mongo --host=139.196.184.230 --port=32937
基本数据库操作
# 查看所有数据库的列表
show dbs
# 创建数据库.使用新的数据库名称,向文档中插入一条数据,就会自动创建新的数据库及文档,且数据库与集合名字一样
use db;
db.db-name.insert({})
# 显示 当前 数据库对象或集合
db
# 切换 当前数据库对象或集合
use dbName
#删除 当前 数据库
db.dropDatabase()
# 删除指定数据库
db.dbName.drop()
# 显示当前 数据库的 所有集合列表
show collections / show tables
# 在当前数据库下 创建集合,可以设置参数
db.createCollection("collection-name")
db.createCollection("mycol", { capped : true, autoIndexId : true, size : 6142800, max : 10000 } )
# 删除 指定 集合。成功返回true,失败返回false。
db.collection-name.drop()
# 插入文档 (文档数据结构 与 JSON一致)
# 若插入的数据主键_id已经存在,则会抛 org.springframework.dao.DuplicateKeyException 异常,提示主键重复,不保存当前数据。
db.collection-name.insert({document-json})
# 插入多个文档
db.collection-name.insertMany([document-json])
# 异常捕捉的 批量插入
try{
db.collection-name.insertMany([document-json])
}catch (e){
print(e);
}
# 查看插入的文档
db.collection-name.find()
# 更新插入的文档
db.collection.update(
<query>,
<update>,
{
upsert: <boolean>,
multi: <boolean>,
writeConcern: <document>
}
)
# 删除 所有 符合条件的文档(status=A)
db.inventory.deleteMany({ status : "A" })
# 删除一个 符合条件的文档
db.inventory.deleteOne({ status : "D" })
db.COLLECTION_NAME.remove({<query>})
# 查询文档
db.collection.find(query, projection)
# 带格式的 查询所以文档
db.col.find().pretty()
# 计数
db.collection-name.count()
# 分页查询:skip(m)从第m个开始,limit(n)向后查询n个
db.sys_user.find().limit(2).skip(2)
# 排序:1 正序,小到大; -1 逆序,大到小。
db.sys_user.find().sort({"age":1})
# 创建索引。keys为字段,options=1/-1,升序或降序
db.collection.createIndex(keys, options)
数据库查询
# 正则的复杂条件查询
db.集合.find({字段:/正则表达式/})
# 比较查询.
# $gt:大于;$lt:小于;$gte:大于等于;$lte:小于等于;$ne:不等于
db.集合名称.find({ "field" : { $gt: value }})
# 包含查询
b.comment.find({userid:{$in:["1003","1004"]}})
# 不包含查询
db.comment.find({userid:{$nin:["1003","1004"]}})
# and连接 $and:[ { },{ },{ } ]
db.comment.find({$and:[{likenum:{$gte:NumberInt(700)}},{likenum:{$lt:NumberInt(2000)}}]})
# or连接 $or:[ { },{ },{ } ]
db.comment.find({$or:[ {userid:"1003"} ,{likenum:{$lt:1000} }]})
常用命令小结
选择切换数据库:use articledb
插入数据:db.comment.insert({bson数据})
查询所有数据:db.comment.find();
条件查询数据:db.comment.find({条件})
查询符合条件的第一条记录:db.comment.findOne({条件})
查询符合条件的前几条记录:db.comment.find({条件}).limit(条数)
查询符合条件的跳过的记录:db.comment.find({条件}).skip(条数)
修改数据:db.comment.update({条件},{修改后的数据}) 或db.comment.update({条件},{$set:{要修改部分的字段:数据})
修改数据并自增某字段值:db.comment.update({条件},{$inc:{自增的字段:步进值}})
删除数据:db.comment.remove({条件})
统计查询:db.comment.count({条件})
模糊查询:db.comment.find({字段名:/正则表达式/})
条件比较运算:db.comment.find({字段名:{$gt:值}})
包含查询:db.comment.find({字段名:{$in:[值1,值2]}})或db.comment.find({字段名:{$nin:[值1,值2]}})
条件连接查询:db.comment.find({$and:[{条件1},{条件2}]})或db.comment.find({$or:[{条件1},{条件2}]})
MongoDB 索引
索引支持在MongoDB中高效地执行查询。如果没有索引,MongoDB必须执行全集合扫描,即扫描集合中的每个文档,以选择与查询语句匹配的文档。这种扫描全集合的查询效率是非常低的,特别在处理大量的数据时,查询可以要花费几十秒甚至几分钟,这对网站的性能是非常致命的。
索引是特殊的数据结构,它以易于遍历的形式存储集合数据集的一小部分。索引存储特定字段或一组字段的值,按字段值排序。索引项的排序支持有效的相等匹配和基于范围的查询操作。此外,MongoDB还可以使用索引中的排序返回排序结果。
MongoDB索引使用 B树数据结构(确切的说是B-Tree,MySQL是B+Tree)。
索引类型
- 单字段索引:在文档的单个字段上创建用户定义的升序/降序索引。
- 复合索引:复合索引中列出的字段顺序具有重要意义。例如,如果复合索引由 { userid: 1,score: -1 } 组成,则索引首先按userid正序排序,然后在每个userid的值内,再在按score倒序排序。
- 其他索引:
- 地理空间索:支持对地理空间坐标数据的有效查询。返回结果时使用平面几何的二维索引和返回结果时使用球面几何的二维球面索引。
- 引文本索引:支持在集合中搜索字符串内容。这些文本索引不存储特定于语言的停止词(例如“the”、“a”、“or”),而将集合中的词作为词干,只存储根词。
- 哈希索引:支持基于散列的分片,MongoDB提供了散列索引类型,它对字段值的散列进行索引。这些索引在其范围内的值分布更加随机,但只支持相等匹配,不支持基于范围的查询。
索引的管理操作
# 查看索引
db.collection.getIndexes()
# 创建索引 options=1/-1,表示升序/降序
db.collection.createIndex(keys, options)
# 移除索引
db.collection.dropIndex(index)
# 移除所有索引
db.collection.dropIndexes()
索引的使用
执行计划:分析查询的性能,例如耗费时间、是否基于索引查询等。
db.collection.find(query,options).explain(options)
优化示例:优化前
> db.comment.find({"userid":"1001"}).explain()
{
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "articledb.comment",
"indexFilterSet" : false,
"parsedQuery" : {
"userid" : {
"$eq" : "1001"
}
},
"queryHash" : "37A12FC3",
"planCacheKey" : "37A12FC3",
"winningPlan" : {
"stage" : "COLLSCAN",
"filter" : {
"userid" : {
"$eq" : "1001"
}
},
"direction" : "forward"
},
"rejectedPlans" : [ ]
},
"serverInfo" : {
"host" : "izuf65paliafskyrp301s2z",
"port" : 32937,
"version" : "4.4.0",
"gitVersion" : "563487e100c4215e2dce98d0af2a6a5a2d67c5cf"
},
"ok" : 1
}
关键点看: "stage" : "COLLSCAN", 表示全集合扫描
创建索引:db.comment.createIndex({userid:1})
优化后
db.comment.find({"userid":"1001"}).explain()
{
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "articledb.comment",
"indexFilterSet" : false,
"parsedQuery" : {
"userid" : {
"$eq" : "1001"
}
},
"queryHash" : "37A12FC3",
"planCacheKey" : "7FDF74EC",
"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" : [
"[\"1001\", \"1001\"]"
]
}
}
},
"rejectedPlans" : [ ]
},
"serverInfo" : {
"host" : "izuf65paliafskyrp301s2z",
"port" : 32937,
"version" : "4.4.0",
"gitVersion" : "563487e100c4215e2dce98d0af2a6a5a2d67c5cf"
},
"ok" : 1
}
"stage" : "IXSCAN", 基于索引的扫描
涵盖的查询
当查询条件和查询的投影仅包含索引字段时,MongoDB直接从索引返回结果,而不扫描任何文档或将文档带入内存。 这些覆盖的查询可以非常有效。(查询的字段正好是 创建了索引的字段,不需要在集合中查询,直接从索引中获取即可,效率很高。)
SpringBoot整合MongoDB应用
使用的是SpringData家族成员之一:SpringDataMongoDB,操作上类似于 Spring Data JPA。
pom.xml
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.1.RELEASE</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
</dependency>
</dependencies>
application.yml
spring:
data:
mongodb:
host: 139.196.184.230
database: articledb
port: 32937
# 或者
spring:
data:
mongodb:
uri: mongodb://139.196.184.230:32937/articledb
Comment实体类
- Document(collection="comment"):可以省略,如果省略,则默认使用类名小写映射集合。
- 添加复合索引: @CompoundIndex( def = "{'userid': 1, 'nickname': -1}")。
- 添加单字段索引: 变量添加注解 @Indexed。
@Data
@Document(collection = "comment")
@CompoundIndex(def = "{'userId:1,'nickName':-1}")
public class Comment implements Serializable {
@Id
private String id;
@Field("content")
private String content;
@Indexed
private String userId;
private String nickName;
private LocalDateTime createDatetime;
private Integer likeNum;
private Integer replyNum;
private String state;
private String parentId;
private String articleId;
}
CommentRepository 数据访问接口
public interface CommentRepository extends MongoRepository<Comment, String> {
Page<Comment> findCommentByNickNameIn(List<String> nickName, Pageable pageable);
List<Comment> findCommentByArticleIdAndState(String article, String state);
}
CommentService 文章平品论类 业务层
@Service
public class CommentService {
@Resource
private CommentRepository repository;
/**
* 新增
*
* @param comment 内容
* @return 结果
*/
public Comment saveComment(Comment comment) {
return repository.save(comment);
}
/**
* 更新
*
* @param comment 内容
* @return 结果
*/
public Comment updateComment(Comment comment) {
return repository.save(comment);
}
/**
* 根据ID删除品论
*
* @param id id
*/
public void deleteCommentById(String id) {
repository.deleteById(id);
}
/**
* 查询所有评论
*
* @return 结果
*/
public List<Comment> findCommentList() {
return repository.findAll();
}
public Page<Comment> findCommentPage(List<String> nickNameList) {
PageRequest pageRequest = PageRequest.of(0, 2);
return repository.findCommentByNickNameIn(nickNameList, pageRequest);
}
/**
* 根据id查询评论
*
* @param id 评论ID
* @return 结果
*/
public Comment findCommentById(String id) {
return repository.findById(id).get();
}
/**
* 计数
*
* @return 结果
*/
public Long countComment() {
return repository.count();
}
/**
* 根据 指定字段查询
*
* @param articleId 文章ID
* @return 结果
*/
public List<Comment> findCommentByArticleId(String articleId) {
return repository.findCommentByArticleIdAndState(articleId, "1");
}
}
单元测试
@SpringBootTest
@RunWith(SpringRunner.class)
public class CommentServiceTest {
@Resource
private CommentService service;
@Test
public void saveComment() {
Comment comment = new Comment();
comment.setId("100");
comment.setArticleId("10000");
comment.setContent("为赋新词强说愁");
comment.setCreateDatetime(LocalDateTime.now());
comment.setUserId("1005");
comment.setNickName("辛弃疾");
comment.setState("1");
comment.setLikeNum(0);
comment.setReplyNum(0);
Comment result = service.saveComment(comment);
System.out.println(result.toString());
}
@Test
public void updateComment() {
Comment comment = service.findCommentById("100");
comment.setLikeNum(100);
comment.setReplyNum(5);
Comment result = service.updateComment(comment);
System.out.println(result.toString());
}
@Test
public void deleteCommentById() {
service.deleteCommentById("100");
}
@Test
public void findCommentList() {
List<Comment> commentList = service.findCommentList();
commentList.forEach(System.out::println);
}
@Test
public void countComment() {
Long aLong = service.countComment();
System.out.println(aLong);
}
@Test
public void findCommentById() {
Comment comment = service.findCommentById("100");
System.out.println(comment.toString());
}
@Test
public void findCommentByArticleId() {
List<Comment> commentList = service.findCommentByArticleId("10000");
commentList.forEach(System.out::println);
}
@Test
public void findCommentPage() {
List<String> nickName = Arrays.asList("杜甫", "辛弃疾", "李白");
Page<Comment> page = service.findCommentPage(nickName);
System.out.println("总页数:" + page.getTotalPages());
System.out.println("记录:" + page.getContent());
}
}