基础概念

MongoDB是一款为web应用程序和互联网基础设施设计的数据库管理系统,是NoSQL类型的数据库。

对比RDBMS

  1. MongoDB提出的是文档、集合的概念,使用BSON(类JSON)作为其数据模型结构,其结构是面向对象的而不是二维表,存储一个用户在MongoDB中是这样子的。

    1. {
    2. username:'123',
    3. password:'123'
    4. }

    使用这样的数据模型,使得MongoDB能在生产环境中提供高读写的能力,吞吐量较于关系型数据库大大增强。

  2. 易伸缩,自动故障转移。易伸缩指的是提供了分片能力,能对数据集进行分片,数据的存储压力分摊给多台服务器。自动故障转移是副本集的概念,MongoDB能检测主节点是否存活,当失活时能自动提升从节点为主节点,达到故障转移。

  3. 数据模型因为是面向对象的,所以可以表示丰富的、有层级的数据结构,比如博客系统中能把“评论”直接怼到“文章“的文档中,而不必像myqsl一样创建三张表来描述这样的关系。

    主要特性

    文档数据类型:

    SQL类型的数据库是正规化的,可以通过主键或者外键的约束保证数据的完整性与唯一性,所以RDBMS常用于对数据完整性较高的系统。MongoDB在这一方面是不如SQL类型的数据库,且MongoDB没有固定的Schema,正因为MongoDB少了一些这样的约束条件,可以让数据的存储数据结构更灵活,存储速度更加快

    schema(模式)是数据库的组织和结构,模式中包含了schema对象,可以是表、列、数据类型、视图、存储过程、关系、主键、外键等。

即时查询能力

MongoDB保留了关系型数据库即时查询的能力,保留了索引(底层是基于B tree)的能力。这一点汲取了关系型数据库的优点,相比于同类型的NoSQL redis 并没有上述的能力。

查阅平衡二叉树、B树、B+树数据结构概念

复制能力

MongoDB自身提供了副本集能将数据分布在多台机器上实现冗余,目的是可以提供自动故障转移扩展读能力

速度与持久性

MongoDB的驱动实现一个写入语义 fire and forget ,即通过驱动调用写入时,可以立即得到返回得到成功的结果(即使是报错),这样让写入的速度更加快,当然会有一定的不安全性,完全依赖网络。

MongoDB提供了Journaling日志的概念,实际上像mysql的bin-log日志,当需要插入的时候会先往日志里面写入记录,再完成实际的数据操作,这样如果出现停电,进程突然中断的情况,可以保障数据不会错误,可以通过修复功能读取Journaling日志进行修复。

数据扩展

MongoDB使用分片技术对数据进行扩展,MongoDB能自动分片、自动转移分片里面的数据块,让每一个服务器里面存储的数据都是一样大小

Mongo服务模型

MongoDB核心服务器主要是通过mongod程序启动的,而且在启动时不需对MongoDB使用的内存进行配置,因为其设计哲学是内存管理最好是交给操作系统缺少内存配置是MongoDB的设计亮点,另外,还可通过mongos路由服务器使用分片功能。
MongoDB的主要客户端是可以交互的js shell 通过mongo启动,使用js shell能使用js直接与MongoDB进行交流,像使用sql语句查询mysql数据一样使用js语法查询MongoDB的数据,另外还提供了各种语言的驱动包,方便各种语言的接入。
MongoDB结合键值存储和关系数据库的最好特性。因为简单,所以数据极快,而且相对容易伸缩提供复杂查询机制的数据库。MongoDB需要跑在64位的服务器上面,且最好单独部署,因为是数据库,所以也需要对其进行热备、冷备处理。


MongoDB常见shell

mongodump

备份数据库,输出BSON格式数据

  1. $ mongodump -h dbhost -d dbname -o dbdirectory

mongorestore

恢复数据库,源于mongodump的数据

  1. $ mongorestore -h <hostname><:port> -d dbname <path>

mongoexport

