mongoDB详解
mongodb是一个nosql数据库,它有高性能、无模式、文档型的特点。他是nosql数据库中功能最丰富,最像关系数据库的。
mongoDb基本介绍
- mongodb里面有以下几个核心概念:
- 文档:mongodb数据库的最小数据集,是由多个键值对有序组合的数据单元,类似于mysql的数据记录;
- 集合:由一组文档构成,类似于mysql的表;
- 库:每个数据库都是独立的,有自己的用户,权限,独立存储集合,类似于mysql的库;
- 实例:系统上运行的mongodb的进程,类似于mysql实例;
- MongoDb作为nosql数据库,它具有以下特性:
- 面向集合文档存储,适合存储json形式的数据;
- 格式自由,数据格式不固定,数据结构发生变更的同时不会影响程序运行;
- 面向对象的sql查询语句,基本涵盖关系型数据库的所有查询语句;
- 有索引的支持,查询效率更快;
- 支持复制和自动故障转移;
- 可以使用分片集群提升查询性能
- 针对以上的特性,我们可以在以下场景中采用mongdb数据库:1、应用不需要事务(这一条是最重要的,如果应用的事务的依赖很强,是不能选择该数据库的);2、数据模型无法确定,经常发生变更;3、应用的qps达到2000以上;4、应用存储的数据很大,达到TB级别以上;5、应用需要大量的地理位置查询或者文本查询。因此mongodb使用与游戏,社交,物流,物联网,视频直播这些场景。
- 上面介绍特性的时候说道mongodb是有索引支持的,它的索引采用的B树(B树,B+树,B*树的对比,可以参考mysql详解那篇文章的介绍),我们知道B树它的非叶子节点是可以存储数据信息的,数据库每次从磁盘中读取的文件大小又是固定,这样不就使每次读取的数据变少了吗?但是mongodb的使用场景经常是只针对一条数据的查询,这样查到节点,也就查到了对应的数据,这样其实是提高了查询效率。当然如果查询场景经常是范围数据这种,效率会变慢。
mongodb的安装与使用
-
这里的安装是以linux为例的,因为实际生产场景大部分都是在linux进行部署。接下来我看介绍一下mongdb的安装步骤;
-
先进入mongodb的官网MongoDB下载,选择要下载的版本以及系统,需要注意的是,在安装mongodb之前需要先准备好jdk的环境变量,选择好要下载的版本以后,点击copy link,在linux系统中执行以下命令;
-
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZdEJeJ3B-1688777273214)(C:\Users\quyanliang\AppData\Roaming\Typora\typora-user-images\1688776421675.png)]
-
wget https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-rhel70-5.0.11.tgz
-
下载完毕以后,解压该压缩包,接下来需要配置mongodb的环境变量
-
# 通过vi命令编辑以下文件 vi /etc/profile # 在该文件中追加以下命令 # 解压完以后mongo的文件夹地址 export MONGODB_HOME=/opt/self/mongodb export PATH=$PATH:$MONGODB_HOME/bin # 刷新环境变量 source /etc/profile
-
配置完环境变量以下,在创建对应的mongodb的数据以及日志存储位置,然后启动即可
-
# 创建mongodb存储位置 mkdir mongodb-data # 进入到创建的文件家中,创建数据和日志存储文件夹 mkdir data mkdir logs # 启动mongodb,指定数据以及日志存储位置,在解压后的mongodb的bin目录下执行 mongod --dbpath /opt/self/mongodb-data/data --logpath /opt/self/mongodb-home/ogs/mongodb.log --fork
-
启动完mongodb以后,我们可以通过一下命令,连接mongo
-
# 如果是在本机测试,可以简化为mongo mongo --host 127.0.0.1 --port 27017 # 通过以下命令查看当前存在的数据库 show dbs show databases
-
mongo在创建完成以后会默认存在admin,config,local这三个数据库,其中admin中存储的是mongodb的用户、角色等信息;config中存储的是分片集群基础信息;local中主要存储的是副本集的元数据。
基本命令
-
切换或者创建库
-
-- 切换指定库,如果该库不存在,则创建该库,在切换到该库 use database;
-
创建集合
-
-- 直接创建 db.createCollection(name) -- 如果collection不存在,则会先创建collection,这是一种隐式创建方式 db.collection.insert({id:1}) -- 查询当前库中存在的集合 show tables
-
文档的增删改查
-
-- 查询集合所有 db.collection.find() -- 查询集合的第一个 db.collection.findOne() -- 根据条件进行查询,支持传入两个参数 -- 第一个参数是过滤条件 -- 第二个是控制显示的数据列,1代表显示,0-代表不显示,如果不配置,默认显示所有 db.collection.find({id:1},{id:1,name:0}) -- 使用正则表达式,匹配姓名以a开头的数据 db.collection.find({name:/^a/}) -- 插入数据 db.collection.insert({id:1,name:"a"}) -- 批量插入数据 db.collection.insertMany([{id:1,name:"a"},{id:2,name:"b"}]) -- 覆盖更新,将id为一的这条数据更新为只有name=2,即使原数据存在sex=0,执行完以下语句以后, -- 这条语句也会变成只有name=2 db.correction.update({id:1},{name:2}) -- 选择更新,只更新对应的字段,下面的语句只会吧id=1的数据中,name修改为2,其它不变 -- 但是它只会更新满足条件的第一条数据 db.correction.update({id:1},{$set:{name:2}}) -- 选择更新,将所有id=1的数据中的name变更为2,其它数据不变 db.correction.update({id:1},{$set:{name:2}},{multi:true}) -- 删除数据,删除id为1的数据 db.collection.remove({id:1}) -- 删除全部数据 db.collection.remove({})
-
统计操作
-
-- 统计所有数据 db.collection.count() -- 统计id为1的数据 db.collection.count({id:1})
-
分页与排序
-
-- 查询前5条数据 db.collection.find().limit(5) -- 查询跳过前5条的其它数据 db.collection.find().skip(5) -- 分页查询,每页显示5条,查询第一页和第二页的数据 db.collection.find().skip(0).limit(5) db.collection.find().skip(5).limit(5) -- 排序,1-升序,-1-降序 db.collection.sort({id:1, age:-1})
-
范围查找
-
-- $gt-大于,$lt-小于,$gte-大于等于,$lte-小于等于,$ne-不等于 -- 查询id大于10的数据 db.collection.find({id:{$gt:10}}) -- 查询id大于10小于30的数据,需要注意这里必须使用$and db.collection.find({$and:[{id:{$gt:10}},{id:{$lt:30}}]}) -- 查询id是1和3的数据 db.collection.find({id:{$in:[1,3]}})
-
索引操作
-
-- 创建索引,1-升序索引,-1降序索引 db.collection.createIndex({id:1}) -- 查看集合已经创建的索引 db.collection.getIndexs() -- 移除指定索引 db.collection.dropIndex({id:1}) -- 移除集合的全部索引 db.collection.dropIndexes()
-
加锁操作
-
# 加锁 db.collection.fsyncLock() # 解锁 db.collection.fsyncUnlock()
-
事务操作(mongodb不推荐使用事务)
-
# 插入多条数据 db.test_1.insertMany([{ id: 1 }, { id: 2 }]); # 开启事务 var session = db.getMongo().startSession(); session.startTransaction(); # 获取需要修改的集合 var coll = session.getDatabase('test').getCollection("test_1"); # 进行数据更新 coll.updateOne({id: 1}, {$set: {name: "a"}}); # 获取的文档为id:1,name:a coll.findOne({id: 1}); # 获取的文档为id:1 db.test_1.findOne({id: 1}); # 事务回滚或者提交 session.abortTransaction();
-
针对事务有以下几个注意点:
- 事务必须在60s内完成,超时将取消
- 涉及事务的分片不能使用仲裁节点
- 事务会影响chunk(在后面的分片集群中会介绍)的迁移效率。在chunk迁移过程中会导致事务提交失败
- 多文档事务中的读操作必须使用主节点
- readConcern(在后文的复制集中会进行介绍)只应该在事务级别设置,不能设置在每次读写操作。
复制集
- 复制集指的就是mongodb的集群,它的主要意义是用来实现mongodb的高可用。其结构为一主多从的结构。其中从节点分为三类:
- 从主节点进行数据复制,可以对外部提供数据读取,并且当主节点出现问题以后,可以参与主节点的选举;
- 只能从主节点进行数据复制,但是没有选举功能;
- 只存在选举功能,这类节点是不推荐使用的,造成资源浪费;
- 复制集具有以下的做:
- 数据分发:这种是针对全球性网站类似的情况,将数据分发到不同的服务器,在读取时减少读取时间,提高读取效率;
- 读写分离:提高并发量,将不同类型的操作压力分到不同的节点上面;
- 异地容灾:当本地的数据中心出现问题时,切换到异地的数据中心上;
复制集的搭建
-
在这里简单介绍一下复制集的搭建,本文是在一台linux搭建三个mongodb节点来进行模拟,真实生产环境一个复制集的不同节点肯定是分布在不同的服务器上的。创建三个目录,分别为db1,db2,db3,在三个目录下添加配置文件:
-
# 日志文件 systemLog: # 日志为文件格式 destination: file # 日志文件的存储路径,不同的节点需要配置不同的路径 path: /data/db1/mongod.log logAppend: true storage: # mongodb数据存储位置,不同的节点需要配置不同的路径 dbPath: /data/db1 net: bindIp: 0.0.0.0 # 当前节点的端口 port: 28017 replication: # 复制集名称 replSetName: rs0 processManagement: fork: true
-
在启动时需要指定配置文件
-
mongod -f /data/db1/mongod.conf
-
连接入作为主节点的mongodb,然后执行以下命令:
-
# 复制集的初始化 rs.initiate() # 添加从节点 rs.add("centosvm:28018") rs.add("centosvm:28019") #查看当前复制集的状态 rs.status()
-
通过一下命令进行验证
-
# 在主节点插入一条数据 db.test.insert({ id:1 }) #在从节点执行一下命令 rs.slaveOk() db.test.find()
-
接下来用一个最简单的主从结构介绍复制集,一主三从。如小图所示:
-
在这个复制集中,数据的同步流程是:当主节点发生写操作时,它的写操作会被记录到oplog中,然后主节点将这个oplog推送给从节点,从节点通过在主节点上打开一个taiable游标读取oplog中的新数据,让后将数据同步到自己节点上,通过这种方式与主节点保持数据一致。
-
mongdb的复制集两两节点之间每2s发送一次心跳,如果5次心跳未收到,则判断为节点失联。如果失联的节点为主节点,此时就会发起选举,选出新的节点,选举是基于raft一致性算法实现,选举成功的必要条件是超过一半具有投票功能的节点存活。对于上面那个集群,当主节点失联,从节点必须有两个存活才能进行选举(注,复制集最多有50个节点,具有投票权额节点最多有7个)。
-
在进行选举的时候,能够选取为主节点的从节点必须具有以下的特点:
-
能够与更多的节点建立链接
-
oplog读取的位置最新
-
如果从节点设置了优先级,满足上述要求的优先级最高的节点
-
针对从节点,还有一些常见选项:
-
投票权:从节点默认具有投票权,可以关闭,单纯作为复制节点,参数为v;
-
优先级:优先级越高的节点在选举时同等条件下优先成为主节点,优先级为0的节点无法成为主节点,参数为priority;
-
隐藏:单纯作为数据备份,无法被应用访问,隐藏节点的优先级必须为0,参数为hidden;
-
延迟:在复制数据时,复制指定延迟时间之前的数据,与主节点保持时间差,常用来作为数据备份与恢复,参数为slaveDelay,单位为秒。
-
# 动态修改从节点的信息 conf=rs.conf() conf.members[2].secondaryDelaySecs = 5 conf.members[2].priority = 0 rs.reconfig(conf)
复制集的写策略
- 复制集的写策略是通过writeConcern来进行配置的,他代表的是多少个节点确认完成才算是写入成功。当数据写入主节点的时候,主节点会向从节点发送确认消息,如果从节点返回确认成功,才算一个从节点完成了确认,接下来主节点会再向从节点发送写入消息。如下图
- writeConcern的配置方式为:writeConcern:{w:n},你的赋值规则如下:
- 0:不关心写入结果,当应用发送写入命令以后,直接返回成功;
- 1:当应用发送写入命令以后,只要写入主节点,则返回成功;
- n:大于1小于总结点的一个数值,代表写入主节点以后,需要n-1个节点确认成功,才返回成功;
- majority:需要保证大多数节点完成确认,即超过总结点数的一半,才返回写入成功,这种方式为推荐方式;
- all:全部节点完成确认,才会返回成功;
- 在写操作这里还有一个特殊的日志文件journal日志文件,它类似于关系型数据库中的事务日志。它的作用为mongodb数据库由于故障宕机以后的快速恢复。设置参数为j,值为true时代表数据要写入journal日志文件才算成功;false代表写操作记录到内存即可。开启该配置以后,肯定会影响一部分写性能,但是它有利于数据库的快速恢复,建议生产环境进行开启。参考下图:
复制集的读策略
-
在复制集中只要节点不是隐藏节点或者仅投票节点,这些节点是都可以向外部提供读取权限的,那么我们在读取的时候,应该从哪个节点进行读取呢?这需要我们根据具体的业务情况进行策略配置,配置参数为readPreference。它有一下几种值可供选择:
-
primary:只从主节点进行读取,这是mongodb的默认值;
-
primaryPreferred:优先从主节点读取,如果当前主节点不可用,则选择从节点读取;
-
secondary:只选择从节点进行读取;
-
secondaryPreferred:优先选择从节点读取,如果从节点不可用,则从主节点读取;
-
nearest:选择最近的节点,这里的最近指的是地理位置。
-
这些策略分别适用的场景为:primary/primaryPreferred:对数据实时性要求较高的业务场景,因为从节点有一个数据复制的过程,会造成数据延迟;secondary/secondartPreferred:适用查询历史数据的业务场景,其中secondary也适用于报表生成这类需要耗费大量资源的业务场景;nearest:适用于地域性较广的应用,进行就近读取,减少传输时间。
-
readPreferred的配置方式有三种,如下:
-
# 在配置文件中的mongdb连接串后添加对应参数 mongodb://host1:27107,host2:27107/?replicaSet=rs&readPreference=secondary # 在java代码中 MongoCollection.withReadPreference(ReadPreference readPref) #在xshell控制台中 db.collection.find().readPref( "secondary")
-
上面的这种方式,只能指定一类节点,主从节点。但是在实际生产中可能需要根据硬件配置,业务场景来区分要读取哪些节点。这是我们可以通过给不同的mongodb节点打上不同的tag来区分,方式如下:
-
# 给mongodb打tag conf = rs.conf() conf.members[1].tags = {"type" : "a" } rs.reconfig(conf) #应用程序的mongodb配置文件 mongodb://host1:27107,host2:27107/?replicaSet=rs&type=a
-
上面介绍了我们要从哪个节点读取数据的方式。但是节点上的哪些数据是可以读取的呢?这就需要用到另一个参数readConcern。这个参数的配置值类似于数据库中的隔离级别。它有一下的策略配置,他们的隔离级别由上到下越来越强:
-
available:读取所有可用的数据,可以直接查询到写入复制集的数据;
-
local:读取所有可用且属于当前分片的数据,可以直接查询到该分片的数据;
-
majority:读取在大多数节点上提交的数据,使用该策略可以有效的避免脏读的问题。majority采用mvcc机制来保证数据的读取。通过维护多个快照来链接不同的版本,每个被大多数节点确认过的版本都将是一个快照,快照持续到不再使用的时候才会进行删除。因为该策略是需要写入操作需要大多数节点完成确认,才能够被读取,当本次写入没有被大多数节点确认,主节点就发生回滚,那么本次写入对其它应用是不可见的;
-
linearizable:可线性化读取文档;
-
snapshot:读取最近快照中的数据,即每次读取都是从同一个快照中进行读取,那么也就避不会出现脏读,不可重复读以及幻读。下面看一个可重复的例子:
-
# 开启事务,并且将readConcern设置为快照 var session = db.getMongo().startSession(); session.startTransaction({readConcern: {level: "snapshot"},writeConcern: {w: "majority"}}); var coll = session.getDatabase('test').getCollection("test_1");、 # 读取id为1的数据,获得的文档为id:1 coll.findOne({id: 1}); # 不通过事务对id为1的数据进行更新 db.tx.updateOne({id: 1}, {$set: {name: "b"}}); # 事务外读取id为1的数据,获得的文档为id:1,name:b db.tx.findOne({id: 1}); # 事务内再次读取id为1的数据,获取的文档为id:1,它与第一次读时用的是同一个快照,所以两次读取是一样的 coll.findOne({id: 1}); session.abortTransaction();
分片集群
- 分片集群可以理解为是有多个复制集组成一个可以横向扩展的集群,每个复制集中存储不同范围的数据。分片集群主要是为了解决当下数据量日益增大,当一个节点存储大量数据,在进行数据恢复时需要的时间更大的情况以及解决全球化网站数据的存储以及访问问题。我们先来看一下分片集群的架构图,如下:
- 路由节点:提供分片集群的统一入口,一个mongodb节点即可满足要求,但是为了高可用,还是部署两台,因为它只是起到一个路由转发的功能,不需要进行存储大量数据,使用低配的服务器即可。
- 配置节点:配置节点的架构就是一个普通的复制集,提供集群元数据的存储,分片数据分布的映射。我们一般采用hash的方式作为分片依据,如果使用范围作为分片标准,会导致数据分布不均匀。
- 数据节点:数据节点以复制集作为存储单位,每个数据节点的数据是不重复的。一个数据节点就是1片,最大能达到1024分片。当数据量越来越大时,我们可以通过添加数据节点的方式进行横向扩容。每个分片的大小最好不要超过3T,尽可能保证2T每片。
- 分片数量可以由一下三个维度来进行计算,然后取其最大值:1)总存储量/单台服务器容量;2)工作集大小/单台服务器硬盘0.6;3)并发总数/单服务器并发数0.7
- 下面介绍一下分片集群中的一些概念:
- shard key:片键,文档中的一个字段;
- doc:文档,包含shard key的一行数据
- chunk:块,包含多个文档;
- shard:分片,包含多个chunk;
- cluster:集群,包含多个cluster;
mongodb的数据备份
- mongodb的数据备份方式有两种:延迟节点备份以及全量备份+oplog。接下来我们做一个简单的介绍,一般这些工作是由运维完成。
- 延迟节点备份
- 在上面的介绍中,我们知道从节点可以配置延迟同步时间,我们可以利用这一机制进行数据备份,能够让主节点恢复到指定时间之前的状态。这种情况适用于误操作时进行数据恢复。
- 此时我们可以利用节点2以及oplog,将主节点的数据恢复到安全范围内的任意一个节点的数据状态。
全量备份+oplog
- 全量备份的方式有以下三种:
- 复制文件:复制数据库文件,在复制文件的时候需要先关闭或者锁定数据库,该复制过程要在从节点上完成,最好是设置为hidden的节点,这样对整个应用不会产生影响;
- 文件快照:mongodb支持使用文件快照直接获取数据库某一时刻的镜像,快照过程不用统计,但是必须要要与journal在一起;
- mongodump:最灵活的一种备份方式,但是消耗时间长。在t1时间点进行备份,会在t3时间点备份完成,索引mongodunp备份的文件不是某一个时间点的数据,而是一个时间段的数据。
- 当我们要进行数据恢复时,可以找到离恢复时间点最近的一次全量备份,然后再加上这段时间范围之内的oplog,即可完成数据恢复。如果全量备份的频率较高,并且对数据的完整性要求不高的话,可以只恢复到最近的一次全量备份。