mongodb面试题
简介
MongoDB是开源的,面向文档类型的nosql数据库,使用类JSON的BSON格式存储数据,可以给文档中字段建立索引,可以持久化数据,分布式存储,适合处理大量数据。也可以作为关系型数据库的补充使用。
特点:
用C++语言编写的
可以进行大尺寸数据存储
支持分布式文件存储
支持丰富的数据查询方式
文件存储格式为BSON(一种JSON的扩展)
可以给字段创建索引,提高查询效率
Mongo只支持单文档事务,不支持复杂的事务
不支持跨集合查询
说明:
文档类型数据库:
数据是按照文档的形式进行存储的,没有固定的表结构
分布式:
mongo本身支持分布式部署方式,不需要第三方软件
尺寸限制:
一个文档(bson)最大限制为16M,超过16M会报错,这个限制是为了确保单个文档不会使用过多的内存,或者在迁移期间不会占用过多的带宽(如果要从一个服务器迁移数据到另一个服务器,文档过大要求的带宽就大)。
对象的数量不受限制
索引:
使用b-tree的数据结构来构建索引(具体是b-tree还是b+tree存在争议,但b+tree也是b-tree的一种)
事务:
WT存储引擎4.0开始支持
mongodb的应用场景
适合的场景:
内容管理系统:如博客、新闻、文档库等
日志管理:大量日志的存储与分析,数据量大可以做分布式存储
不适合的场景:
需要高度事务的场景
需要关联查询的场景
Mongodb术语说明
数据库(database):一个mongodb中可以建立多个数据库。每一个都有自己的集合和权限,不同的数据库也放置在不同的文件中。
集合(collection)就是一组文档,如果说MongoDB中的文档类似于关系型数据库中的行,那么集合就如同表
文档(document)是MongoDB中数据的基本单元,每一个文档都有一个特殊的键"_id",它在文档所处的集合中是唯一的,相当于关系数据库中的表的主键
为什么适合处理大量数据
面向文档的:使用Bson格式存储,相比关系型数据库更加灵活,方便存储各种类型数据,包括嵌套的对象,也方便添加字段不用修改表结构
支持分布式存储:将数据水平拆分存储到多个服务器上,大量数据存储增加服务器即可
支持索引及内存加载:采用了内存映射文件的方式,将部分数据加载到内存中,并支持字段索引、复合索引、地理空间索引等,查询快
BSON对比json
BSON是MongoDB的数据存储格式,是一个轻量级的二进制数据格式。相比json支持更多的数据类型(日期、时间戳、二进制、javascript、正则表达式等)
- 更快的遍历速度
对json来说如果要跳过一个文档需要扫描这个文档,找到文档结束符”}”,而bson将文档的长度存在文档的头部,只需要读取到这个长度就可以直接跳到指定位置(如分页查询时会跳过前面一些文档)
- 操作更简易
Json的存储都是按照字符串进行存储的,bson的存储是按照各自的数据类型进行存储的,这样会更加有效率的操作各种数据
- 增加了额外的数据类型
Json的数据类型有
1、数字(整型、浮点型);
2、字符串;
3、布尔类型。
4、对象;
5、null;
6、数组。
Bson的数据类型有:
除了json的数据类型还包括这些
- 日期类型
- 时间戳类型
- 二进制类型
- 正则表达式类型
- 代码类型,用于在文档中存储 JavaScript 代码。 {"x":function() {}}
- ObjectId。用于创建文档的 ID。{"id": ObjectId()},MongoDB中存储的文档必须有一个"_id"键等
mongo快的原因
通过将常用数据存储在内存中,MongoDB能够减少磁盘I/O操作,从而显著提高读取速度。
MongoDB采用文档数据模型,允许将相关的数据放在一起,减少了查询时的连接数量和查询时间。
MongoDB支持多种类型的索引,如单字段索引、复合索引、全文索引等。索引可以显著提高查询速度,因为它能够将查询过程从全表扫描变为扫描索引。
MongoDB是分布式集群数据库,可以平行扩展。这意味着可以通过增加更多的节点来提高系统的处理能力,从而支持更高的并发量和更大的数据量。这种分布式架构使得MongoDB在处理大规模数据时能够保持高性能。
怎么区分常用的数据:
使用了一种类似于 LRU的算法来管理缓存中的数据。最近较少访问的数据会被逐出缓存。
Mongodb部署方案
单机
主从复制(主从集群)
副本集
分片集群部署
主从集群和副本集最大的区别副本集提供了故障转移的功能
一般情况下,一个最小化的副本集由一个主节点、一个副本节点和一个仲裁节点组成,但是在实际使用过程中,大多数采用的是由一个主节点和两个副本节点组成的。
副本集说明
副本集中有三种角色:主节点、从节点、仲裁节点。
主节点只能有一个,从节点可以有多个,通过维护冗余的数据副本,能够实现数据的备份,读写分离和自动故障转移。
默认设置下,主节点提供所有增删改查服务,从节点不提供任何服务。但是可以通过设置使备节点提供查询服务
仲裁节点不提供存储数据,不能成为主节点,只提供自己的一个选票,仲裁节点不是必须的,副本集可以没有仲裁节点,如果副本集成员节点数量是奇数,就不再需要仲裁者。
单线程还是多线程
mongo是多个线程处理用户请求
副本集的主节点选举
选举期间不能提供写服务,可以提供读服务
副本集数量最好为奇数
官方推荐副本集的成员数量为奇数,最多12个副本集节点,最多7个节点参与选举。最多12个副本集节点是因为没必要一份数据复制那么多份,备份太多反而增加了网络负载和拖慢了集群性能;而最多7个节点参与选举是因为内部选举机制节点数量太多就会导致1分钟内还选不出主节点
选举要求参与的节点数量必须大于一半,当复制集内存活成员数量不足大多数时,整个复制集将无法选举出主节点,复制集将无法提供写服务,处于只读状态。
进行选举的时机:
1.副本集初始化时;
2.副本集被reconfig;
3.Secondary节点检测到Primary宕机时;
4.当有Primary节点主动stepDown(主动降级为Secondary)时;
主节点降级:
当Primary发现有优先级更高Secondary,并且该Secondary的数据落后在10s内,则Primary会主动降级,让优先级更高的Secondary有成为Primary的机会。
节点的优先级:
优先级即Priority值,每个member都有权重值,默认为都为1,members倾向于选举权重最高者。
Optime: 当前member已经从primary的oplog中应用的最后一个operation的时间戳(此时间戳由primary生成,在oplog中每个操作记录都有);一个member能成为primary的首要条件就是在所有有效的members中它持有最新的optime。
什么节点能成为主节点:
能够与大多数节点建立连接(大多数参考的得到的票数而不是所有节点)
在所有有效的members中它持有最新的optime
前两个条件相同的,Priority优先级高的成为Primary
optime与Priority都相等时,谁发起选举,谁当选Primary
副本集从节点的角色
Secondary节点:可以数据备份和读服务,参与投票,可以升级为主节点
优先级为0的节点:
不会升级为主节点;但是可以投票。可以数据备份和命令执行。可以提供读操作
设置参数为Priority=0
隐藏节点:
隐藏节点不会升级为主节点,不提供读操作,可以投票,可以数据备份
在隐藏节点上,可做一些数据备份、离线计算的任务,不会影响复制集的服务。
延迟节点:
延迟节点代表此节点的数据与Primary的数据有一定的延迟,通过设定一个延迟的属性来确定。
此节点必须是一个Priority=0且为Hidden的节点。
此节点虽然又迟延又Hidden,但是还是可以投票。延迟单位设置为秒。
非投票节点:
MongoDB一个副本集最多有7个投票节点,如果还有其它的节点,需要设置为非投票节点。非投票节点拥有数据副本,但是不参与投票。另外,非投票节点,其priority必须设置为0。
votes属性设置为0表示不参与投票
节点心跳
整个集群需要保持一定的通信才能知道哪些节点活着哪些节点挂掉。MongoDB节点会向副本集中的其他节点每2秒就会发送一次pings包,如果其他节点在10秒钟之内没有返回就标示为不能访问。
每个节点内部都会维护一个状态映射表,表明当前每个节点是什么角色、日志时间戳等关键信息。如果是主节点,除了维护映射表外还需要检查自己能否和集群中内大部分节点通讯,如果不能则把自己降级为只读从节点。
分片集群部署说明
分片在多台服务器上分布数据的方法
一个分片集群包括以下几个组件:分片,查询路由,配置服务器
分片:用来存储数据,为了提供系统可用性和数据一致性,一个生产环境的分片集群,通常每个分片是一个副本集。
查询路由:指客户端应用访问每个分片的路径。
配置服务器:存储集群的元数据,这些数据包含了集群数据集到各分片的映射关系。查询路由就是通过这些元数据到特定的分片上执行指定的数据操作。
Mongo存储引擎
MMAPv1:3.2以前默认的存储引擎,从4.0版本开始被弃用。
WiredTiger(简称WT引擎):3.2及以上默认的存储引擎
MMAPv1:
基于内存映射文件对mongo数据进行存储,通过内存映射文件来进行交互数据,读取数据时从内存读取,修改数据时修改内存中的数据,之后定时刷新到磁盘,写数据也会写入Jounal日志文件中用于数据恢复
WiredTiger:
WiredTiger的写操作会先写入内存Cache中,当Cache大小达到128KB时或每隔50ms便将其持久化到预写Jounal操作日志文件中。每60s或待写入数据达到2GB时会做一次检查点Checkpoint,将数据持久化到磁盘文件中。
mongo索引数据结构
是b-tree的一种变种,具体是b-tree还是b+tree存在争议
- 非叶子节点只包含索引键值和指向子节点的指针,它们不直接存储文档数据。
- 叶子节点包含索引键值以及指向实际文档数据的指针或位置信息。叶子节点有指向下个叶子节点的指针(单向),这有助于范围查询。
- 实际的文档数据存储在数据文件中,与索引文件分离。索引仅用于加速查询过程,通过索引找到对应的文档位置后,再从数据文件中读取实际的文档数据。
相比mysql的主键索引的叶子节点存储的不是指向数据的指针,存储的实际的数据。
mongo索引叶子节点的指针不是指向的物理地址,是包含足够的信息来定位到包含文档的特定位置或记录。这种定位信息可能包括文件标识符、记录位置或其他元数据。所以不会存在两个索引值的数据都在同一个集合中在同一个数据文件中指针内容一样的问题。
mongo写数据原理
写请求过来后,先把索引信息及数据的更改信息写入内存,这个时候用户就可以查询到了,之后写内存中的预操作文件,写入的预操作日志文件大小达到一定值或每隔多长时间进行持久化journal日志文件,磁盘预操作日志文件达到一定的大小或相隔一定时间将预操作日志文件的操作持久化到索引文件和文档数据文件。
mongo读数据原理
mongo的索引数据和文档数据很多都在内存中,很多查询都直接在内存中可以查询到。查询不到的会查询磁盘。
在磁盘上找数据时,它会在索引树上找到对应的叶子节点获取到文档数据。
查询的结果是结合磁盘数据、内存数据一块来实现的。先查内存,内存没有查磁盘。如果一部分数据在内存,一部分数据在磁盘会合并结果并已最新的数据为准。
缓存值与磁盘数据不一致问题
如果有写操作会先更新内存中的索引和值,并更新缓存中的值,这时数据还没有持久化到磁盘上,就会导致缓存数据比磁盘数据更新。
不过查询的时候先查询的缓存和内存,所以不会查询到过期的数据。
mongo持久化机制
预写文件持久化:mongo首先写内存中的预写日志,当达到一定大小或相隔一定时间,会把预写文件持久化到磁盘上。
数据持久化:当预写日志达到一定大小或相隔一定时间,就会做一次检查点,将预写文件中的数据操作持久化到磁盘上。数据持久化完成后,与之相关的预写日志文件就不再需要了,因此MongoDB会将其删除,以释放磁盘空间。数据持久化时候会阻塞mongo的进程,知道持久化完成。
预写文件的持久化频率比数据持久化的频率要高。
mongo检查点
检查点记录了数据库在某个时间点上的状态,用于内存数据恢复。
创建检查点时会根据旧检查点之后产生的预写日志把数据持久化到磁盘上,持久化完成检查点才算创建成功。
新的检查点创建成功会把老的检查点给删除掉。
mongo数据恢复
创建新的检查点成功之前老的检查点还是有效的,如果新的创建失败或没有创建之前需要恢复数据,就从老的检查点开始恢复数据,将磁盘上的数据配合持久化的预写日志文件来恢复内存中的数据。
mongo的文件存储
索引和数据是不同的文件存储
每个集合的索引文件单独存储,每个集合的数据文件也是单独存储
一个集合的数据文件数量并不是固定的,它取决于多个因素,包括集合中文档的数量、大小以及MongoDB的配置和存储引擎等。
journal日志说明
Journal:汉语意思是日志
作用:当mongo异常宕机时,用于数据恢复
默认是开启的
MMAPv1存储引擎:
写操作首先在内存进行修改并将对应操作写入磁盘journal文件。如果修改在数据同步至磁盘之前MongoDB崩溃,重启之后MongoDB可以利用journal日志重新应用对应的写操作到数据文件从而保证数据一致性
WiredTiger
journal日志保存两个检查点之间的所有修改,如果MongoDB在下一个检查点之前崩溃,引擎将会使用journal重放最后一个检查点以来的所有数据修改操作,WiredTiger引擎是在数据更新时,先将数据更新写入到日志文件,然后在创建Checkpoint操作开始时,将日志文件中记录的操作,刷新到数据文件,就是说,通过预写日志和Checkpoint,将数据更新持久化到数据文件中
为了提升性能,write将会首先写入journal日志的内存buffer中,当buffer数据达到100M或者每隔100毫秒,buffer中的数据将会flush到磁盘中的journal文件中;如果mongodb异常退出,将可能导致最多100M数据或者最近100ms内的数据丢失。
指定id插入和不指定的性能对比
MongoDB中,指定id插入比不指定慢很多
这是因为,MongoDB里每一条数据的_id值都是唯一的。当在不指定_id插入数据的时候,其_id是系统自动计算生成的。MongoDB通过计算机特征值、时间、进程ID与随机数来确保生成的_id是唯一的。而在指定_id插入时,MongoDB每插一条数据,都需要检查此_id可不可用,当数据库中数据条数太多的时候,这一步的查询开销会拖慢整个数据库的插入速度。
mongo自动增长
MongoDB 没有像 SQL 一样有自动增长的功能, MongoDB 的 _id 是系统自动生成的12字节的字符串类型的唯一标识。
我们可以通过编程的方式来实现
默认排序
如果 mongo 查询没有指定 sort 排序,那么 find() 的结果集的默认顺序为插入的顺序
mongo原子操作
mongodb提供了许多原子操作,比如文档的保存,修改,删除等,都是原子操作。
所谓原子操作就是要么这个文档保存到Mongodb,要么没有保存到Mongodb,不会出现查询到的文档没有保存完整的情况。
缓存淘汰机制
当内存不够使用时,采用的是常见的LRU淘汰机制(最近最少用的淘汰)将部分数据从内存中清除,如果查询的时候涉及到这部分数据会从磁盘加载在内存。
什么时候触发淘汰:
- 内存使用接近限制:当MongoDB的内存使用接近或达到配置的限制时,为了保持系统的稳定运行,它会开始释放那些不常用或较旧的数据的缓存,以便为新的数据和操作提供内存空间。
- 优化内存使用:MongoDB会根据其内部的缓存管理策略,自动评估并释放那些长时间未被访问或访问频率较低的数据。这是为了优化内存的使用效率,确保重要的、频繁访问的数据能够保留在内存中,从而获得更快的访问速度。
- 系统资源竞争:当其他系统进程或应用程序开始与MongoDB竞争内存资源时,MongoDB可能会响应操作系统的资源分配请求,释放部分内存以缓解资源紧张的情况。
Mongo索引说明
索引的重要性:
索引本身也很大,不可能全部存储在内存中,因此索引往往以索引文件的形式存储在磁盘上。这样的话,索引查找过程中就要产生磁盘I/O消耗,相对于内存存取,I/O存取的消耗要高几个数量级,所以评价一个数据结构作为索引的优劣最重要的指标就是在查找过程中磁盘I/O操作次数,换句话说,索引的结构组织要尽量减少查找过程中磁盘I/O的存取次数。
索引可以提高数据的查询效率
MongoDB支持多种类型的索引,包括单字段索引、复合索引(多个字段)、多key索引、文本索引、hash索引,地理空间索引等
常用的索引属性:唯一索引、局部索引、TTL索引、稀疏索引
MongoDB默认会为集合创建_id字段的索引。
mongoDB中一个复合索引最多可以包含32个字段。
多键索引是建在数组上的索引,这种索引适用于数组或嵌套文档的字段。当在数组的某个字段上创建索引时,MongoDB会为数组中的每个元素创建索引。
文本索引:用于文本字段的全文搜索
地理空间索引:引用于包含地理坐标的字段,支持地理空间查询。
哈希索引就是将field的值进行hash计算后作为索引,其强大之处在于实现O(1)查找,最主要的功能也就是实现定值查找,对于经常需要排序或查询范围查询的集合不要使用哈希索引。通常用于分片键。
唯一索引:即强制要求collection中的索引字段没有重复值。
局部索引:只对collection的一部分添加索引。创建索引的时候,根据过滤条件判断是否对document添加索引,如age>25
稀疏索引:如在address字段上添加稀疏索引时,只有document有address字段时才会添加索引。而普通索引则是为所有的document添加索引,使用普通索引时如果document没有索引字段的话,设置索引字段的值为null。
TTL索引:用于设置document的过期时间,mongoDB会在document过期后将其删除,TTL索引只能设置在date类型字段(或者包含date类型的数组)上,过期时间为字段值+exprireAfterSeconds;
索引命令
db.collection.createIndex(keys, options)
语法中 Key 值为你要创建的索引字段,1 为指定按升序创建索引,如果你想按降序来创建索引指定为 -1 即可。
通过索引设置记录有效时间
TTL 索引必须是单字段索引,只能在单一日期或日期数组字段上设置文档的过期时间。
已经过了有效期的文档,如果还没有真正被删除,还是可以被查询到的。(可以查询出来后自己手动去检查数据是否过期)
设置方法:
MongoDB 的索引 可以支持文档在一定时间之后自动过期删除,
插入数据时在该字段指定日期时间,经过在创建索引时指定的秒数后,该记录会被MongoDB认为已经过期,然后删除。
db.test_timer.createIndex({"timer":1}, {expireAfterSeconds: 10})
db.test_timer.insert({"timer":new Date(), "a":'abc'}) // 指定当前时间
db.test_timer.insert({"timer": new Date("2017/3/25 13:11:00"), "c": "CC"}) // 指定任意时间
具体如何做:在类的属性上添上@Indexed注解写上过期时间就行
@Document(collection="hotContentVo")
public class HotContentVo implements Serializable{
private static final long serialVersionUID = 1L;
private Long contentId;
/**
* 创建时间,超过这个时间7天,插入的记录过期
*/
@Indexed(expireAfterSeconds=604800)
private Date createTime;
原理:
TTL 索引目前是后台单线程来定期(默认60s一次)去删除已过期的文档
Mongo中的时间存储
mongo中的date是以0时区进行存储的,我们处于东8区,这样的话它存储的时间比我们小8小时,但当我们获取时间的时候根据我们的时区获取就不会有时差,java的api对时间的操作已经指定了时区,所以我们不用担心时区差问题
我们使用mongo客户端工具也可以设置时区,来方便查看

Mongo常用命令
添加
向集合添加文档,使用命令 db.集合名称.insert({}),例如:
db.user1.insert({name:”jack”,age:20})
db.users.save({name: ‘zhangsan’, age: 25, sex: true});
替换或者插入文档,当传入_id且与数据库数据冲突时替换掉该_id的文档数据
若新增遇到主键冲突 ,insert() 会提示错误,而save() 则更改原来的内容为新内容。
删除
删除集合中的文档,使用命令 db.集合名称.remove({删除条件}),不加删除条件为删除集合中的所有文档,例如,db.c1.remove() 为删除c1集合中的所有文档,db.c1.remove({name:”user1”})为删除c1集合中name为user1的文档
remove和drop区别
remove用于将集合中的文档删除,但不删除集合本身,也不删除集合的索引。
drop不仅删除集合的文档,也会删除集合本身,同时也会删除在集合上创建的索引。
类似mysql的delete和trunct
更新
更新集合中的文档,语法如下:
db.collection.update(criteria,objNew,upsert,multi)
参数说明:
criteria:用于设置查询条件的对象
objNew:用于设置更新内容的对象
upsert:如果记录已经存在,更新它,否则新增一个记录,取值为0或1
multi:如果有多个符合条件的记录,是否全部更新,取值为0或1
注意:默认情况下,只会更新第一个符合条件的记录
一般情况下后两个参数分别为0,1
如:
将name为user1的文档修改address为tj,其它键值对不变,命令为:
db.c1.update({name:”user1”},{$set:{address:”tj”}},0,1)
查询:
db.集合名称.find({条件})
或者使用 db.集合名称.findOne() 查询第一个文档
查询指定条件的数据
db.getCollection('user').find({name:'xgss'})//键加不加引号都可以,值如果是字符串要添加上引号(单引号双引号都可以),如果是数值类型不能加引号
多条件
db.getCollection('user').find({name:'xgss',age:20})
查询集合c中y的值为null或者不存在
db.c.find( { “y” : null } )
查询集合c中y的值为null,(仅返回y的值为null的数据,不会返回不存在的)
>db.c.find( { “y” : { $type : 10 } } )
还有一种写法如下
>db.c.find({“y”:{“$in”:[null], “$exists”:true}})
查询深层结构的数据可以用点,如:
db.getCollection('resumeVo').find({"resumeInfo.telephone":"13240335573"})
查询指定条件指定返回字段数据
db.getCollection('user').find({name:'xgss'},{userid:1})//只返回符合条件的userid字段
db.getCollection('user').find({name:'xgss'},{userid:0})//返回符合条件的除了userid的其他字段
如果要查询所有数据的指定字段,条件的地方写{}就可以了
查询使用条件表达式(<, <=, >, >=,!=)
//大于: field > value
db.collection.find({field:{$gt:value}});
//小于: field < value
db.collection.find({field:{$lt:value}});
//大于等于: field >= value
db.collection.find({field:{$gte:value}});
//小于等于: field <= value
db.collection.find({field:{$lte:value}});
//不等于: field != value
db.collection.find({field:{$ne:value}});
时间范围查询
db.getCollection('logLotteryVo').find({"activityCode" : "CJ191012001","createTime":{$gte:ISODate("2019-11-05T16:00:00.000Z"),$lt:ISODate("2019-11-06T09:32:35.000Z")}})
查询统计(count)、排序(sort)、分页(skip、limit)
db.customer.count();
db.customer.find().count();
db.customer.find({age:{$lt:5}}).count();
db.customer.find().sort({age:1}); //1是升序,-1
db.customer.find().skip(2).limit(3);//跳过2条,查询3条
db.customer.find().sort({age:-1}).skip(2).limit(3);
db.customer.find().sort({age:-1}).skip(2).limit(3).count();
db.customer.find().sort({age:-1}).skip(2).limit(3).count(0);
db.customer.find().sort({age:-1}).skip(2).limit(3).count(1);
$ne 查询不等于
$all 查询包含所有
$in 查询包含其中一个或多个
$nin 查询不包括其中一个或多个
$or 查询或者关系
$nor 查询不满足或者关系
$exists查询存在某个键的文档或不存在
Distinct查询:
db.collection.distinct(field, query)
模糊查询
db + 表名 + find({属性名:{$regex:{/查询的内容/}}})
例:
db.admins.find(user:{$regex:{/hehe/}})
也可以去掉$regex
查询包含xxx
{name:/xxx/}
查询以xxx开头
{name:/^xxx/}
查询以xxx结尾
{name:/xxx^/}
查询忽略大小写
{name:/xxx/i}
db.collection.group({ key, reduce, initial [, keyf] [, cond] [, finalize] })
aggregate() 聚合
管道聚合:将当前命令的输出结果作为下一个命令的参数。
db.runCommand()在当前数据库的上下文中运行命令。
在公司mongo的使用
使用mongo存储日志信息,比如抽奖,秒杀等,后台信息的修改日志;上架的活动信息,上架的奖品信息等

浙公网安备 33010602011771号