用来导出JSON、CSV和TSV数据。数据需要支持多格式时有用。mongoimport还能用于大数据集的初始导入。

  • -h,—host :代表远程连接的数据库地址,默认连接本地Mongo数据库;
  • —port:代表远程连接的数据库的端口,默认连接的远程端口27017;
  • -u,—username:代表连接远程数据库的账号,如果设置数据库的认证,需要指定用户账号;
  • -p,—password:代表连接数据库的账号对应的密码;
  • -d,—db:代表连接的数据库;
  • -c,—collection:代表连接数据库中的集合;
  • -f, —fields:代表集合中的字段,可以根据设置选择导出的字段;
  • —type:代表导出输出的文件类型,包括csv和json文件;
  • -o, —out:代表导出的文件名;
  • -q, —query:代表查询条件;
  • —skip:跳过指定数量的数据;
  • —limit:读取指定数量的数据记录;
  • —sort:对数据进行排序,可以通过参数指定排序的字段,并使用 1 和 -1 来指定排序的方式,其中 1 为升序排列,而-1是用于降序排列,如sort({KEY:1})。

    1. $ mongoexport -d dbname -c collection -f a,b,c --type=csv -o "<path>/test.csv"

    mongoimport

    用来导入JSON、CSV和TSV数据。数据需要支持多格式时有用。

  • -h,—host :代表远程连接的数据库地址,默认连接本地Mongo数据库;

  • —port:代表远程连接的数据库的端口,默认连接的远程端口27017;
  • -u,—username:代表连接远程数据库的账号,如果设置数据库的认证,需要指定用户账号;
  • -p,—password:代表连接数据库的账号对应的密码;
  • -d,—db:代表连接的数据库;
  • -c,—collection:代表连接数据库中的集合;
  • -f, —fields:代表导入集合中的字段;
  • —type:代表导入的文件类型,包括csv和json,tsv文件,默认json格式;
  • —file:导入的文件名称
  • —headerline:导入csv文件时,指明第一行是列名,不需要导入;
    1. $ mongoimport -d dbname -c collection -f a,b,c --type=csv --file "<path>/test.csv"

    mongosniff

    网络嗅探工具,用来观察发送到数据库的操作。基本上就是把网络上传输的BSON转换为易于人们阅读的shell语句

    切换数据库

    创建数据库并不是必须的操作,数据库与集合只有在第一次插入文档时才会被创建与对数据的动态处理方式是一致的。简化并加速开发过程,而且有利于动态分配命名空间。如果担心数据库或集合被意外创建,可以开启严格模式。

    严格模式: BSON类型的严格模式符合JSON RFC。任何JSON解析器都可以将这些严格模式的表示解析为键/值对;然而,仅有MongoDB内部JSON解析器可以识别由格式所表达的类型信息

  1. use dba

插入

  1. db.users.insert({username:"smith"})
  2. db.users.save({username:"smith"})
  • 区别: 若新增的数据中存在主键insert() 会提示错误,而save() 则更改原来的内容为新内容。如:
    已存在数据:{_id : 1, “ name “ : “ n1 “ },再次进行插入操作时,insert({_id : 1, “ name “ : “ n2 “ }) 会报主键重复的错误提示,save({ _id : 1, “ name “ : “ n2 “ }) 会把 n1 修改为 n2 。
  • 相同点: 若新增的数据中没有主键时会增加一条记录。已存在数据:{ _id : 1, “ name “ : “ n1 “ },再次进行插入操作时,insert({ “ name “ : “ n2 “ }) 插入的数据因为没有主键,所以会增加一条数据,save({ “ name “ : “ n2 “ }) 增加一条数据。

    查找

    1. db.users.find()
    2. db.users.count()

    更新

    ```javascript db.users.update({username:”smith”},{$set:{country:”Canada”}}) //把用户名为smith的用户的国家改成Canada

db.users.update({username:”smith”},{$unset:{country:1}}) //把用户名为smith的用户的国家字段给移除

db.users.update({username:”jones”},{$set:{favorites:{movies:[“casablance”,”rocky”]}}}) //这里主要体现多值修改,在favorties字段中添加多个值

db.users.update({“favorites.movies”:”casablance”},{$addToSet:{favorites.movies:”the maltese”}},false,true) //多项更新

  1. <a name="PfLVn"></a>
  2. ## 删除
  3. ```javascript
  4. db.foo.remove() //删除所有数据
  5. db.foo.remove({favorties.cities:"cheyene"}) //根据条件进行删除
  6. db.drop() //删除整个集合

索引

  1. db.numbers.ensureIndex({num:1})
  2. //创建一个升序索引
  3. db.numbers.getIndexes()
  4. //获取全部索引

基本管理

  1. show dbs
  2. //查询所有数据库
  3. show collections
  4. //显示所有表
  5. db.stats()
  6. //显示数据库状态信息
  7. db.numbers.stats()
  8. //显示集合表状态信息
  9. db.shutdownServer()
  10. //停止数据库
  11. db.help()
  12. //获取数据库操作命令
  13. db.foo.help()
  14. //获取表操作命令

Schema设计原则

MongoDB的自身特性

在关系型数据库中有带列和行的数据表。而MongoDB数据的基本单元是BSON文档,在键值中有指向不定类型值的键,MongoDB拥有即时查询,但不支持联结操作,简单的键值存储只能根据单个键来获取值,不支持事务,但支持多种原子更新操作

关注系统本身的读写特性

读写比是怎样的,需要何种查询,数据是如何更新的,会不会存在什么并发问题,数据结构化的程度是要求高还是低。系统本身的需求决定mysql还是MongoDB。

设计模式

  • 内嵌与引用 :当子对象总是出现在父对象的上下文中时,使用内嵌文档;否则将子对象单独存一个集合。
  • 一对多的关系 :在“多”的集合关系中添加id指向依赖的id。
  • 多对多 :在其中一种对应关系中使用对象数组指向另外一个对象。
  • :具化路径,在树中的每个节点都包含一个path字段,该字段具体保存了每个节点祖先的id
  • 动态属性 :可以为不同的动态属性添加索引,如果需要将属性圈在一个范围,那么可以通过key-value的方式,然后在统一的key上面加索引
  • 关于事务 :如果需要事务支持,那么只能选择另一种数据库,或者提供补偿性事务来解决事务的问题。

    注意事项

  • 不能创建没用的索引

  • 不能在同一个字段中存不同的类型
  • 不能把多类实体都放在一个集合里 不能创建体积大、嵌套深的文档
  • 不能过多的创建集合,集合、索引、数据库的命名空间都是有限的
  • 不能创建无法分片的集合

    数据库的概念

    数据库是集合的逻辑与物理分组,MongoDB没有提供创建数据库的语法,只有在插入集合时,数据库才开始建立。创建数据库后会在磁盘分配一组数据文件,所有集合、索引和数据库的其他元数据都保存在这些文件中,查阅数据库使用磁盘状态可通过。
    1. db.stats()

    集合概念

    集合是结构上或概念上相似得文档的容器,集合的名称可以包含数字、字母或 . 符号,但必须以字母或数字开头,完全。

    文档

    其次是键值,在MongoDB里面所有的字符串都是UTF-8类型。数字类型包括double、int、long。日期类型都是UTC格式,所以在MongoDB里面看到的时间会比北京时间慢8小时。整个文档大小会限制在16m以内,因为这样可以防止创建难看的数据类型,且小文档可以提升性能,批量插入文档理想数字范围是10~200,大小不能超过16MB。

    索引与查询优化

    索引的作用

  1. 索引能显著减少获取文档的所需工作量,具体的对比可以通过 .explain()方法进行对比
  2. 解析查询时MongoDB通过最优计划选择一个索引进行查询,当没有最适合索引时,会先不同的使用各个索引进行查询,最终选出一个最优索引做查询
  3. 如果有一个a-b的复合索引,那么仅针对a的索引是冗余的
  4. 复合索引里的键的顺序是很重要的

    索引类型

  • 单键索引
  • 复合索引
  • 唯一性索引:索引列上增加了一层唯一约束。添加唯一性索引的数据列可以为空,但是只要存在数据值,就必须是唯一的。
  • 稀疏索引:叶子节点仅保存了键位信息以及该行数据的地址,有的稀疏索引只保存了键位信息机器主键

    索引的构建问题

    如果数据集很大时,构建索引将会花费很长的时间,且会影响程序性能,可通过以下命令查看索引构建时间
    1. db.currentOp()
    当使用 mongorestore 时会重新构建索引。当曾经执行过大规模的删除时,可使用
    1. db.values.reIndex()
    对索引进行压缩,重建。

    识别慢查询

  1. 查阅慢查询文件 ```javascript grep -E ‘([0-9])+ms’ mongod.log //使用grep 命令 识别命令信息

db.setProfillingLevel(2) //使用解刨器,将记录每次的读写到日志

db.setProfillingLevel(1) //只记录慢(100ms)操作

  1. 2. 分析慢查询
  2. MySQL 的慢查询日志不同,MongoDB Profile 记录是直接存在系统 db 里的,记录位置 system.profile。所以,我们只要查询这个 Collection 的记录就可以获取到我们的 Profile 记录了。
  3. 查看最新的 Profile 记录:
  4. ```javascript
  5. db.system.profile.find().sort({$natural:-1}).limit(1)

列出执行时间长于某一限度(5ms)的 Profile 记录

  1. > db.system.profile.find( { millis : { $gt : 5 } } )
  2. { "ts" : ISODate("2012-05-20T16:50:36.321Z"), "info" : "query test.system.profile reslen:1219 nscanned:8 \nquery: { query: {}, orderby: { $natural: -1.0 } } nreturned:8 bytes:1203", "millis": 0 }
  • ts: 该命令在何时执行
  • info: 本命令的详细信息
  • reslen: 返回结果集的大小
  • nscanned: 本次查询扫描的记录数
  • nreturned: 本次查询实际返回的结果集
  • millis: 该命令执行耗时,以毫秒记

    副本集

    副本集可以提供主从复制能力、热备能力、故障转移能力,增强了集群的强壮性和查询能力

    构建方式

    1. rs.initiate()
    2. rs.add("localhost:40001")
    3. rs.add("localhost:40002",{arbiterOnly:true})

    监控

    1. db.isMaster()
    2. rs.status()

    副本集的工作原理

    实际上MongoDB对副本集的操作跟mysql主从操作是差不多的

  • mysql: 主binlog -> 从relay.log -> 从bin.log -> 从数据库

  • mongodb: 主oplog -> 从oplog

写操作先被记录下来,添加到主节点的oplog里。与此同时,所有从结点复制oplog。首先,查看自己oplog里最后一条的时间戳;其次,查询主节点oplog里所有大于此时间戳的条目;最后,把那些条目添加到自己的oplog里并应用到自己的库里。从节点使用长轮询立即应用来自主结点oplog的新条目

当遇到以下情况,从节点会停止复制

  • 如果从节点在主节点的oplog里找不到它所同步的点,那么会永久停止复制
  • 一旦某个从节点没能 在主节点的oplog里找到它已经同步的点,就无法再保证这个从结点的完美副本

local数据库保存了所有副本集元数据和oplog日志

  • replset.minvalid:包含指定副本集成员的初始化同步信息
  • system.replset:保存在副本集配置文档
  • system.indexes:标准索引说明容器
  • me slaves:主要用于写关注

可以使用以下命令查看复制情况

  1. db.oplog.rs.findOne()

心跳检测

每个副本集成员每秒钟ping一次其他所有成员,可以通过rs.status()看到节点上次的心跳检测时间戳和健康状况。

故障转移

如果从节点和仲裁节点都被杀了,只剩下主节点,他会把自己降级成为从节点。

提交和回滚

如果主节点的数据还没有写到从库,那么数据不能算提交,当该主节点变成从节点时,便会触发回滚,那些没写到从库的数据将会被删除,可以通过rollback子目录中的BSON文件恢复回滚的内容。

驱动和复制

  1. 使用单节点链接

只能链接到主节点,如果链接到从节点的话,会被拒绝写入操作,但是如果没有使用安全模式,因为mongo的fire and forget 特性,会把拒绝写入的异常给吃掉。

  1. 使用副本集方式链接

根据写入的情况自动进行故障转移,但是当副本集进行新的选举时,还是会出现故障,如果不使用安全模式,依旧会出现写不进去,但现实成功的情况。

  1. 写关注

可以使用写关注来关注数据是否已经被写入MongoDB的库中,使用写关注会消耗性能,需要在速度和持久性之间做出权衡。

分片

当数据量过大,索引和工作数据集占用的内存就会越来越多,所以需要通过分片负载来解决这个问题

工作原理

分片组件

  • 分片:每个分片都是一个副本集
  • mongos路由器:是一个路由器,将读写请求指引到合适的分片上
  • 配置服务器config:持久化分片集群的元数据,包括:

    • 全局集群配置;
    • 每个数据库、集合和特定范围数据位置;
    • 一份变更记录,保存了数据在分片之间进行迁移的历史信息。
    • 配置服务器之间不是副本集形式存在,mongos向配置服务器提交信息时是两阶段提交,保证配置服务器之间的一致性。

      分片的核心操作

  • 分片一个集合:分片是根据一个属性的范围进行划分的,MongoDB使用所谓的分片键让每个文档在这些范围里找到自己的位置

  • 块:是位于一个分片中的一段连续的分片键范围,可以理解为若干个块组成分片,分片组成MongoDB的全部数据

    拆分和迁移

  • 块的拆分:初始化时只有一个块,达到最大块尺寸64MB或100000个文档就会触发块的拆分。把原来的范围一分为二,这样就有了两个块,每个块都有相同数量的文档。

  • 迁移:当分片中的数据大小不一时会产生迁移的动作,比如分片A的数据比较多,会将分片A里面的一些块转移到分片B里面去。分片集群通过在分片中移动块来实现均衡,是由名为均衡器的软件进程管理的,任务是确保数据在各个分片中保持均匀分布,当集群中拥有块最多的分片与拥有块最少分片的块差大于8时,均衡器就会发起一次均衡处理。

    操作

    1. sh.help() //查看分片相关帮助
    2. sh.addShard() //添加分片
    3. db,getSiblingDB("config").shards.find() //查看分片列表
    4. sh.status() //分片详情
    5. sh.enableSharding("cloud-docs") //开启一个数据库上的分片
    6. db.getSiblingDB("config").databases,find() //查看数据库列表
    7. sh.shardCollection("cloud-docs.spreadsheets",{username:1,_id:1}) //使用一个分片键定义一个分片集合spreadsheets,根据用户名进行切分
    8. sh.getSiiblingDB("config").collections.findOne() //查看集合列表
    9. db.chunks.count() //查看块的个数
    10. db.chunks.findOne() //查看块的信息
    11. db.changelog.count(}what:"split"|) //查看块切分日志
    12. db.changelog.find({what:"moveChunk.commit"}).count() //查看日志迁移记录

    查询与索引

    分片查询类型

  • 针对性查询:查询包含分片键

  • 全局查询或分散/聚集查:查询不包含分片键
  • 查询过程:通过分片键将查询路由给指定分片,一旦到了某个分片上,由分片自行决定使用哪个索引来执行该查询

    索引

    每个分片都维护了自己的索引,当在分片集合上声明索引时,每个分片都会为它那部分集合构建独立的索引,每个分片上的分片集合都应该拥有相同的索引。
    分片集合只允许在_id字段和分片键上添加唯一性索引,其他地方不行,因为这需要在分片间进行通信,实施起来很复杂。
    当创建分片时,会根据分片键创建一个索引。

    选择分片键

  1. 分片键是不可修改的、分片键的选择非常重要
  2. 低效的分片键
  • 分布性差:如使用BSON对象ID,那么会导致所有最新插入的文档都会落到某个很小的连续范围,无法分散插入
  • 缺乏局部性:升序分片键有明确的方向,完全随机的分片键则根本没有方向。前者无法分散插入,后者插入分散,如使用MD5作为分片键
  1. 理想的分片键
  • 将插入数据均匀分布到各个分片上
  • 保证CRUD操作能够利用局部性 有足够的粒度进行块拆分
  • 满足这些要求的分片键通常由两个字段组成,第一个是粗粒度的,第二个粒度较细

    生产环境中的分片

    部署拓扑

  • 复制mongod:需要独立的部署服务器

  • 配置服务器:配置服务器不需要有自己的机器

根据不同的数据中心划分
image.png

最低要求

  • 副本集每个成员,无论是完整的副本集节点还是仲裁节点,都需要放在不同的机器上 每个用于复制的副本集成员都需要有自己的机器
  • 副本集仲裁节点很轻量级,和其他进程共用一台机器即可
  • 配置服务器也可以选择与其他进程共用一台机器

image.png

注意事项

需要估计集群大小,可使用以下命令对现有集合进行分片处理

  1. sh.splitAt("cloud-docs.spreadsheets",{"username":"chen","_id":ObjectId("")})
  2. //手动拆分块
  3. sh.moveChunk("cloud-docs.spreadsheets",{username:"chen"},"shardB")
  4. //手动将某分块移至分片B
  5. db.runCommand({removeshard:"shard-1/arete:30100,arete:30101"})
  6. //删除分片
  7. db.runCommand({moveprimary:"test",to:"shard-0-test-rs"});
  8. //移动主分片

备份分片集群

备份分片时需要停止均衡器

  1. db.settings.update({_id:"ba;ancer"},{$set:{stopped:true},true});
  2. sh.setBalancerState(false);
  3. //停止均衡器,此时均衡器将进行最后一轮均衡
  4. db.locks.find({_id:"balancer"});
  5. sh.isBalancerRunning();
  6. //查看均衡器状态,任何状态大于0 的状态值都说明均衡器仍在进行中

部署和管理

使用64位机器、32位机器会制约mongodb的内存,使其最大值为1.5GB

CPU

mongodb 只有当索引和工作集都可放入内存时,才会遇到CPU瓶颈,CPU在mongodb使用中的作用是用来检索数据,如果看到CPU使用饱和的情况,可以通过查询慢查询日志排查是不是查询的问题导致的,如果是可以通过添加索引来解决问题
mongodb写入数据时会使用到CPU,但是mongodb写入时间一次只用到一个核,如果有频繁的写入行为,可以通过分片来解决这个问题

内存

大内存是mongodb的保障,如果工作集大小超过内存,将会导致性能下降,因为这将会增加数据加载入内存的动作

硬盘

mongodb默认每60s会与磁盘强制同步一次,称为后台刷新,会产生I/O操作。在重启时mongodb会将磁盘里面的数据加载至内存,高速磁盘将会减少同步的时间

文件系统

使用ext4 和 xfs 文件系统

文件描述符

linux 默认文件描述符是1024,需要大额度的提升这个额度。文件描述符不足可能会导致读取文件失败

文件描述符:简称fd,当应用程序请求内核打开/新建一个文件时,内核会返回一个文件描述符用于对应这个打开/新建的文件,其fd本质上就是一个非负整数,读写文件也是需要使用这个文件描述符来指定待读写的文件的

时钟

mongodb各个节点服务器之间使用ntp服务器

知识链接

  1. MongoDB 是什么?看完你就知道了!