NoSQL 数据库与 MongoDB

本文合并自:05-NoSQL数据库.md、06-NoSQL数据库.md、01-MongoDB副本集集群.md

本文系统讲解 NoSQL 数据库的理论基础与工程实践。NoSQL 较好地满足了大数据时代各种非结构化数据的存储需求,具有灵活的可扩展性、灵活的数据模型,并与云计算紧密融合。全文先阐述 NoSQL 兴起的原因、NoSQL 与关系数据库的比较、NoSQL 四大类型、CAP / BASE / 最终一致性三大基石,以及从 NoSQL 到 NewSQL 的演进;随后以文档数据库 MongoDB 为主线,完整覆盖安装部署、增删改查、索引与聚合、副本集与分片集群、安全认证、备份恢复、监控排错等运维实战内容。


一、NoSQL 概述

1.1 什么是 NoSQL

NoSQL 数据库是为应对大数据时代的数据存储挑战而发展起来的一类非关系型数据库。通常,NoSQL 数据库具有以下几个特点:

  1. 灵活的可扩展性:可以方便地通过增加廉价节点实现水平扩展,应对海量数据增长。
  2. 灵活的数据模型:不要求预先定义严格的模式(schema),可以存储结构化、半结构化乃至非结构化数据。
  3. 与云计算紧密融合:天然契合云环境弹性伸缩、按需服务的特性。

现在已经有很多公司使用了 NoSQL 数据库,代表性用户包括:

  • 国外:Google、Facebook、Mozilla、Adobe、Foursquare、LinkedIn、Digg、McGraw-Hill Education、Vermont Public Radio
  • 国内:百度、腾讯、阿里、新浪、华为等

1.2 NoSQL 兴起的原因

1.2.1 关系数据库无法满足 Web 2.0 的需求

关系数据库已经无法满足 Web 2.0 的需求,主要表现在以下几个方面:

  1. 无法满足海量数据的管理需求;
  2. 无法满足数据高并发的需求;
  3. 无法满足高可扩展性和高可用性的需求。

以 MySQL 集群为例,它并不能完全解决上述问题,具体表现为:

  • 复杂性:部署、管理、配置都很复杂。
  • 数据库复制:MySQL 主备之间采用异步复制,当主库压力较大时可能产生较大延迟,主备切换可能丢失最后一部分更新事务,这时往往需要人工介入,备份和恢复不方便。
  • 扩容问题:如果系统压力过大需要增加新机器,这个过程涉及数据重新划分,整个过程比较复杂且容易出错。
  • 动态数据迁移问题:如果某个数据库组压力过大,需要将其中部分数据迁移出去,迁移过程需要总控节点整体协调以及数据库节点配合,很难做到自动化。

1.2.2 "One size fits all" 模式不再适用

关系模型作为统一的数据模型,既被用于数据分析,也被用于在线业务。但这两者一个强调高吞吐,一个强调低延时,已经演化出完全不同的架构,用同一套模型来抽象显然不合适:

  • Hadoop 针对数据分析场景;
  • MongoDB、Redis 等针对在线业务场景。

两者都抛弃了关系模型。

1.2.3 关系数据库关键特性成为"鸡肋"

关系数据库引以为傲的两个关键特性——完善的事务机制和高效的查询机制,到了 Web 2.0 时代却成了"鸡肋",主要表现在:

  1. Web 2.0 网站系统通常不要求严格的数据库事务;
  2. Web 2.0 并不要求严格的读写实时性;
  3. Web 2.0 通常不包含大量复杂的 SQL 查询(去结构化,以存储空间换取更好的查询性能)。

1.3 NoSQL 与关系数据库的全面比较

下表从多个维度对 NoSQL 数据库和关系数据库(RDBMS)进行了比较。

比较标准 RDBMS NoSQL 备注
数据库原理 完全支持 部分支持 RDBMS 有关系代数理论作为基础;NoSQL 没有统一的理论基础
数据规模 大 超大 RDBMS 很难实现横向扩展,纵向扩展空间有限,性能随数据规模增大而降低;NoSQL 可以很容易通过添加更多设备支持更大规模数据
数据库模式 固定 灵活 RDBMS 需要定义数据库模式,严格遵守数据定义和相关约束;NoSQL 不存在数据库模式,可以自由灵活定义并存储各种不同类型的数据
查询效率 复杂查询性能高 简单查询快,复杂查询不尽人意 RDBMS 借助索引机制可以实现快速查询(包括记录查询和范围查询);很多 NoSQL 数据库没有面向复杂查询的索引,虽然可使用 MapReduce 加速,但复杂查询性能仍不如 RDBMS
一致性 强一致性 弱一致性 RDBMS 严格遵守事务 ACID 模型,可保证事务强一致性;很多 NoSQL 放松了对 ACID 的要求,遵守 BASE 模型,只能保证最终一致性
数据完整性 容易实现 很难实现 RDBMS 可通过主键/非空约束实现实体完整性,通过主键/外键实现参照完整性,通过约束/触发器实现用户自定义完整性;NoSQL 无法实现
扩展性 一般 高 RDBMS 很难横向扩展,纵向扩展空间有限;NoSQL 在设计之初就充分考虑了横向扩展需求,可以很容易通过添加廉价设备实现扩展
可用性 好 很好 RDBMS 任何时候都以保证数据一致性为优先目标,随数据规模增大只能提供相对较弱的可用性;大多数 NoSQL 都能提供较高的可用性
标准化 已标准化(SQL) 没有行业标准 不同 NoSQL 数据库都有自己的查询语言,很难规范应用程序接口。Stonebraker 认为:NoSQL 缺乏统一查询语言,将会拖慢 NoSQL 发展
技术支持 高 低 RDBMS 经过几十年发展已非常成熟,Oracle 等大型厂商可提供很好的技术支持;NoSQL 在技术支持方面仍处于起步阶段
可维护性 复杂 复杂 RDBMS 需要专门的 DBA 维护;NoSQL 虽然没有 DBMS 复杂,但也难以维护

1.3.1 优劣势总结

(1)关系数据库

  • 优势:以完善的关系代数理论作为基础,有严格的标准,支持事务 ACID 四性,借助索引机制可以实现高效查询,技术成熟,有专业公司的技术支持。
  • 劣势:可扩展性较差,无法较好支持海量数据存储;数据模型过于死板,无法较好支持 Web 2.0 应用;事务机制影响了系统整体性能。

(2)NoSQL 数据库

  • 优势:可以支持超大规模数据存储;灵活的数据模型可以很好地支持 Web 2.0 应用;具有强大的横向扩展能力。
  • 劣势:缺乏数学理论基础;复杂查询性能不高;大都不能实现事务强一致性,很难实现数据完整性;技术尚不成熟,缺乏专业团队的技术支持;维护较困难。

关系数据库和 NoSQL 数据库各有优缺点,彼此无法取代:

  • 关系数据库应用场景:电信、银行等领域的关键业务系统,需要保证强事务一致性。
  • NoSQL 数据库应用场景:互联网企业、传统企业的非关键业务(如数据分析)。

实践中通常采用混合架构。亚马逊公司就使用不同类型的数据库来支撑其电子商务应用:

  • 对于"购物篮"这种临时性数据,采用键值存储更加高效;
  • 当前的产品和订单信息适合存放在关系数据库中;
  • 大量的历史订单信息则适合保存在类似 MongoDB 的文档数据库中。

关于 NoSQL 依赖的 MapReduce 批处理模型、以及 HBase 等列族数据库如何借助 Hadoop 生态实现扩展性,详见 16 章大数据部分(交叉引用见本文第五、六阶段的相关小节)。

1.4 NoSQL 的四大类型

NoSQL 数据库虽然数量众多,但典型的 NoSQL 数据库通常包括以下四大类型:键值数据库、列族数据库、文档数据库和图形数据库。

(原图:NoSQL 四大类型示意)

1.4.1 键值数据库

项目 说明
相关产品 Redis、Riak、SimpleDB、Chordless、Scalaris、Memcached
数据模型 键/值对。键是一个字符串对象;值可以是任意类型的数据,如整型、字符型、数组、列表、集合
典型应用 涉及频繁读写、拥有简单数据模型的应用;内容缓存(如会话、配置文件、参数、购物车等);存储配置和用户数据信息的移动应用
优点 扩展性好,灵活性好,大量写操作时性能高
缺点 无法存储结构化信息,条件查询效率较低
不适用情形 不是通过键而是通过值来查询(键值数据库没有通过值查询的途径);需要存储数据之间的关系(不能通过两个或两个以上的键来关联数据);需要事务支持(一些键值数据库在产生故障时不可以回滚)
使用案例 百度云数据库(Redis)、GitHub(Riak)、BestBuy(Riak)、Twitter(Redis 和 Memcached)、StackOverflow(Redis)、Instagram(Redis)、YouTube(Memcached)、Wikipedia(Memcached)

键值数据库是理想的缓冲层解决方案。Redis 有时被称为"强化版的 Memcached",它支持持久化、数据恢复和更多数据类型。

1.4.2 列族数据库

项目 说明
相关产品 BigTable、HBase、Cassandra、HadoopDB、GreenPlum、PNUTS
数据模型 列族
典型应用 分布式数据存储与管理;数据在地理上分布于多个数据中心的应用;可以容忍副本中存在短期不一致情况的应用;拥有动态字段的应用;拥有潜在大量数据(大到几百 TB)的应用
优点 查找速度快,可扩展性强,容易进行分布式扩展,复杂性低
缺点 功能较少,大都不支持强事务一致性
不适用情形 需要 ACID 事务支持的情形,Cassandra 等产品不适用
使用案例 eBay(Cassandra)、Instagram(Cassandra)、NASA(Cassandra)、Twitter(Cassandra and HBase)、Facebook(HBase)、Yahoo!(HBase)

1.4.3 文档数据库

"文档"其实是一个数据记录,这个记录能够对包含的数据类型和内容进行"自我描述"。XML 文档、HTML 文档和 JSON 文档都属于这一类。SequoiaDB 就是使用 JSON 格式的文档数据库。

与关系数据库对比:

  • 关系数据库必须有 schema 信息才能理解数据的含义。例如:学生(学号,姓名,性别,年龄,系,年级),记录如 (1001,张三,男,20,计算机,2002)。
  • XML 文档则自描述其结构与内容,例如:
<configuration>
  <property>
    <name>hbase.rootdir</name>
    <value>hdfs://localhost:9000/hbase</value>
  </property>
</configuration>

文档数据库中的数据是不规则的,每一条记录包含了所有相关信息而没有任何外部引用,这条记录就是"自包含"的。这使得记录很容易完全移动到其他服务器,因为这条记录的所有信息都包含在里面了,不需要考虑还有信息在别的表没有一起迁移走。同时,在移动过程中只有被移动的那一条记录(文档)需要操作,而不像关系型中每个有关联的表都需要锁住来保证一致性,因此 ACID 保证会更快速,读写速度也会有很大提升。

项目 说明
相关产品 MongoDB、CouchDB、Terrastore、ThruDB、RavenDB、SisoDB、RaptorDB、CloudKit、Perservere、Jackrabbit
数据模型 键/值,值(value)是版本化的文档
典型应用 存储、索引并管理面向文档的数据或者类似的半结构化数据;如后台具有大量读写操作的网站、使用 JSON 数据结构的应用、使用嵌套结构等非规范化数据的应用程序
优点 性能好(高并发),灵活性高,复杂性低,数据结构灵活;提供嵌入式文档功能,将经常查询的数据存储在同一个文档中;既可以根据键构建索引,也可以根据内容构建索引
缺点 缺乏统一的查询语法
不适用情形 在不同的文档上添加事务。文档数据库并不支持文档间的事务
使用案例 百度云数据库(MongoDB)、SAP(MongoDB)、Codecademy(MongoDB)、Foursquare(MongoDB)、NBC News(RavenDB)

1.4.4 图形数据库

项目 说明
相关产品 Neo4J、OrientDB、InfoGrid、Infinite Graph、GraphDB
数据模型 图结构
典型应用 专门用于处理具有高度相互关联关系的数据,比较适合于社交网络、模式识别、依赖分析、推荐系统以及路径寻找等问题
优点 灵活性高,支持复杂的图形算法,可用于构建复杂的关系图谱
缺点 复杂性高,只能支持一定的数据规模
使用案例 Adobe(Neo4J)、Cisco(Neo4J)、T-Mobile(Neo4J)

1.4.5 不同类型数据库比较分析

  • MySQL 产生年代较早,随 LAMP 大潮得以成熟。尽管没有大的改进,但是新兴互联网使用最多的数据库。
  • MongoDB 是新生事物,提供更灵活的数据模型、异步提交、地理位置索引等丰富的功能。
  • HBase 是"仗势欺人"的大象兵,依仗 Hadoop 的生态环境获得很好的扩展性。但就像象兵一样,使用者需要养一头大象(Hadoop)才能驱使它。
  • Redis 是键值存储的代表,功能最简单,提供随机数据存储,就像一根棒子没有多余构造,但也正因此伸缩性特别好,就像孙悟空手里的金箍棒,大可捅破天,小能缩成针。

1.5 NoSQL 的三大基石

NoSQL 的三大理论基石是 CAP、BASE 和最终一致性,它们是理解一切 NoSQL 产品设计取舍的基础。

1.5.1 CAP 理论

CAP 分别指:

  • C(Consistency,一致性):任何一个读操作总是能够读到之前完成的写操作的结果。即在分布式环境中,多点的数据是一致的,所有节点在同一时间具有相同的数据。
  • A(Availability,可用性):快速获取数据,可以在确定的时间内返回操作结果,保证每个请求不管成功或失败都有响应。
  • P(Tolerance of Network Partition,分区容忍性):当出现网络分区(即系统中一部分节点无法和其他节点通信)时,分离的系统也能够正常运行,即系统中任意信息的丢失或失败不会影响系统继续运作。

CAP 理论告诉我们:一个分布式系统不可能同时满足一致性、可用性和分区容忍性这三个需求,最多只能同时满足其中两个,正所谓"鱼和熊掌不可兼得"。

不可同时满足的证明思路:假设系统中有两个节点 N1 和 N2,各持有数据副本 V0。现在网络在 N1 与 N2 之间发生分区(P 必须容忍),客户端向 N1 写入 V1。

  • 若系统要保证 C:N1 必须等待把 V1 同步给 N2 后才能返回成功,但分区导致同步无法完成,N1 只能一直阻塞或返回失败 —— A 被牺牲。
  • 若系统要保证 A:N1 立即返回写成功,此时客户端从 N2 读到的仍是旧值 V0 —— C 被牺牲。

因此在网络分区不可避免的前提下(分布式系统中 P 是客观存在的),C 与 A 只能二选一。

(原图:一个牺牲一致性来换取可用性的实例——初始状态、正常执行过程、更新传播失败时的执行过程)

当处理 CAP 问题时,有以下几个明显的选择:

  • CA:强调一致性(C)和可用性(A),放弃分区容忍性(P)。最简单的做法是把所有与事务相关的内容都放到同一台机器上,这会严重影响系统可扩展性。传统关系数据库(MySQL、SQL Server、PostgreSQL)都采用了这种设计原则,因此扩展性都比较差。
  • CP:强调一致性(C)和分区容忍性(P),放弃可用性(A)。当出现网络分区时,受影响的服务需要等待数据一致,因此等待期间无法对外提供服务。
  • AP:强调可用性(A)和分区容忍性(P),放弃一致性(C),允许系统返回不一致的数据。

各类系统的典型代表:

类型 取舍 代表产品
CA 一致性 + 可用性,放弃分区容忍 MySQL、SQL Server、PostgreSQL 等传统关系数据库(单机/单点事务)
CP 一致性 + 分区容忍,放弃可用性 HBase、BigTable、MongoDB(默认写关注下)、Redis 主从、ZooKeeper、etcd
AP 可用性 + 分区容忍,放弃强一致 Cassandra、Dynamo、Voldemort、Riak、CouchDB、SimpleDB

(原图:不同产品在 CAP 理论下的不同设计原则)

1.5.2 BASE 理论

说起 BASE(Basically Available、Soft-state、Eventual consistency),不得不谈到 ACID。二者对比如下:

ACID BASE
原子性(Atomicity) 基本可用(Basically Available)
一致性(Consistency) 软状态/柔性事务(Soft state)
隔离性(Isolation) 最终一致性(Eventual consistency)
持久性(Durability) ——

一个数据库事务具有 ACID 四性:

  • A(Atomicity,原子性):事务必须是原子工作单元,对其数据修改要么全都执行,要么全都不执行。
  • C(Consistency,一致性):事务完成时,必须使所有数据保持一致状态。
  • I(Isolation,隔离性):由并发事务所做的修改必须与其他并发事务所做的修改隔离。
  • D(Durability,持久性):事务完成之后对系统的影响是永久性的,即使出现致命的系统故障修改也将一直保持。

BASE 的基本含义是基本可用(Basically Available)、软状态(Soft-state)、最终一致性(Eventual consistency):

  • 基本可用:分布式系统的一部分发生问题变得不可用时,其他部分仍然可以正常使用,也就是允许分区失败的情形出现。
  • 软状态:与"硬状态(hard-state)"相对应。数据库保存的数据是"硬状态"时,可以保证数据一致性,即保证数据一直是正确的;"软状态"是指状态可以有一段时间不同步,具有一定的滞后性。
  • 最终一致性:一致性的类型包括强一致性和弱一致性,二者的主要区别在于高并发的数据访问操作下,后续操作是否能够获取最新的数据。对于强一致性而言,当执行完一次更新操作后,后续的其他读操作就可以保证读到更新后的最新数据;反之,如果不能保证后续访问读到的都是更新后的最新数据,那么就是弱一致性。最终一致性是弱一致性的一种特例,允许后续的访问操作可以暂时读不到更新后的数据,但经过一段时间之后,必须最终读到更新后的数据。最常见的实现最终一致性的系统是 DNS(域名系统):一个域名更新操作根据配置被分发出去,并结合有过期机制的缓存,最终所有客户端都可以看到最新的值。

1.5.3 最终一致性与数据一致性模型

按照"更新数据后各进程访问到数据的时间和方式"的不同,一致性可细分为下列模型:

一致性模型 含义
强一致性 更新完成后,后续任意读操作都能读到最新值。关系数据库、HDFS 属于此类
弱一致性 不保证后续读操作能读到最新值,也不承诺多久之后能读到
最终一致性 弱一致性的特例。允许暂时读不到新值,但经过一段时间后必须最终能读到新值
因果一致性 如果进程 A 通知进程 B 它已更新了一个数据项,那么进程 B 的后续访问将获得 A 写入的最新值;而与进程 A 无因果关系的进程 C 的访问,仍然遵守一般的最终一致性规则
"读己之所写"一致性 可以视为因果一致性的一个特例。当进程 A 自己执行一个更新操作之后,它自己总是可以访问到更新过的值,绝不会看到旧值
单调读一致性 如果进程已经看到过数据对象的某个值,那么任何后续访问都不会返回在那个值之前的值
会话一致性 把访问存储系统的进程放到会话(session)的上下文中,只要会话还存在,系统就保证"读己之所写"一致性。如果由于某些失败情形令会话终止,就要建立新的会话,而且系统保证不会延续到新的会话
单调写一致性 系统保证来自同一个进程的写操作顺序执行,否则就非常难以编程

如何实现各种类型的一致性?

对于分布式数据系统,定义以下三个参数:

  • N — 数据复制的份数;
  • W — 更新数据时需要保证写完成的节点数;
  • R — 读取数据时需要读取的节点数。

判定规则:

  • 如果 W + R > N,写节点和读节点重叠,是强一致性。例如典型的一主一备同步复制关系数据库,N=2, W=2, R=1,无论读主库还是备库都是一致的。一般设定 R + W = N + 1,这是保证强一致性的最小设定。
  • 如果 W + R <= N,则是弱一致性。例如一主一备异步复制的关系数据库,N=2, W=1, R=1,如果读的是备库,就可能无法读取主库已更新的数据,所以是弱一致性。

对于分布式系统,为保证高可用性,一般设置 N >= 3。不同的 N、W、R 组合,是在可用性和一致性之间取一个平衡:

  • 若 N = W, R = 1,任何一个写节点失效都会导致写失败,可用性降低,但由于数据分布的 N 个节点是同步写入的,因此可以保证强一致性。
  • 实例:HBase 借助底层 HDFS 实现数据冗余备份。HDFS 采用强一致性保证,在数据没有完全同步到 N 个节点前,写操作不会返回成功,即 W = N,而读操作只需要读到一个值即可,即 R = 1。
  • 像 Voldemort、Cassandra、Riak 这类 Dynamo 系统,通常允许用户按需设置 N、R、W,即使 W + R <= N 也可以,即允许在强一致性和最终一致性之间自由选择。当用户选择最终一致性或 W < N 的强一致性时,总会出现一段"各个节点数据不同步导致系统处理不一致的时间"。为了提供最终一致性支持,这些系统会提供一些工具使数据更新被最终同步到所有相关节点。

1.6 分布式数据库的分片与复制

几乎所有 NoSQL 产品的水平扩展能力,都建立在两个基本机制之上:

(1)复制(Replication)——解决可用性与读扩展

  • 同一份数据保存多个副本,分布在不同节点/机架/机房。
  • 主从复制(Master-Slave):一个主节点接收写,从节点异步或半同步复制。MongoDB 副本集、Redis 主从、MySQL 主备均属此类。
  • 多主复制 / 无主复制(Peer-to-Peer):任意节点均可接收写,通过版本向量、Quorum(W/R)解决冲突。Dynamo、Cassandra 属此类。
  • 收益:故障时可用副本接管(故障转移);读请求可分流到副本(读写分离)。
  • 代价:副本间存在复制延迟,产生一致性问题(见上文 N/W/R 模型)。

(2)分片(Sharding / Partitioning)——解决容量与写扩展

  • 把一个大数据集按某个"分片键"水平切分成多个子集,分布到不同节点上,每个节点只保存和处理一部分数据。
  • 范围分片(Range Sharding):按分片键取值区间划分,如按时间、按 ID 区间。范围查询高效,但容易产生热点(如自增键的写入永远落在最后一个分片)。
  • 哈希分片(Hash Sharding):对分片键做散列后划分,写入分布均匀,但范围查询需要广播到所有分片。
  • 一致性哈希:Dynamo/Cassandra 采用,节点增删时只影响相邻数据,迁移量小。
  • 分片 + 复制通常同时使用:每个分片自身又是一个复制组(MongoDB 分片集群中每个 shard 就是一个副本集)。

1.7 从 NoSQL 到 NewSQL 与云数据库

大数据的兴起引发了数据处理架构的变革。关系数据库、NoSQL 数据库和 NewSQL 数据库代表了不同阶段的产品:

  • 关系数据库在事务一致性、复杂查询方面表现优异,但扩展性受限;
  • NoSQL 牺牲了部分一致性和事务能力,换取了良好的水平扩展性和灵活性;
  • NewSQL 数据库则试图融合传统关系数据库和 NoSQL 的优点,既保留关系数据库的 SQL 支持和 ACID 事务,又具备 NoSQL 的水平扩展能力和高可用性。

典型 NewSQL 产品:Google Spanner / F1、CockroachDB、TiDB、OceanBase、VoltDB、Clustrix、MemSQL、NuoDB。

云数据库是部署和虚拟化在云计算环境中的数据库,用户按需租用、弹性伸缩、免运维,是 NoSQL "与云计算紧密融合"特性的直接体现。代表产品:阿里云 RDS / MongoDB 版、腾讯云 TencentDB、AWS DynamoDB / DocumentDB、Azure Cosmos DB。

详见 11-09-云数据库.md

1.8 MapReduce 与数据库的关系

NoSQL 数据库普遍缺乏面向复杂查询的索引与优化器,因此在做复杂分析时往往借助 MapReduce 批处理模型:把查询拆解为 Map(映射,逐条处理并输出中间键值对)与 Reduce(归约,按键汇总)两个阶段,在集群上并行执行。

  • MongoDB 内置 mapReduce 命令,主要用于对数据进行批量处理和聚合操作(自 2.2 起推荐改用聚合管道 aggregate,性能更好;5.0 起 mapReduce 已被标记为废弃)。
  • HBase 依托 Hadoop 生态,天然与 MapReduce / Spark 集成,是"数据分析型"NoSQL 的代表。
  • "One size fits all" 的破产正体现在此:Hadoop + MapReduce 面向数据分析(高吞吐),MongoDB / Redis 面向在线业务(低延时)。

关于 MapReduce 编程模型、Hadoop 生态与 HBase 的完整内容,详见 16-05-HBase分布式数据库.md 及 16 章其余各节。


二、MongoDB 概述与安装部署

2.1 MongoDB 简介

MongoDB(来自英文单词 "Humongous",中文含义为"庞大")是由 C++ 语言编写的、基于分布式文件存储的开源数据库系统,是一款可应用于各种规模企业、各个行业以及各类应用程序的开源数据库。在高负载情况下,添加更多节点可以保证服务器性能。MongoDB 旨在为 Web 应用提供可扩展的高性能数据存储解决方案。

作为一款适用于敏捷开发的数据库,MongoDB 的数据模式可以随着应用程序的发展而灵活更新;同时也为开发人员提供了传统数据库的功能:二级索引、完整的查询系统以及严格一致性等。MongoDB 能够使企业更具敏捷性和可扩展性,各种规模的企业都可以通过使用 MongoDB 来创建新的应用,提高与客户之间的工作效率,加快产品上市时间,降低企业成本。

MongoDB 专为可扩展性、高性能和高可用性而设计:

  • 可从单服务器部署扩展到大型、复杂的多数据中心架构;
  • 利用内存计算优势提供高性能的数据读写操作;
  • 本地复制和自动故障转移功能,使应用程序具备企业级的可靠性和操作灵活性。

MongoDB 将数据存储为一个文档,数据结构由键值(key/value)对组成。MongoDB 文档类似于 JSON 对象,字段值可以包含其他文档、数组及文档数组。

主要特性:

  • 提供面向文档存储,操作简单、容易;
  • 数据以 BSON(Binary JSON)二进制格式存储;
  • 动态模式(Schema-less):文档不需要设置相同的字段,相同字段也不需要相同的数据类型;
  • 可以为任何属性设置索引,实现更快的排序;
  • 具有较好的水平可扩展性;
  • 支持丰富的查询表达式,可轻易查询文档中内嵌的对象及数组;
  • 可以替换完整的文档(数据)或一些指定的数据字段;
  • MongoDB 中的 Map/Reduce 主要用于对数据进行批量处理和聚合操作;
  • 支持 RUBY、PYTHON、JAVA、C++、PHP、C# 等多种编程语言;
  • 安装简单。

2.2 适用与不适用场景

2.2.1 适合的使用场景

场景 说明
网站数据 非常适合实时的插入、更新与查询,并具备网站实时数据存储所需的复制及高度伸缩性
缓存 性能很高,适合作为信息基础设施的缓存层;系统重启后,由 MongoDB 搭建的持久化缓存可避免下层数据源过载
大尺寸、低价值的数据 使用传统关系数据库存储某些数据成本较高时,可替代传统的文件存储方式
高伸缩性场景 适合由数十乃至数百台服务器组成的数据库集群
对象及 JSON 数据存储 BSON 数据格式非常适合文档格式化的存储及查询

2.2.2 不适合的使用场景

  • 高度事务性的系统:例如银行或会计系统,传统关系型数据库目前更适用于需要大量原子性复杂事务的应用程序。
  • 传统的商业智能应用:针对特定问题的 BI 数据库会产生高度优化的查询方式,此类应用数据仓库可能是更合适的选择。
  • 需要跨文档复杂 JOIN 的场景:MongoDB 虽有 $lookup,但性能与表达能力不及 SQL。

2.3 核心概念与术语对照

在 MongoDB 中,基本的概念是文档(document)、集合(collection)、数据库(database)。下表列出了 SQL 术语/概念与 MongoDB 术语/概念的对应关系。

SQL 术语/概念 MongoDB 术语/概念 解释/说明
database database 数据库
table collection 数据库表/集合
row document 数据记录行/文档
column field 数据字段/域
index index 索引
table join —— 表连接,MongoDB 不支持(4.0+ 可用 $lookup 近似替代)
primary key primary key 主键,MongoDB 自动将 _id 字段设置为主键

RDBMS 与 MongoDB 对应的术语如下:

RDBMS MongoDB
数据库 数据库
表 集合
行 文档
字段 字段
表联合 嵌入文档
主键 主键(MongoDB 提供了 key 为 _id)
数据库服务端 mysqld / Oracle ↔ mongod
数据库客户端 mysql / sqlplus ↔ mongo(4.4+ 为 mongosh)

数据库:一个 MongoDB 中可以建立多个数据库。MongoDB 默认数据库为 db(shell 连接后默认切到 test),存储在 data 目录中。单个实例可以容纳多个独立的数据库,每个数据库都有自己的集合和权限,不同数据库也放置在不同文件中。系统保留库有 admin(权限库)、local(本机专属,存放 oplog)、config(分片配置)。

文档:文档是一个键值(key-value)对(即 BSON)。MongoDB 的文档不需要设置相同的字段,并且相同字段不需要相同的数据类型,这与关系数据库有很大区别,也是 MongoDB 非常突出的特点。一个简单的文档示例如下:

{"site": "dblab.xmu.edu.cn", "name": "厦门大学数据库实验室"}

集合:集合是 MongoDB 文档组,类似于 RDBMS 中的表。集合存在于数据库中,没有固定结构,意味着可以向集合插入不同格式和类型的数据,但通常插入集合的数据都会有一定关联性。例如可以将以下不同数据结构的文档插入到同一集合中:

{"site":"www.baidu.com"}
{"site":"dblab.xmu.edu.cn", "name":"厦门大学数据库实验室"}
{"site":"www.runoob.com","name":"菜鸟教程","num":5}

文档示例:通过下面的实例可以更直观地了解 MongoDB 中的概念。集合中存储了两个用户文档:

{
  "_id": ObjectId("5146bb52d8524270060001f3"),
  "age": 25,
  "city": "Los Angeles",
  "email": "mark@abc.com",
  "user_name": "Mark Hanks"
}
{
  "_id": ObjectId("5146bb52d8524270060001f2"),
  "age": 31,
  "city": "Dallas",
  "email": "richard@abc.com",
  "user_name": "Richard Peter"
}

嵌入文档示例:在关系数据库中,一篇博客(包含文章内容、评论、评论的投票)会被打散在多张数据表中;而在文档数据库 MongoDB 中,可以用一个文档表示一篇博客,评论与投票作为文档数组放在正文主文档中。这样数据更易于管理,消除了传统关系型数据库中影响性能和水平扩展性的 JOIN 操作。关系数据库中的一条记录在 MongoDB 中的存储方式类似如下:

{
  "id": 1,
  "author": "Jane",
  "blogposts": {
    "title": "MyFirstPost",
    "comment": {
      "by": "Ada",
      "text": "Good post"
    }
  }
}

2.3.1 _id 主键与 ObjectId 的构成

每个文档必须有唯一的 _id 字段作为主键。若插入时未指定,MongoDB 自动生成一个 12 字节的 ObjectId,其构成为:

字节偏移 长度 含义
0-3 4 字节 Unix 时间戳(秒),可由 ObjectId.getTimestamp() 取出,因此 ObjectId 天然按时间递增
4-8 5 字节 随机值(旧版本为 3 字节机器标识 + 2 字节进程 ID)
9-11 3 字节 自增计数器,同一秒内保证唯一
> var id = ObjectId()
> id.getTimestamp()          // 取出生成时间
ISODate("2018-11-28T09:09:03Z")
> id.str                     // 24 位十六进制字符串
5bfe642bb96cecc8f6910c88

_id 也可以自定义为任意唯一值(字符串、数字、复合子文档),但一旦写入不可修改。

2.4 BSON 数据类型全表

所有存储在集合中的数据都是 BSON 格式(Binary JSON,类 JSON 的二进制存储格式)。

数据类型 描述
String 字符串。存储数据常用的数据类型,UTF-8 编码的字符串才是合法的
Integer 整型数值,根据服务器可分为 32 位(NumberInt)或 64 位(NumberLong)
Boolean 布尔值,用于存储真/假
Double 双精度浮点值,用于存储浮点值
Decimal128 高精度十进制(3.4+,NumberDecimal),适合金额计算
Min/Max keys 将一个值与 BSON(二进制 JSON)元素的最低值和最高值相对比
Array 用于将数组、列表或多个值存储为一个键
Timestamp 时间戳,记录文档修改或添加的具体时间(内部使用,如 oplog)
Object 用于内嵌文档
Null 用于创建空值
Symbol 符号,基本等同于字符串类型,一般用于采用特殊符号类型的语言
Date 日期时间,用 UNIX 时间格式存储当前日期或时间,可指定年月日(ISODate)
Object ID 对象 ID,用于创建文档的 ID
Binary Data 二进制数据,用于存储二进制数据
Code 代码类型,用于在文档中存储 JavaScript 代码
Regular expression 正则表达式类型,用于存储正则表达式

2.5 安装部署

MongoDB 提供了 Windows 和 Linux 平台上 32 位和 64 位系统的预编译二进制包,可从 MongoDB 官网下载:http://www.mongodb.org/downloads。注意:MongoDB 2.2 版本后不再支持 Windows XP 系统。

参考教程:

2.5.1 方式一:配置官方 yum 源安装(CentOS 6 / 7 均适用,推荐)

创建 yum 仓库文件 /etc/yum.repos.d/mongodb-org.repo:

cat > /etc/yum.repos.d/mongodb-org.repo <<'EOF'
[mongodb-org-4.4]
name=MongoDB Repository
# CentOS 7 使用 $releasever=7;CentOS 6 请把 7Server 改为 6Server
baseurl=https://repo.mongodb.org/yum/redhat/7Server/mongodb-org/4.4/x86_64/
gpgcheck=1
enabled=1
gpgkey=https://www.mongodb.org/static/pgp/server-4.4.asc
EOF

安装与启动:

# 安装(会自动装 mongodb-org-server / -shell / -mongos / -tools)
yum install -y mongodb-org

# CentOS 7:systemd 管理
systemctl start mongod
systemctl enable mongod
systemctl status mongod

# CentOS 6:SysV init 管理
service mongod start
chkconfig mongod on
service mongod status

yum 方式安装后的默认路径:

项目 路径
配置文件 /etc/mongod.conf
数据目录 /var/lib/mongo
日志文件 /var/log/mongodb/mongod.log
运行用户 mongod
服务端可执行文件 /usr/bin/mongod

2.5.2 方式二:二进制包解压安装(免编译,可控性强)

(1)下载并解压

# 以 CentOS 7 + MongoDB 3.2.9 为例(副本集实战使用的版本)
wget https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-rhel70-3.2.9.tgz
tar -zxvf mongodb-linux-x86_64-rhel70-3.2.9.tgz -C /opt/
mv /opt/mongodb-linux-x86_64-rhel70-3.2.9 /opt/truplatform/mongodb

Ubuntu 环境下对应包名为 mongodb-linux-x86_64-ubuntu1604-3.2.7.tgz,解压到 /usr/local/mongodb 即可。

(2)目录与数据目录规划

# 规划目录
mkdir -p /opt/truplatform/mongodb/{data/db,logs,etc}

# 创建专用运行用户(不要用 root 运行 mongod)
groupadd mongod
useradd -g mongod -s /sbin/nologin mongod
chown -R mongod:mongod /opt/truplatform/mongodb

规划结果示例(本文副本集实战沿用此布局):

MONGODB_HOME : /opt/truplatform/mongodb
MONGODB_DATA : /opt/truplatform/mongodb/data/db
MONGODB_LOG  : /opt/truplatform/mongodb/logs
MONGODB_CONF : /opt/truplatform/mongodb/etc

若使用厂商提供的安装脚本,执行效果如下:

cd mongodb-3.2.9-x64
./mongo_install.sh
[root@server-node1 mongodb-3.2.9-x64]# ./mongo_install.sh
starting install mongodb.......
mongodb install success
MONGODB_HOME:/opt/truplatform/mongodb
MONGODB_DATA:/opt/truplatform/mongodb/data/db
MONGODB_LOG:/opt/truplatform/mongodb/logs
MONGODB_CONF:/opt/truplatform/mongodb/etc

(3)配置环境变量

cat > /etc/profile.d/mongodb.sh <<'EOF'
export MONGODB_HOME=/opt/truplatform/mongodb
export PATH=$MONGODB_HOME/bin:$PATH
EOF

# 让环境变量生效
source /etc/profile.d/mongodb.sh

(4)最简启动方式(实验环境,命令行指定数据目录)

mkdir /root/tmp
cd /usr/local/mongodb/bin
mongod --dbpath /root/tmp

(5)验证安装

mongo
[root@server-node1 mongodb-3.2.9-x64]# mongo
MongoDB shell version: 3.2.9
connecting to: test
> show dbs;
local 0.000GB

2.6 mongod 配置文件详解(YAML 格式)

MongoDB 2.6 起推荐使用 YAML 格式配置文件(默认 /etc/mongod.conf)。完整示例与逐项说明:

# ===== 日志 =====
systemLog:
  destination: file                                  # file 或 syslog
  path: /opt/truplatform/mongodb/logs/mongod.log     # 日志文件路径
  logAppend: true                                    # 以追加方式写日志,重启不清空
  logRotate: rename                                  # 日志轮转方式:rename / reopen
  verbosity: 0                                       # 日志级别 0~5,越大越详细
  quiet: false                                       # true 时减少日志输出

# ===== 存储 =====
storage:
  dbPath: /opt/truplatform/mongodb/data/db           # 数据文件目录
  directoryPerDB: true                               # 每个库单独目录,便于按库挂盘
  journal:
    enabled: true                                    # 开启预写日志(WAL),保证崩溃可恢复
    commitIntervalMs: 100                            # journal 刷盘间隔
  engine: wiredTiger                                 # 存储引擎,3.2+ 默认 wiredTiger
  wiredTiger:
    engineConfig:
      cacheSizeGB: 4                                 # 缓存大小,建议 (物理内存-1G)*0.5
      journalCompressor: snappy                      # journal 压缩算法
      directoryForIndexes: false                     # 索引是否单独存目录
    collectionConfig:
      blockCompressor: snappy                        # 集合数据压缩:none/snappy/zlib/zstd
    indexConfig:
      prefixCompression: true                        # 索引前缀压缩,节省内存

# ===== 网络 =====
net:
  port: 27017                                        # 监听端口,默认 27017
  bindIp: 127.0.0.1,192.168.1.220                    # 监听地址,逗号分隔;0.0.0.0 表示全部(危险)
  maxIncomingConnections: 20000                      # 最大并发连接数
  wireObjectCheck: true                              # 校验客户端写入的 BSON 合法性
  # ssl:
  #   mode: requireSSL
  #   PEMKeyFile: /opt/truplatform/mongodb/etc/mongodb.pem

# ===== 进程管理 =====
processManagement:
  fork: true                                         # 以守护进程(后台)方式运行
  pidFilePath: /opt/truplatform/mongodb/mongod.pid   # pid 文件路径
  timeZoneInfo: /usr/share/zoneinfo

# ===== 安全 =====
security:
  authorization: enabled                             # 开启访问控制(用户名密码认证)
  keyFile: /opt/truplatform/mongodb/mongodb.key      # 副本集成员间内部认证密钥文件
  # clusterAuthMode: keyFile                         # keyFile / x509 / sendKeyFile / sendX509

# ===== 副本集 =====
replication:
  replSetName: myreplset                             # 副本集名称,同一副本集所有成员必须一致
  oplogSizeMB: 100                                   # oplog 大小(MB)
  enableMajorityReadConcern: true

# ===== 分片(仅分片集群节点需要) =====
# sharding:
#   clusterRole: shardsvr                            # shardsvr(分片节点)/ configsvr(配置节点)

# ===== 慢查询分析 =====
operationProfiling:
  mode: slowOp                                       # off / slowOp / all
  slowOpThresholdMs: 100                             # 慢操作阈值(毫秒)

# ===== 参数覆盖 =====
setParameter:
  enableLocalhostAuthBypass: false

2.6.1 旧式(key = value)配置文件

MongoDB 2.4 及更早版本、以及很多存量环境使用键值式配置。本文副本集实战即采用此格式:

fork = true
port = 27017
bind_ip = 192.168.1.220
quiet = true
dbpath = /opt/truplatform/mongodb/data/db
logpath = /opt/truplatform/mongodb/logs/mongod.log
logappend = true
journal = true
auth = false
replSet = myreplset
oplogSize = 100
noprealloc = true
#keyFile = /opt/truplatform/mongodb/mongodb.key

参数说明:

参数 说明 YAML 对应项
dbpath 存放数据目录 storage.dbPath
logpath 日志数据目录 systemLog.path
pidfilepath pid 文件路径 processManagement.pidFilePath
keyFile 节点之间用于验证的文件,内容必须保持一致,权限 600,仅 Replica Set 模式有效 security.keyFile
logappend 日志以追加方式存放 systemLog.logAppend
replSet 副本集的名字 replication.replSetName
bind_ip MongoDB 绑定的 IP 地址 net.bindIp
port 端口 net.port
auth 是否开启验证 security.authorization
oplogSize 设置 oplog 的大小(MB) replication.oplogSizeMB
fork 以守护进程方式运行,创建进程 processManagement.fork
quiet 减少日志输出 systemLog.quiet
noprealloc 关闭数据文件预分配(MMAPv1 时代参数) ——

注意:keyFile 和 auth 选项需要在集群配置好、并且添加了验证用户之后再启用。

2.7 启动、关闭与 systemd 管理

(1)直接用 mongod 启动/关闭

# 指定配置文件启动
mongod -f /opt/truplatform/mongodb/etc/mongod.conf

# 命令行参数方式启动
mongod --dbpath /opt/truplatform/mongodb/data/db \
       --logpath /opt/truplatform/mongodb/logs/mongod.log \
       --logappend --port 27017 --bind_ip 0.0.0.0 --fork

# 优雅关闭(推荐):shutdown 命令
mongo --eval "db.getSiblingDB('admin').shutdownServer()"

# 或用 mongod 自带的 shutdown
mongod -f /opt/truplatform/mongodb/etc/mongod.conf --shutdown

# 切勿使用 kill -9,可能导致数据文件损坏;必要时用 kill -2 / kill -15
kill -15 $(cat /opt/truplatform/mongodb/mongod.pid)

(2)CentOS 7:systemd 管理

cat > /etc/systemd/system/mongod.service <<'EOF'
[Unit]
Description=MongoDB Database Server
After=network.target

[Service]
User=mongod
Group=mongod
Type=forking
ExecStart=/opt/truplatform/mongodb/bin/mongod -f /opt/truplatform/mongodb/etc/mongod.conf
ExecStop=/opt/truplatform/mongodb/bin/mongod -f /opt/truplatform/mongodb/etc/mongod.conf --shutdown
PIDFile=/opt/truplatform/mongodb/mongod.pid
LimitNOFILE=64000
LimitNPROC=64000
TimeoutStartSec=180

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl start mongod
systemctl enable mongod        # 开机自启
systemctl restart mongod       # 重启生效

(3)CentOS 6:SysV init 管理

# 拷贝启动脚本后注册服务
cp /etc/init.d/mongod /etc/init.d/mongod-arbiter
chkconfig --add mongod
chkconfig mongod on            # 开机自启
service mongod start
service mongod restart         # 重启生效
service mongod stop

CentOS 6 与 7 的服务管理差异

操作 CentOS 6(SysV / upstart) CentOS 7(systemd)
启动 service mongod start systemctl start mongod
停止 service mongod stop systemctl stop mongod
重启 service mongod restart systemctl restart mongod
状态 service mongod status systemctl status mongod
开机自启 chkconfig mongod on systemctl enable mongod
服务定义 /etc/init.d/mongod /usr/lib/systemd/system/mongod.service
注册服务 chkconfig --add mongod systemctl daemon-reload
资源限制 /etc/security/limits.conf .service 文件中的 LimitNOFILE 等

2.8 防火墙与 SELinux

# ---- CentOS 7:firewalld ----
firewall-cmd --permanent --add-port=27017/tcp
firewall-cmd --permanent --add-port=27018/tcp    # Arbiter 或第二实例
firewall-cmd --permanent --add-port=27019/tcp    # config server
firewall-cmd --reload
firewall-cmd --list-ports

# ---- CentOS 6:iptables ----
iptables -I INPUT -p tcp --dport 27017 -j ACCEPT
service iptables save
service iptables restart

# ---- SELinux(两个版本通用)----
getenforce
# 临时关闭
setenforce 0
# 永久关闭:修改配置后重启
sed -i 's/^SELINUX=.*/SELINUX=disabled/' /etc/selinux/config

# 若必须保留 SELinux,则为自定义数据目录打标签并放通端口
yum install -y policycoreutils-python
semanage fcontext -a -t mongod_var_lib_t '/opt/truplatform/mongodb/data/db.*'
restorecon -Rv /opt/truplatform/mongodb/data/db
semanage port -a -t mongod_port_t -p tcp 27017

2.9 系统调优:THP、NUMA 与 ulimit

MongoDB 启动时若检测到以下问题会打印 startup warnings,生产环境必须处理。

(1)关闭透明大页 THP(Transparent Huge Pages)

# 临时关闭
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag

# 永久关闭:写入 /etc/rc.local(记得 chmod +x /etc/rc.d/rc.local)
cat >> /etc/rc.local <<'EOF'
if test -f /sys/kernel/mm/transparent_hugepage/enabled; then
   echo never > /sys/kernel/mm/transparent_hugepage/enabled
fi
if test -f /sys/kernel/mm/transparent_hugepage/defrag; then
   echo never > /sys/kernel/mm/transparent_hugepage/defrag
fi
EOF

# 或在 GRUB 内核参数中追加:transparent_hugepage=never

(2)关闭 NUMA

# 查看 NUMA 状态
numactl --hardware

# 方式一:启动时用 numactl 交错分配内存(推荐)
numactl --interleave=all /opt/truplatform/mongodb/bin/mongod -f /etc/mongod.conf

# 方式二:关闭 zone_reclaim_mode
echo 0 > /proc/sys/vm/zone_reclaim_mode
echo "vm.zone_reclaim_mode = 0" >> /etc/sysctl.conf
sysctl -p

# 方式三:BIOS 中关闭 NUMA,或 GRUB 内核参数追加 numa=off

(3)调整 ulimit 与内核参数

cat >> /etc/security/limits.conf <<'EOF'
mongod  soft  nofile  64000
mongod  hard  nofile  64000
mongod  soft  nproc   64000
mongod  hard  nproc   64000
mongod  soft  memlock unlimited
mongod  hard  memlock unlimited
EOF

# CentOS 6 还需修改 /etc/security/limits.d/90-nproc.conf
sed -i 's/^\*.*soft.*nproc.*/*  soft  nproc  64000/' /etc/security/limits.d/90-nproc.conf

# 网络与虚拟内存参数
cat >> /etc/sysctl.conf <<'EOF'
net.core.somaxconn = 4096
net.ipv4.tcp_max_syn_backlog = 4096
net.ipv4.tcp_keepalive_time = 120
vm.swappiness = 1
vm.dirty_ratio = 15
vm.dirty_background_ratio = 5
EOF
sysctl -p

(4)其他建议

  • 数据盘使用 XFS 文件系统(WiredTiger 官方推荐),挂载参数加 noatime。
  • 磁盘 IO 调度器:SSD 用 noop/none,机械盘用 deadline。
  • 不要以 root 运行 mongod(否则启动会有 WARNING: You are running this process as the root user, which is not recommended.)。

2.10 客户端连接

(1)本地连接

# 3.x/4.0 使用 mongo;4.4+ 推荐 mongosh
mongo
mongo --port 27017
mongosh

(2)远程连接

mongo --host 192.168.1.220 --port 27017
mongo --host 192.168.1.220 --port 27017 -u admin -p JWIServer27017 --authenticationDatabase admin

(3)连接字符串 URI 格式

标准格式:

mongodb://[username:password@]host1[:port1][,host2[:port2],...][/[database][?options]]

各类示例:

# 连接本机默认端口
mongodb://localhost

# 使用用户名和密码连接登录到指定数据库
mongodb://admin:123456@localhost/test

# 指定端口
mongodb://192.168.1.220:27017/mydb

# 连接副本集(列出多个 seed 节点 + replicaSet 参数)
mongodb://192.168.1.220:27017,192.168.1.221:27017,192.168.1.222:27017/mydb?replicaSet=myreplset

# 副本集 + 认证 + 读偏好 + 写关注
mongodb://admin:JWIServer27017@192.168.1.220:27017,192.168.1.221:27017/mydb?replicaSet=myreplset&authSource=admin&readPreference=secondaryPreferred&w=majority&wtimeoutMS=5000

# DNS Seed List 形式(3.6+)
mongodb+srv://user:pwd@cluster0.example.com/mydb

常用 options:

选项 说明
replicaSet 副本集名称,驱动会自动发现全部成员并跟踪主节点切换
authSource 认证所用的数据库,通常为 admin
readPreference 读偏好,见副本集章节
w / journal / wtimeoutMS 写关注,见副本集章节
maxPoolSize / minPoolSize 连接池大小
connectTimeoutMS / socketTimeoutMS 连接与套接字超时
ssl=true 启用 TLS/SSL
retryWrites=true 可重试写(4.2+ 默认开启)

(4)常用 shell 辅助命令

show dbs                 // 列出所有数据库(旧写法 show databases)
use 学生系统              // 切换/隐式创建数据库
db                       // 显示当前所在数据库
show collections         // 列出当前库的所有集合(旧写法 show tables)
show users               // 列出当前库的用户
show roles               // 列出当前库的角色
db.help()                // 数据库级帮助
db.collection.help()     // 集合级帮助
rs.help()                // 副本集命令帮助
sh.help()                // 分片命令帮助
db.version()             // 服务端版本
db.getMongo()            // 当前连接信息
load("/tmp/init.js")     // 执行外部 JS 脚本
exit                     // 退出(或 quit()、Ctrl+C)

连接成功后的典型输出:

MongoDB shell version: 3.2.7
connecting to: test
Welcome to the MongoDB shell.
For interactive help, type "help".
For more comprehensive documentation, see
    http://docs.mongodb.org/
Questions? Try the support group
    http://groups.google.com/group/mongodb-user
Server has startup warnings:
2019-01-05T01:23:13.921-0800 I CONTROL  [initandlisten] ** WARNING: You are running this process as the root user, which is not recommended.
2019-01-05T01:23:13.921-0800 I CONTROL  [initandlisten]

客户端默认连接的数据库是 test。启动警告提示当前以 root 用户运行,不推荐这样做。


三、MongoDB 增删改查与聚合

本章的实验数据沿用课程实验的"学生系统"三张表:

学生表 Student:

Sno Sname Ssex Sage Sdept
95001 李勇 男 22 CS
95002 刘晨 女 19 IS
95003 王敏 女 18 MA
95004 张立 男 19 IS

课程表 Course:

Cno Cname Ccredit
1 数据库 4
2 数学 2
3 信息系统 4
4 操作系统 3
5 数据结构 4
6 数据处理 2
7 PASCAL 4

成绩表 SC:

Sno Cno Grade
95001 1 92
95001 2 85
95001 3 88
95002 2 90
95002 3 80

3.1 数据库与集合管理

(1)创建/切换数据库

MongoDB 创建数据库的语法格式如下:

use DATABASE_NAME

如果数据库不存在则创建,否则切换到指定数据库。注意:这是隐式创建,只有真正写入数据后数据库才会持久化,show dbs 才能看到。

> use 学生系统
switched to db 学生系统
> db
学生系统
> show dbs                 // 此时还看不到"学生系统",因为里面没有数据
local  0.000GB

(2)创建集合

MongoDB 没有单独创建集合名的 shell 命令:在插入数据的时候 MongoDB 会自动创建对应的集合。同时 MongoDB 也不需要预先设计表结构,避免了关系型数据库中设计表时的繁琐。

如需显式创建并指定选项,使用 db.createCollection():

// 语法:db.createCollection(name, options)
db.createCollection("Student")

// 创建固定集合(capped collection):空间用满后覆盖最老的文档,适合日志类数据
db.createCollection("mylog", {
  capped: true,          // 是否为固定集合
  size: 6142800,         // 固定集合的总字节数(capped 必填)
  max: 10000             // 最多允许的文档数量
})

// 3.2+ 可指定校验规则
db.createCollection("users", {
  validator: { $jsonSchema: {
     bsonType: "object",
     required: ["name", "age"],
     properties: {
        name: { bsonType: "string" },
        age:  { bsonType: "int", minimum: 0 }
     }
  }},
  validationLevel: "strict",     // strict / moderate
  validationAction: "error"      // error / warn
})

// 查看集合是否为固定集合
db.mylog.isCapped()
// 将已有普通集合转为固定集合
db.runCommand({ convertToCapped: "mylog", size: 100000 })

(3)删除数据库与集合

// 删除当前所在数据库
> use 学生系统
> db.dropDatabase()
{ "dropped" : "学生系统", "ok" : 1 }

// 删除集合(连同其上索引一并删除)
> db.Student.drop()
true

// 重命名集合
db.Student.renameCollection("Student_bak")

(4)查看统计信息

db.stats()                    // 数据库级统计:集合数、文档数、数据大小、索引大小
db.stats(1024*1024)           // 以 MB 为单位显示
db.Student.stats()            // 集合级统计
db.Student.count()            // 文档数
db.Student.totalSize()        // 集合数据 + 索引总大小
db.Student.dataSize()         // 仅数据大小
db.Student.totalIndexSize()   // 仅索引大小
db.getCollectionNames()       // 集合名数组
db.serverStatus()             // 服务端运行状态

db.stats() 关键字段:

字段 含义
collections 集合数量
objects 文档总数
avgObjSize 平均文档大小(字节)
dataSize 数据实际占用大小
storageSize 磁盘分配的存储大小(含预留空洞)
indexes / indexSize 索引数量 / 索引总大小

3.2 插入文档

文档的数据结构和 JSON 基本一样,所有存储在集合中的数据都是 BSON 格式。

(1)基本插入方法

// 通用(3.x 及以前的主力写法,可传单个文档或数组)
db.COLLECTION_NAME.insert(document)

// 3.2+ 新增的明确语义方法
db.COLLECTION_NAME.insertOne(document)     // 插入单条
db.COLLECTION_NAME.insertMany([d1, d2])    // 插入多条

// save():_id 已存在则替换整个文档,不存在则插入(4.2 起废弃)
db.COLLECTION_NAME.save(document)

(2)单条插入示例

> db.col.insert({
    title: 'MongoDB 教程',
    description: 'MongoDB 是一个 Nosql 数据库',
    by: '厦门大学数据库实验室',
    url: 'http://dblab.xmu.edu.cn',
    tags: ['mongodb', 'database', 'NoSQL'],
    likes: 100
})
WriteResult({ "nInserted" : 1 })

(3)批量插入示例(学生系统实验)

先定义一个包含 4 名学生信息的数组,再调用 insert 插入到 Student 集合:

var data = [
  {Sno:95001, Sname:"李勇", Ssex:"男", Sage:20, Sdept:"CS"},
  {Sno:95002, Sname:"刘晨", Ssex:"女", Sage:19, Sdept:"IS"},
  {Sno:95003, Sname:"王敏", Ssex:"女", Sage:18, Sdept:"MA"},
  {Sno:95004, Sname:"张立", Ssex:"男", Sage:19, Sdept:"IS"}
];
db.Student.insert(data);

插入课程与成绩:

db.Course.insertMany([
  {Cno:1, Cname:"数据库",   Ccredit:4},
  {Cno:2, Cname:"数学",     Ccredit:2},
  {Cno:3, Cname:"信息系统", Ccredit:4},
  {Cno:4, Cname:"操作系统", Ccredit:3},
  {Cno:5, Cname:"数据结构", Ccredit:4},
  {Cno:6, Cname:"数据处理", Ccredit:2},
  {Cno:7, Cname:"PASCAL",   Ccredit:4}
]);

db.SC.insertMany([
  {Sno:95001, Cno:1, Grade:92},
  {Sno:95001, Cno:2, Grade:85},
  {Sno:95001, Cno:3, Grade:88},
  {Sno:95002, Cno:2, Grade:90},
  {Sno:95002, Cno:3, Grade:80}
]);

(4)_id 的自动生成与自定义

// 不指定 _id:自动生成 ObjectId
db.Student.insertOne({Sno:95005, Sname:"赵六"})

// 自定义 _id(必须唯一,重复会报 E11000 duplicate key error)
db.Student.insertOne({_id: 95006, Sname:"孙七"})

// 用业务主键作 _id,可省一个唯一索引
db.SC.insertOne({_id: {Sno:95003, Cno:1}, Grade: 76})

(5)有序插入与无序插入

// ordered: true(默认)—— 顺序插入,遇到第一个错误立即停止,后续文档不再插入
db.Student.insertMany([{_id:1},{_id:1},{_id:2}], {ordered: true})
// 结果:_id=1 插入成功,第二条重复报错并中断,_id=2 未插入

// ordered: false —— 并行插入,遇错跳过继续,最后汇总所有错误,吞吐更高
db.Student.insertMany([{_id:1},{_id:1},{_id:2}], {ordered: false})
// 结果:_id=1 和 _id=2 均插入成功,仅重复的那条报错

3.3 查询文档

(1)find 与 findOne

db.Student.find();                  // 查询所有数据
db.Student.find({Sno:95002});       // 按条件查询(学号为 95002 的学生)
db.Student.find().pretty();         // 格式化输出,便于阅读
db.Student.findOne({Sno:95002});    // 只返回第一条匹配文档(不是游标,是文档对象)
db.Student.find({}, {Sname:1});     // 第二个参数是投影

(2)查询条件操作符全表

操作符 含义 示例
$eq 等于 db.Student.find({Sage: {$eq: 19}}),等价于 {Sage: 19}
$ne 不等于 db.Student.find({Sdept: {$ne: "IS"}})
$gt 大于 db.SC.find({Grade: {$gt: 85}})
$gte 大于等于 db.SC.find({Grade: {$gte: 85}})
$lt 小于 db.Student.find({Sage: {$lt: 20}})
$lte 小于等于 db.Student.find({Sage: {$lte: 19}})
$in 在集合内(相当于 SQL IN) db.Student.find({Sdept: {$in: ["CS","IS"]}})
$nin 不在集合内 db.Student.find({Sdept: {$nin: ["MA"]}})
$and 逻辑与(默认多条件即为 AND) db.Student.find({$and: [{Ssex:"女"}, {Sage:{$lt:19}}]})
$or 逻辑或 db.Student.find({$or: [{Sdept:"CS"}, {Sage:18}]})
$not 逻辑非 db.Student.find({Sage: {$not: {$gt: 19}}})
$nor 都不满足 db.Student.find({$nor: [{Sdept:"CS"}, {Sage:18}]})
$exists 字段是否存在 db.Student.find({Sclass: {$exists: true}})
$type 字段的 BSON 类型 db.Student.find({Sno: {$type: "int"}}) 或 {$type: 16}
$regex 正则匹配 db.Course.find({Cname: {$regex: "^数据"}}),等价于 {Cname: /^数据/}
$mod 取模 db.SC.find({Grade: {$mod: [10, 0]}})
$all 数组包含全部指定元素 db.col.find({tags: {$all: ["mongodb","NoSQL"]}})
$size 数组长度等于 db.col.find({tags: {$size: 3}})
$elemMatch 数组中至少一个元素同时满足多个条件 见下文
$expr 使用聚合表达式做条件(3.6+) db.SC.find({$expr: {$gt: ["$Grade", "$pass"]}})
$text 全文检索(需文本索引) db.col.find({$text: {$search: "mongodb"}})
$where 用 JS 表达式过滤(性能差,慎用) db.Student.find({$where: "this.Sage > 19"})

组合示例:

// AND:IS 系且年龄小于 20
db.Student.find({Sdept: "IS", Sage: {$lt: 20}})

// OR
db.Student.find({$or: [{Sdept: "CS"}, {Sdept: "MA"}]})

// AND 与 OR 混合:女生 且 (CS 系 或 年龄=18)
db.Student.find({
  Ssex: "女",
  $or: [{Sdept: "CS"}, {Sage: 18}]
})

// 区间:85 <= Grade <= 90
db.SC.find({Grade: {$gte: 85, $lte: 90}})

// 正则忽略大小写
db.col.find({title: {$regex: "mongodb", $options: "i"}})

(3)嵌套文档与数组查询

// 准备嵌套数据
db.blog.insertOne({
  author: "Jane",
  post: { title: "MyFirstPost", views: 100 },
  comments: [
    { by: "Ada", score: 8,  text: "Good post" },
    { by: "Bob", score: 3,  text: "So so" }
  ],
  tags: ["mongodb", "database", "NoSQL"]
})

// 用点号访问嵌套字段
db.blog.find({"post.title": "MyFirstPost"})
db.blog.find({"post.views": {$gt: 50}})

// 精确匹配整个子文档:字段顺序和数量必须完全一致
db.blog.find({post: {title: "MyFirstPost", views: 100}})

// 数组:只要数组包含该元素即匹配
db.blog.find({tags: "mongodb"})

// 数组精确匹配:顺序与内容都要一致
db.blog.find({tags: ["mongodb", "database", "NoSQL"]})

// 按数组下标查询(下标从 0 开始)
db.blog.find({"tags.0": "mongodb"})

// 数组对象的字段查询(条件可能由不同元素分别满足)
db.blog.find({"comments.by": "Ada"})
db.blog.find({"comments.score": {$gt: 5}, "comments.by": "Bob"})   // 会匹配上!

// $elemMatch:要求同一个数组元素同时满足所有条件
db.blog.find({comments: {$elemMatch: {by: "Bob", score: {$gt: 5}}}})   // 不匹配,符合预期
db.blog.find({comments: {$elemMatch: {by: "Ada", score: {$gt: 5}}}})   // 匹配

(4)投影:控制返回字段

// 1 表示包含,0 表示排除(除 _id 外,二者不能混用)
db.Student.find({}, {Sname: 1, Sage: 1})            // 只返回 Sname、Sage 和 _id
db.Student.find({}, {Sname: 1, Sage: 1, _id: 0})    // 不返回 _id
db.Student.find({}, {Ssex: 0, Sdept: 0})            // 排除这两个字段,其余全返回

// 数组投影操作符
db.blog.find({}, {comments: {$slice: 2}})           // 只返回数组前 2 个元素
db.blog.find({}, {comments: {$slice: -1}})          // 只返回最后 1 个元素
db.blog.find({}, {comments: {$slice: [1, 2]}})      // 跳过 1 个,取 2 个
db.blog.find({"comments.by":"Ada"}, {"comments.$":1})   // 只返回第一个匹配的数组元素
db.blog.find({}, {comments: {$elemMatch: {by: "Ada"}}}) // 只返回满足条件的元素

(5)排序、限制与分页

// sort:1 升序,-1 降序
db.Student.find().sort({Sage: 1})
db.Student.find().sort({Sdept: 1, Sage: -1})     // 多字段排序

// limit:限制返回条数
db.Student.find().limit(2)

// skip:跳过前 n 条
db.Student.find().skip(2)

// 分页:每页 2 条,取第 2 页(先 sort,再 skip,再 limit)
db.Student.find().sort({Sno: 1}).skip(2).limit(2)

// 通用分页公式:skip((page-1)*pageSize).limit(pageSize)

执行顺序:MongoDB 内部始终按 sort → skip → limit 执行,与链式调用书写顺序无关。

(6)计数、去重与游标

// 计数
db.Student.count()                              // 全部文档数(旧接口,分片下可能不精确)
db.Student.count({Sdept: "IS"})
db.Student.countDocuments({Sdept: "IS"})        // 4.0+ 推荐,走真实聚合,结果精确
db.Student.estimatedDocumentCount()             // 4.0+,读元数据,极快但为估算值
db.Student.find({Sdept:"IS"}).count()

// 去重(返回数组)
db.Student.distinct("Sdept")
// [ "CS", "IS", "MA" ]
db.Student.distinct("Sage", {Sdept: "IS"})      // 带过滤条件去重

// 游标(cursor):find() 返回的是游标,不是结果集
var cur = db.Student.find();
while (cur.hasNext()) {
   printjson(cur.next());
}

db.Student.find().forEach(function(doc){ print(doc.Sname); })
db.Student.find().toArray()          // 一次性转成数组(数据量大时慎用)
db.Student.find().itcount()          // 游标实际返回的文档数
db.Student.find().size()             // 应用 skip/limit 后的数量
db.Student.find().noCursorTimeout()  // 禁止游标 10 分钟超时
db.Student.find().batchSize(100)     // 每批返回 100 条
db.Student.find().hint({Sno: 1})     // 强制使用指定索引
db.Student.find().max(...).min(...)  // 限定索引扫描边界

shell 中直接执行 find() 默认只显示前 20 条,输入 it 可继续迭代下一批。

3.4 更新文档

(1)更新方法

// 通用(3.x 主力写法)
db.collection.update(
   <query>,                 // 查询条件
   <update>,                // 更新内容
   {
     upsert:  <boolean>,    // 不存在匹配文档时是否插入新文档,默认 false
     multi:   <boolean>,    // 是否更新全部匹配文档,默认 false(只更新第一条)
     writeConcern: <doc>
   }
)

// 3.2+ 明确语义方法
db.collection.updateOne(<filter>, <update>, <options>)    // 更新第一条
db.collection.updateMany(<filter>, <update>, <options>)   // 更新全部匹配
db.collection.replaceOne(<filter>, <replacement>)         // 整文档替换(不能用 $ 操作符)

// 找到并更新,返回文档
db.collection.findOneAndUpdate(<filter>, <update>, {returnNewDocument: true})
db.collection.findOneAndReplace(<filter>, <replacement>)
db.collection.findAndModify({query:..., update:..., new:true, upsert:false})

使用 update 配合 $set,将学号 95001 的学生年龄修改为 22 岁:

db.Student.update({Sno:95001}, {$set:{Sage:22}});
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })

// 更新多条:把 IS 系全部学生年龄 +1
db.Student.update({Sdept:"IS"}, {$inc:{Sage:1}}, {multi:true});
db.Student.updateMany({Sdept:"IS"}, {$inc:{Sage:1}});

重要陷阱:update 的第二个参数如果不带 $ 操作符,会把整个文档替换掉(未列出的字段全部丢失)。要修改部分字段必须使用 $set。

(2)字段更新操作符全表

操作符 作用 示例
$set 设置字段值(不存在则新增) {$set: {Sage: 22, Sclass: "CS01"}}
$unset 删除字段(值任意,一般写 "") {$unset: {Sclass: ""}}
$inc 数值自增/自减(负数为减) {$inc: {Sage: 1, Grade: -5}}
$mul 数值相乘 {$mul: {Grade: 1.1}}
$rename 字段重命名 {$rename: {"Sdept": "Department"}}
$min 仅当新值更小时才更新 {$min: {Grade: 60}}
$max 仅当新值更大时才更新 {$max: {Grade: 100}}
$currentDate 设为当前日期/时间戳 {$currentDate: {updatedAt: true}} 或 {updatedAt: {$type:"timestamp"}}
$setOnInsert 仅在 upsert 触发插入时才设置 {$setOnInsert: {createdAt: new Date()}}
// 综合示例
db.Student.updateOne(
  {Sno: 95001},
  {
    $set:  {Sname: "李勇", Sclass: "CS01"},
    $inc:  {Sage: 1},
    $unset:{tmpField: ""},
    $currentDate: {updatedAt: true},
    $max:  {maxScore: 95}
  }
)

(3)数组更新操作符全表

操作符 作用
$push 向数组末尾追加一个元素(字段不存在则创建数组)
$push + $each 一次追加多个元素
$push + $each + $slice 追加后裁剪数组长度(正数保留前 n 个,负数保留后 n 个)
$push + $each + $sort 追加后对数组排序
$push + $position 在指定下标位置插入
$addToSet 追加元素,但已存在则不添加(集合语义,去重)
$pop 删除数组元素:1 删末尾,-1 删开头
$pull 删除数组中所有匹配条件的元素
$pullAll 删除数组中所有出现在指定列表里的元素
$ 位置操作符,指向 query 匹配到的第一个数组元素
$[] 匹配数组的所有元素(3.6+)
$[<identifier>] 配合 arrayFilters 匹配满足条件的元素(3.6+)
// $push:追加单个
db.blog.updateOne({author:"Jane"}, {$push: {tags: "bigdata"}})

// $each:追加多个
db.blog.updateOne({author:"Jane"}, {$push: {tags: {$each: ["nosql","cluster"]}}})

// $each + $slice:追加后只保留最后 5 个(实现"最近 N 条"列表)
db.blog.updateOne({author:"Jane"},
  {$push: {tags: {$each: ["a","b"], $slice: -5}}})

// $each + $sort:追加评论后按分数降序,只留前 3 条
db.blog.updateOne({author:"Jane"},
  {$push: {comments: {
      $each: [{by:"Cid", score:9}],
      $sort: {score: -1},
      $slice: 3
  }}})

// $each + $position:在下标 0 处插入
db.blog.updateOne({author:"Jane"},
  {$push: {tags: {$each: ["first"], $position: 0}}})

// $addToSet:去重追加
db.blog.updateOne({author:"Jane"}, {$addToSet: {tags: "mongodb"}})        // 已存在,无变化
db.blog.updateOne({author:"Jane"}, {$addToSet: {tags: {$each:["x","y"]}}})

// $pop:删末尾 / 删开头
db.blog.updateOne({author:"Jane"}, {$pop: {tags: 1}})
db.blog.updateOne({author:"Jane"}, {$pop: {tags: -1}})

// $pull:按条件删除
db.blog.updateOne({author:"Jane"}, {$pull: {tags: "nosql"}})
db.blog.updateOne({author:"Jane"}, {$pull: {comments: {score: {$lt: 5}}}})

// $pullAll:删除多个指定值
db.blog.updateOne({author:"Jane"}, {$pullAll: {tags: ["a","b","x"]}})

// $ 位置操作符:把 by="Ada" 的那条评论分数改为 10
db.blog.updateOne({"comments.by": "Ada"}, {$set: {"comments.$.score": 10}})

// $[] 更新所有数组元素
db.blog.updateOne({author:"Jane"}, {$inc: {"comments.$[].score": 1}})

// arrayFilters 更新满足条件的元素(3.6+)
db.blog.updateOne(
  {author:"Jane"},
  {$set: {"comments.$[elem].flag": true}},
  {arrayFilters: [{"elem.score": {$lt: 5}}]}
)

(4)upsert 选项

// 存在则更新,不存在则插入
db.Student.update({Sno:95010}, {$set:{Sname:"新同学", Sdept:"CS"}}, {upsert:true})
db.Student.updateOne({Sno:95010}, {$set:{Sname:"新同学"}}, {upsert:true})

// upsert + multi
db.Student.update({Sdept:"EE"}, {$set:{active:true}}, {upsert:true, multi:true})

// 返回结果中 nUpserted=1 且带 _id 表示发生了插入
WriteResult({
  "nMatched" : 0, "nUpserted" : 1, "nModified" : 0,
  "_id" : ObjectId("5bfe642bb96cecc8f6910c99")
})

3.5 删除文档

// 通用写法
db.collection.remove(
   <query>,
   {
     justOne: <boolean>,      // true 只删一条,默认 false 删全部匹配
     writeConcern: <doc>
   }
)

// 删除学号为 95004 的学生文档
db.Student.remove({Sno:95004});
WriteResult({ "nRemoved" : 1 })

// 3.2+ 明确语义方法
db.Student.deleteOne({Sno: 95004})          // 删除第一条匹配
db.Student.deleteMany({Sdept: "IS"})        // 删除全部匹配
db.Student.deleteMany({})                   // 清空集合(保留集合与索引)

// 找到并删除,返回被删文档
db.Student.findOneAndDelete({Sno: 95003})

// remove 只删一条
db.Student.remove({Sdept:"IS"}, {justOne: true})
db.Student.remove({Sdept:"IS"}, 1)          // 简写

// 清空整个集合(比 deleteMany({}) 快得多,但会重建集合)
db.Student.drop()

清空大集合优先用 drop()(元数据操作,瞬间完成),deleteMany({}) 会逐条删除并写满 oplog。

3.6 索引

索引是提升查询性能最重要的手段。MongoDB 索引基于 B-Tree 实现。

(1)创建与查看索引

// 语法:db.collection.createIndex(keys, options)
db.Student.createIndex({Sno: 1})                       // 1 升序,-1 降序
db.Student.createIndex({Sno: 1}, {name: "idx_sno"})    // 自定义索引名
db.Student.createIndex({Sno: 1}, {background: true})   // 后台创建,不阻塞读写(生产必加)

// 复合索引:字段顺序决定可用性(遵循最左前缀原则)
db.Student.createIndex({Sdept: 1, Sage: -1})
// 可支持:{Sdept} 、{Sdept, Sage} 的查询与排序;不能单独支持 {Sage}

// 唯一索引
db.Student.createIndex({Sno: 1}, {unique: true})
// 复合唯一索引:约束组合唯一
db.SC.createIndex({Sno: 1, Cno: 1}, {unique: true})

// 稀疏索引:只索引存在该字段的文档,字段缺失的文档不进索引
db.Student.createIndex({email: 1}, {sparse: true})
// 唯一 + 稀疏:允许多个文档不含该字段,但含该字段的必须唯一
db.Student.createIndex({email: 1}, {unique: true, sparse: true})

// 部分索引(3.2+,比 sparse 更灵活)
db.SC.createIndex({Grade: 1}, {partialFilterExpression: {Grade: {$gte: 60}}})

// TTL 索引:字段为 Date 类型,文档在到期后由后台线程自动删除
db.session.createIndex({createdAt: 1}, {expireAfterSeconds: 3600})   // 1 小时后过期
// expireAfterSeconds: 0 表示以字段中的时间为准点过期
db.session.createIndex({expireAt: 1}, {expireAfterSeconds: 0})

// 文本索引与全文检索(每个集合最多一个文本索引)
db.col.createIndex({title: "text", description: "text"})
db.col.createIndex({"$**": "text"})                    // 所有字段建文本索引
db.col.find({$text: {$search: "mongodb database"}})    // 空格分词,OR 语义
db.col.find({$text: {$search: "\"mongodb database\""}})// 引号内为短语精确匹配
db.col.find({$text: {$search: "mongodb -mysql"}})      // 减号表示排除
// 按相关度评分排序
db.col.find({$text:{$search:"mongodb"}}, {score:{$meta:"textScore"}})
       .sort({score: {$meta: "textScore"}})

// 地理空间索引
db.places.createIndex({loc: "2dsphere"})               // 球面(GeoJSON,经纬度)
db.places.createIndex({xy: "2d"})                      // 平面坐标
db.places.find({loc: {$near: {
   $geometry: {type:"Point", coordinates:[118.09, 24.44]},
   $maxDistance: 5000                                   // 米
}}})
db.places.find({loc: {$geoWithin: {$centerSphere: [[118.09, 24.44], 5/6378.1]}}})

// 哈希索引(用于哈希分片)
db.Student.createIndex({Sno: "hashed"})

// 通配符索引(4.2+)
db.col.createIndex({"userMeta.$**": 1})

// 查看与删除
db.Student.getIndexes()                    // 查看全部索引
db.Student.getIndexKeys()
db.Student.totalIndexSize()                // 索引总大小
db.Student.dropIndex({Sno: 1})             // 按键删除
db.Student.dropIndex("idx_sno")            // 按名称删除
db.Student.dropIndexes()                   // 删除除 _id 外的全部索引
db.Student.reIndex()                       // 重建索引(会锁库,谨慎)

(2)explain 执行计划

db.Student.find({Sno: 95001}).explain()                    // 默认 queryPlanner 模式
db.Student.find({Sno: 95001}).explain("executionStats")    // 含实际执行统计(最常用)
db.Student.find({Sno: 95001}).explain("allPlansExecution") // 含所有候选计划

// 聚合也可 explain
db.SC.explain("executionStats").aggregate([{$match:{Grade:{$gt:85}}}])

关键字段判读:

字段 含义与判断标准
winningPlan.stage IXSCAN = 走了索引扫描(好);COLLSCAN = 全表扫描(坏,需加索引)
IDHACK 按 _id 精确查询的极速路径
FETCH 用索引拿到 _id 后回表取完整文档
PROJECTION_COVERED 覆盖索引查询,无需回表(最优)
SORT 内存排序。若出现 SORT_KEY_GENERATOR + SORT,说明排序未走索引,超过 32MB 会报错
nReturned 实际返回的文档数
totalKeysExamined 扫描的索引条目数
totalDocsExamined 扫描的文档数
executionTimeMillis 执行耗时(毫秒)
rejectedPlans 被优化器淘汰的候选方案

健康的查询:nReturned 与 totalKeysExamined、totalDocsExamined 数量级接近(理想为 1:1:1)。若 totalDocsExamined 远大于 nReturned,说明索引选择性差或未命中索引。

// 反面示例:COLLSCAN
> db.Student.find({Sname: "李勇"}).explain("executionStats").executionStats
{
  "nReturned" : 1,
  "executionTimeMillis" : 45,
  "totalKeysExamined" : 0,
  "totalDocsExamined" : 1000000,      // 扫了一百万条只为返回 1 条
  "executionStages" : { "stage" : "COLLSCAN", ... }
}

// 建索引后:IXSCAN
> db.Student.createIndex({Sname: 1}, {background: true})
> db.Student.find({Sname: "李勇"}).explain("executionStats").executionStats
{
  "nReturned" : 1,
  "executionTimeMillis" : 0,
  "totalKeysExamined" : 1,
  "totalDocsExamined" : 1,
  "executionStages" : { "stage" : "FETCH", "inputStage": {"stage":"IXSCAN", ...} }
}

(3)索引设计建议

  1. ESR 原则:复合索引字段顺序为 Equality(等值条件)→ Sort(排序字段)→ Range(范围条件)。
  2. 最左前缀:复合索引 {a:1, b:1, c:1} 可服务 {a}、{a,b}、{a,b,c},但不能单独服务 {b} 或 {c}。
  3. 覆盖索引:查询条件与投影字段都在索引中时无需回表,性能最佳(记得 _id: 0)。
  4. 高选择性字段优先(区分度高的字段放前面)。
  5. 索引不是越多越好:每个索引都增加写入开销与内存占用,删除无用索引(可用 $indexStats 聚合查看使用次数)。
  6. 索引必须能装进内存(WiredTiger cache),否则性能急剧下降。
  7. 排序字段务必建索引,避免内存排序 32MB 限制。
  8. 生产环境创建索引必须加 background: true(4.2+ 已默认为混合模式,不再阻塞)。
  9. 数组字段索引(多键索引 Multikey)会为每个元素生成一条索引项,注意膨胀;一个复合索引中最多一个数组字段。
  10. 用 $indexStats 审计索引使用情况:db.Student.aggregate([{$indexStats:{}}])。

3.7 聚合(Aggregation Pipeline)

聚合管道把文档依次送入多个"阶段(stage)"处理,前一阶段输出作为后一阶段输入。

db.collection.aggregate([ {stage1}, {stage2}, ... ], {options})

(1)常用聚合阶段

阶段 作用 对应 SQL
$match 过滤文档 WHERE
$project 选择/重命名/计算字段 SELECT
$group 分组聚合 GROUP BY
$sort 排序 ORDER BY
$limit 限制条数 LIMIT
$skip 跳过条数 OFFSET
$unwind 拆解数组,一个元素生成一条文档 UNNEST
$lookup 关联另一个集合 LEFT OUTER JOIN
$addFields / $set 新增字段(保留原有字段) 追加计算列
$unset 移除字段 ——
$count 统计文档数 COUNT(*)
$bucket / $bucketAuto 按区间分桶统计 CASE WHEN + 分组
$facet 一次执行多条子管道(多维统计) 多个子查询
$out 结果写入新集合(必须是最后一个阶段) CREATE TABLE AS
$merge 结果合并到已有集合(4.2+) MERGE INTO
$sample 随机抽样 ORDER BY RAND()
$graphLookup 递归图查询 递归 CTE
$replaceRoot 用子文档替换根文档 ——
$indexStats 索引使用统计 ——

(2)$group 累加器

累加器 作用
$sum 求和($sum: 1 即计数)
$avg 求平均
$max 最大值
$min 最小值
$first 分组内第一条的字段值(需先 $sort)
$last 分组内最后一条的字段值
$push 把值收集成数组(不去重)
$addToSet 把值收集成数组(去重)
$stdDevPop / $stdDevSamp 总体/样本标准差

(3)实战示例(学生系统)

// 1. 按系分组统计人数和平均年龄
db.Student.aggregate([
  {$group: {
      _id: "$Sdept",
      count: {$sum: 1},
      avgAge: {$avg: "$Sage"},
      maxAge: {$max: "$Sage"},
      minAge: {$min: "$Sage"},
      names:  {$push: "$Sname"},
      sexes:  {$addToSet: "$Ssex"}
  }},
  {$sort: {count: -1}}
])

// 2. 每个学生的总分、平均分、选课数(成绩 >= 85 的科目)
db.SC.aggregate([
  {$match: {Grade: {$gte: 85}}},
  {$group: {_id: "$Sno", total: {$sum: "$Grade"}, avg: {$avg: "$Grade"}, cnt: {$sum: 1}}},
  {$sort: {avg: -1}},
  {$limit: 10}
])

// 3. $project:字段筛选与计算
db.Student.aggregate([
  {$project: {
      _id: 0,
      学号: "$Sno",
      姓名: "$Sname",
      出生年: {$subtract: [2026, "$Sage"]},
      是否成年: {$gte: ["$Sage", 18]},
      姓: {$substr: ["$Sname", 0, 1]}
  }}
])

// 4. $addFields:在保留原字段的基础上新增
db.SC.aggregate([
  {$addFields: {等级: {$switch: {
      branches: [
        {case: {$gte:["$Grade",90]}, then:"优"},
        {case: {$gte:["$Grade",80]}, then:"良"},
        {case: {$gte:["$Grade",60]}, then:"及格"}
      ],
      default: "不及格"
  }}}}
])

// 5. $unwind:拆解数组
db.blog.aggregate([
  {$unwind: "$tags"},
  {$group: {_id: "$tags", cnt: {$sum: 1}}},
  {$sort: {cnt: -1}}
])
// 保留空数组文档并输出下标
db.blog.aggregate([
  {$unwind: {path: "$tags", includeArrayIndex: "idx", preserveNullAndEmptyArrays: true}}
])

// 6. $lookup:关联查询(相当于 LEFT JOIN)
// 成绩表关联学生表和课程表
db.SC.aggregate([
  {$lookup: {from: "Student", localField: "Sno", foreignField: "Sno", as: "stu"}},
  {$lookup: {from: "Course",  localField: "Cno", foreignField: "Cno", as: "crs"}},
  {$unwind: "$stu"},
  {$unwind: "$crs"},
  {$project: {_id:0, 姓名:"$stu.Sname", 课程:"$crs.Cname", 学分:"$crs.Ccredit", 成绩:"$Grade"}},
  {$sort: {成绩: -1}}
])

// 7. $bucket:按分数段分桶
db.SC.aggregate([
  {$bucket: {
     groupBy: "$Grade",
     boundaries: [0, 60, 70, 80, 90, 101],
     default: "其他",
     output: {人数: {$sum: 1}, 平均: {$avg: "$Grade"}}
  }}
])
// $bucketAuto:自动均分为 4 桶
db.SC.aggregate([{$bucketAuto: {groupBy: "$Grade", buckets: 4}}])

// 8. $facet:一次返回多个维度的统计(分页 + 总数的经典用法)
db.SC.aggregate([
  {$facet: {
     "按分段": [{$bucket: {groupBy:"$Grade", boundaries:[0,60,80,101]}}],
     "总条数": [{$count: "total"}],
     "前三名": [{$sort: {Grade:-1}}, {$limit: 3}]
  }}
])

// 9. 结果写入新集合
db.SC.aggregate([
  {$group: {_id:"$Sno", avg:{$avg:"$Grade"}}},
  {$out: "SC_summary"}
])

// 10. 允许使用磁盘(结果集超 100MB 内存限制时必须开启)
db.SC.aggregate([...], {allowDiskUse: true})

(4)聚合与 SQL 对照表

SQL 聚合管道
SELECT COUNT(*) FROM SC db.SC.aggregate([{$group:{_id:null, n:{$sum:1}}}])
SELECT SUM(Grade) FROM SC db.SC.aggregate([{$group:{_id:null, s:{$sum:"$Grade"}}}])
SELECT Sno, SUM(Grade) FROM SC GROUP BY Sno db.SC.aggregate([{$group:{_id:"$Sno", s:{$sum:"$Grade"}}}])
... GROUP BY Sno HAVING SUM(Grade)>200 [{$group:{_id:"$Sno",s:{$sum:"$Grade"}}}, {$match:{s:{$gt:200}}}]
... WHERE Grade>85 GROUP BY Sno [{$match:{Grade:{$gt:85}}}, {$group:{_id:"$Sno", ...}}]
SELECT DISTINCT Sdept FROM Student db.Student.aggregate([{$group:{_id:"$Sdept"}}])
SELECT a.*, b.* FROM SC a JOIN Student b ON a.Sno=b.Sno [{$lookup:{from:"Student",localField:"Sno",foreignField:"Sno",as:"b"}}]
ORDER BY x DESC LIMIT 10 OFFSET 20 [{$sort:{x:-1}}, {$skip:20}, {$limit:10}]
SELECT a+b AS c FROM t [{$project:{c:{$add:["$a","$b"]}}}]

性能要点:$match 和 $sort 尽量放在管道最前面,这样可以利用索引并尽早减少数据量;$project 提前裁剪字段可减少内存占用。

(5)MapReduce 简述

聚合管道之前,MongoDB 用 mapReduce 做复杂聚合:

db.SC.mapReduce(
  function() { emit(this.Sno, this.Grade); },              // map:输出 key-value
  function(key, values) { return Array.sum(values); },     // reduce:按 key 归约
  {
    query: {Grade: {$gte: 60}},        // 过滤条件
    out: "SC_total",                   // 输出集合(或 {inline: 1} 直接返回)
    sort: {Sno: 1},
    limit: 10000,
    finalize: function(key, val){ return val; },
    scope: {}                          // 传入全局变量
  }
)

mapReduce 使用 JavaScript 引擎,性能远逊于聚合管道,5.0 起已废弃。除历史代码维护外,一律改用 aggregate。

3.8 事务简述

  • MongoDB 单文档操作天然原子(包括对嵌套子文档和数组的更新),这是 schema 设计推荐"嵌入"的重要原因。
  • 4.0 起支持副本集上的多文档 ACID 事务;4.2 起支持分片集群上的分布式事务。
  • 使用事务的前置条件:必须部署为副本集或分片集群(单机 mongod 不支持),存储引擎为 WiredTiger,且 featureCompatibilityVersion >= 4.0。
// mongo shell 中的事务用法
var session = db.getMongo().startSession();
session.startTransaction({
   readConcern:  {level: "snapshot"},
   writeConcern: {w: "majority"}
});
try {
   var sc  = session.getDatabase("学生系统").SC;
   var stu = session.getDatabase("学生系统").Student;
   sc.insertOne({Sno: 95001, Cno: 4, Grade: 78});
   stu.updateOne({Sno: 95001}, {$inc: {totalCredit: 3}});
   session.commitTransaction();       // 提交
} catch (e) {
   session.abortTransaction();        // 回滚
   throw e;
} finally {
   session.endSession();
}

事务限制与注意事项:

  • 默认事务最长执行 60 秒(transactionLifetimeLimitSeconds),超时自动回滚。
  • 事务中不能创建集合/索引(4.4 起部分放开),不能操作 admin/config/local 库。
  • 事务的 oplog 条目有 16MB 限制,不宜在一个事务中修改海量文档。
  • 事务会持有锁并可能导致写冲突(WriteConflict),应用需实现重试逻辑。
  • 设计建议:优先通过合理的文档嵌入设计避免跨文档事务,事务应是例外而非常态。

四、副本集与分片集群

4.1 MongoDB 集群类型

MongoDB 的集群搭建方式主要有三种:

模式 特点 适用场合
主从模式(Master-Slave) 经典主从架构 现在使用较少,3.6 起已移除
Replica Set(副本集)模式 具备自动故障转移能力 应用最为广泛
Sharding(分片)模式 功能最完备,水平扩展能力强 配置维护较复杂,所需服务器较多,一般用于数据超海量场景

本章重点介绍 Replica Set(副本集)模式 的搭建方法,并概述分片集群。

4.2 副本集的作用与架构

MongoDB 的 Replica Set 即副本集方式,主要有两个目的:

  1. 数据冗余与故障恢复:当发生硬件故障或其他原因造成宕机时,可使用副本进行恢复。
  2. 读写分离:读请求分流到副本上,减轻主节点(Primary)的读压力。

Replica Set 是一组 mongod 实例的集合,它们拥有同样的数据内容,包含三类角色。

4.2.1 成员角色说明

角色 说明
主节点(Primary) 接收所有写请求,然后把修改同步到所有 Secondary。一个 Replica Set 只能有一个 Primary;当 Primary 挂掉后,其他 Secondary 或 Arbiter 会重新选举出一个新的主节点。默认读请求也发到 Primary 处理,需要转发到 Secondary 时需在客户端修改连接配置。最佳做法是使用 DNS 解析副本集连接地址,当主节点故障时只需修改 DNS 地址,无需修改客户端配置。
副本节点(Secondary) 与主节点保持同样的数据集,通过拉取并重放主节点的 oplog 保持同步;当主节点宕机时参与选主。
仲裁者(Arbiter) 不保有数据,不参与选主(即不能被选为主),只进行选主投票。使用 Arbiter 可减轻数据存储的硬件需求,其运行几乎不占用大的硬件资源。在生产环境下,Arbiter 不要与其他数据节点部署在同一台机器上。

注意:一个支持自动 failover 的 Replica Set 节点数必须为奇数,目的是在选主投票时能形成大多数(majority)以进行决策。

其他特殊成员类型(通过配置属性实现,见 4.8 节):

类型 配置 用途
隐藏节点(Hidden) hidden: true, priority: 0 对客户端不可见,不接受读请求,专用于备份、离线分析
延迟节点(Delayed) slaveDelay: N, hidden: true, priority: 0 数据落后主节点 N 秒,可用于误删数据的"时光机"恢复
只读节点 priority: 0 永不参选主,仅提供数据冗余与读服务
无投票节点 votes: 0, priority: 0 超过 7 个成员时(副本集最多 7 个投票成员)用于扩展只读能力

4.2.2 oplog 的作用与大小设置

oplog(operation log,操作日志) 是副本集复制的核心:

  • 位于 local 库的固定集合 local.oplog.rs 中,是一个 capped collection(循环覆盖)。
  • 主节点把每一次写操作转换成幂等的语句记录到 oplog;Secondary 持续拉取(tail)主节点 oplog 并按顺序重放,从而保持数据一致。
  • 幂等性保证同一条 oplog 重放多次结果相同(例如 $inc: {n:1} 会被转成 $set: {n: 具体值})。
// 查看 oplog 状态
myreplset:PRIMARY> rs.printReplicationInfo()
configured oplog size:   100MB
log length start to end: 3600secs (1hrs)
oplog first event time:  Wed Nov 28 2018 15:00:00 GMT+0800
oplog last event time:   Wed Nov 28 2018 16:00:00 GMT+0800
now:                     Wed Nov 28 2018 16:00:05 GMT+0800

// 查看各从节点的复制延迟
myreplset:PRIMARY> rs.printSlaveReplicationInfo()

// 直接查看 oplog 内容
use local
db.oplog.rs.find().sort({$natural:-1}).limit(5).pretty()
db.oplog.rs.stats().maxSize          // oplog 上限

oplog 大小设置建议:

  • 配置项:旧式 oplogSize = 100(MB),YAML 为 replication.oplogSizeMB: 100。
  • 默认值:WiredTiger 下取磁盘可用空间的 5%,最小 990MB,最大 50GB。
  • 经验法则:oplog 应能容纳至少 24~72 小时的写操作量,保证从节点在维护/故障后仍能追上增量,而不必做全量初始同步。
  • 高写入量业务(如日志、IoT)应显著加大 oplog。
  • oplog 太小的后果:从节点长时间落后后,需要的 oplog 已被覆盖,节点进入 RECOVERING 状态且无法自愈,只能清空数据目录做全量初始同步(Initial Sync)。
// 4.0+ 支持在线调整 oplog 大小
use admin
db.adminCommand({replSetResizeOplog: 1, size: 4096})    // 单位 MB

4.2.3 选举机制

  • 若 Secondary 宕机,集群不受影响;
  • 若 Primary 宕机,剩余的 Secondary 与 Arbiter 将通过投票重新选举出新的 Primary。

选举要点:

  1. 心跳检测:成员之间每 2 秒(heartbeatIntervalMillis)互发心跳;若 10 秒(electionTimeoutMillis)内未收到主节点心跳,Secondary 发起选举。
  2. 多数派(majority)原则:候选者必须获得超过半数投票成员的选票才能当选。总投票数为 N 时,majority = floor(N/2) + 1。
  3. 因此投票成员总数应为奇数:4 个成员的 majority 是 3,与 3 个成员相同,却多承担了一份成本,且容错能力并未提升。
  4. 优先级 priority(0~1000,默认 1):值越大越优先成为主节点。priority: 0 表示永不参选。当一个更高优先级的成员追上数据后,会主动触发选举把主位"抢"回来。
  5. oplog 新旧:只有拥有最新(或不落后于多数派)oplog 的成员才能当选,防止数据丢失。
  6. votes:每个成员 0 或 1 票,副本集最多 7 个投票成员、最多 50 个成员。

容错能力对照:

数据节点数 投票成员数 majority 最多可容忍宕机数
1 1 1 0
2 2 2 0(挂一个就无法选主,因此需加 Arbiter)
2 + 1 Arbiter 3 2 1
3 3 2 1
4 + 1 Arbiter 5 3 2
5 5 3 2
7 7 4 3

原文此处配有选主过程示意图,当前文本中无图,请结合上述文字说明理解选主流程。

4.2.4 使用 Arbiter 搭建 Replica Set

当数据节点为偶数个时,可加入一个 Arbiter 共同构成 Replica Set,使投票节点总数为奇数,从而满足多数派选举要求。

4.3 副本集部署实战:环境规划

4.3.1 环境信息

系统 MongoDB 版本 主机 IP 用途
CentOS 7.4 mongodb-linux-x86_64-rhel70-3.2.9.tgz 192.168.1.220 主节点(master)
CentOS 7.4 mongodb-linux-x86_64-rhel70-3.2.9.tgz 192.168.1.221 备份节点
CentOS 7.4 mongodb-linux-x86_64-rhel70-3.2.9.tgz 192.168.1.222 备份节点 + 仲裁节点(slave + arbiter)

注意:不同场景下 IP 不相同,但安装方法与流程一致。安装时注意 IP 与主机名的差异。

4.3.2 角色与端口规划

节点 IP 端口 角色
node1 192.168.1.220 27017 Primary
node2 192.168.1.221 27017 Secondary
node3 192.168.1.222 27017 Secondary
node3-arbiter 192.168.1.222 27018 Arbiter

4.3.3 主机名与 hosts 配置

生产环境强烈建议在副本集配置中使用主机名而非 IP,这样 IP 变更时只需改 DNS/hosts,无需重配副本集。

# 每个节点分别设置主机名
# CentOS 7
hostnamectl set-hostname server-node1     # 192.168.1.220
hostnamectl set-hostname server-node2     # 192.168.1.221
hostnamectl set-hostname server-node3     # 192.168.1.222

# CentOS 6
hostname server-node1
sed -i 's/^HOSTNAME=.*/HOSTNAME=server-node1/' /etc/sysconfig/network

# 三台机器的 /etc/hosts 都要写入全部成员(内容保持一致)
cat >> /etc/hosts <<'EOF'
192.168.1.220  server-node1
192.168.1.221  server-node2
192.168.1.222  server-node3
EOF

# 验证互通
ping -c 2 server-node2

同时确保各节点时间同步(选举与 oplog 时间戳依赖时钟):

# CentOS 7
yum install -y chrony
systemctl enable --now chronyd
chronyc sources -v

# CentOS 6
yum install -y ntp
service ntpd start
chkconfig ntpd on
ntpdate -u ntp1.aliyun.com

4.4 各节点安装 MongoDB

4.4.1 主节点(192.168.1.220)安装

(1)进入安装目录

cd mongodb-3.2.9-x64

(2)执行安装脚本

./mongo_install.sh

安装结果如下:

[root@server-node1 mongodb-3.2.9-x64]# ./mongo_install.sh
starting install mongodb.......
mongodb install success
MONGODB_HOME:/opt/truplatform/mongodb
MONGODB_DATA:/opt/truplatform/mongodb/data/db
MONGODB_LOG:/opt/truplatform/mongodb/logs
MONGODB_CONF:/opt/truplatform/mongodb/etc

(3)让环境变量生效

source /etc/profile.d/mongodb.sh

(4)执行 mongo 命令测试

mongo
[root@server-node1 mongodb-3.2.9-x64]# mongo
MongoDB shell version: 3.2.9
connecting to: test
> show dbs;
local 0.000GB

(5)修改配置文件

编辑 MongoDB 配置文件(原文使用 vim,通常位于 /opt/truplatform/mongodb/etc/mongod.conf),关键是加入 replSet 副本集名称:

fork = true
port = 27017
bind_ip = 192.168.1.220
quiet = true
dbpath = /opt/truplatform/mongodb/data/db
logpath = /opt/truplatform/mongodb/logs/mongod.log
logappend = true
journal = true
auth = false
replSet=myreplset
oplogSize=100
noprealloc=true
#keyFile=/opt/truplatform/mongodb/mongodb.key

等价的 YAML 写法:

systemLog:
  destination: file
  path: /opt/truplatform/mongodb/logs/mongod.log
  logAppend: true
  quiet: true
storage:
  dbPath: /opt/truplatform/mongodb/data/db
  journal:
    enabled: true
net:
  port: 27017
  bindIp: 192.168.1.220
processManagement:
  fork: true
replication:
  replSetName: myreplset          # 同一副本集所有成员必须完全一致
  oplogSizeMB: 100
# security:
#   authorization: enabled
#   keyFile: /opt/truplatform/mongodb/mongodb.key

(6)重启 MongoDB 生效

service mongod restart          # CentOS 6/7 通用
# systemctl restart mongod      # CentOS 7 推荐

4.4.2 第一个备份节点(192.168.1.221)安装

(1)进入安装目录

cd mongodb-3.2.9-x64

(2)执行安装脚本

./mongo_install.sh

安装结果:

[root@server-node2 mongodb-3.2.9-x64]# ./mongo_install.sh
starting install mongodb.......
mongodb install success
MONGODB_HOME:/opt/truplatform/mongodb
MONGODB_DATA:/opt/truplatform/mongodb/data/db
MONGODB_LOG:/opt/truplatform/mongodb/logs
MONGODB_CONF:/opt/truplatform/mongodb/etc

(3)让环境变量生效

source /etc/profile.d/mongodb.sh

(4)执行 mongo 命令测试

[root@server-node2 mongodb-3.2.9-x64]# mongo
MongoDB shell version: 3.2.9
connecting to: test
> show dbs;
local 0.000GB

(5)修改配置文件(仅 bind_ip 与主节点不同)

fork = true
port = 27017
bind_ip = 192.168.1.221
quiet = true
dbpath = /opt/truplatform/mongodb/data/db
logpath = /opt/truplatform/mongodb/logs/mongod.log
logappend = true
journal = true
auth = false
replSet=myreplset
oplogSize=100
noprealloc=true
#keyFile=/opt/truplatform/mongodb/mongodb.key

(6)重启 MongoDB 生效

service mongod restart

4.4.3 第二个备份节点(192.168.1.222)安装

对应主机 192.168.1.222,作为第二个 Secondary 节点。

(1)进入安装目录

cd mongodb-3.2.9-x64

(2)执行安装脚本

./mongo_install.sh

安装结果:

[root@server-node2 mongodb-3.2.9-x64]# ./mongo_install.sh
starting install mongodb.......
mongodb install success
MONGODB_HOME:/opt/truplatform/mongodb
MONGODB_DATA:/opt/truplatform/mongodb/data/db
MONGODB_LOG:/opt/truplatform/mongodb/logs
MONGODB_CONF:/opt/truplatform/mongodb/etc

(3)让环境变量生效

source /etc/profile.d/mongodb.sh

(4)执行 mongo 命令测试

[root@server-node2 mongodb-3.2.9-x64]# mongo
MongoDB shell version: 3.2.9
connecting to: test
> show dbs;
local 0.000GB

(5)修改配置文件

fork = true
port = 27017
bind_ip = 192.168.1.222
quiet = true
dbpath = /opt/truplatform/mongodb/data/db
logpath = /opt/truplatform/mongodb/logs/mongod.log
logappend = true
journal = true
auth = false
replSet=myreplset
oplogSize=100
noprealloc=true
#keyFile=/opt/truplatform/mongodb/mongodb.key

(6)重启 MongoDB 生效

service mongod restart

4.4.4 在第二个备份节点上安装 MongoDB Arbiter

在 192.168.1.222 上额外部署一个 Arbiter 实例,与本机 Secondary 使用不同端口(27018)和独立的数据目录。

(1)拷贝备份节点 MongoDB 为 mongodb-arbiter

进入 /opt/truplatform/ 目录:

cd /opt/truplatform/

复制目录:

cp -rfp mongodb mongodb-arbiter

查看结果:

[root@server-node2 truplatform]# ll
total 24
drwxr-xr-x 6 mongod mongod 4096 Nov 28 15:33 mongodb
drwxr-xr-x 6 mongod mongod 4096 Nov 28 15:33 mongodb-arbiter

注意:复制过来的 data/db 目录如果含有数据,应先清空,避免 Arbiter 携带脏数据。

(2)复制备份节点启动脚本

cp /etc/init.d/mongod /etc/init.d/mongod-arbiter

(3)修改启动脚本变量

将启动脚本中的相关路径变量(如 MONGODB_HOME、数据目录、日志目录、配置文件路径等)修改为指向 mongodb-arbiter 目录,并使用独立的配置文件与端口,以区分原 Secondary 实例。

原文此步骤配有修改启动脚本变量的截图,当前文本中无图,请按实际脚本内容调整。

(4)将 mongodb-arbiter 添加到服务

chkconfig --add mongodb-arbiter

(5)修改配置文件

编辑 Arbiter 的配置文件(位于 /opt/truplatform/mongodb-arbiter/etc/ 下),注意端口为 27018、数据目录独立:

fork = true
port = 27018
bind_ip = 192.168.1.222
quiet = true
dbpath = /opt/truplatform/mongodb-arbiter/data/db
logpath = /opt/truplatform/mongodb-arbiter/logs/mongod.log
logappend = true
journal = true
auth = false
replSet=myreplset
oplogSize=100
noprealloc=true
#keyFile=/opt/truplatform/mongodb-arbiter/mongodb.key

(6)重启 mongodb-arbiter 生效

service mongod-arbiter restart

至此四个 mongod 实例(3 数据节点 + 1 仲裁节点)全部启动完成,但尚未组成副本集。

4.5 初始化副本集

4.5.1 登录主节点 MongoDB

mongo --host 192.168.1.220

4.5.2 编写集群配置对象

在 mongo shell 中输入集群配置对象:

cfg = {
  _id: 'myreplset',
  members: [
    { _id: 0, host: '192.168.1.220:27017', priority: 2 },
    { _id: 1, host: '192.168.1.221:27017', priority: 1 },
    { _id: 2, host: '192.168.1.222:27017', priority: 1 },
    { _id: 3, host: '192.168.1.222:27018', arbiterOnly: true }
  ]
};

说明:原文中 members 数组存在重复 _id(均为 1、2)的笔误,且 220 的 priority 实际期望为 2(见后文容灾测试说明),此处按 MongoDB 对 _id 唯一的要求以及文中 priority 行为,修正为 0/1/2/3、220 优先级为 2。请以自身实际规划为准。

配置正常显示如下:

> cfg = {
...   _id: 'myreplset',
...   members: [
...     { _id: 0, host: '192.168.1.220:27017', priority: 2 },
...     { _id: 1, host: '192.168.1.221:27017', priority: 1 },
...     { _id: 2, host: '192.168.1.222:27017', priority: 1 },
...     { _id: 3, host: '192.168.1.222:27018', arbiterOnly: true }
...   ]
... };
{
  "_id" : "myreplset",
  "members" : [
    { "_id" : 0, "host" : "192.168.1.220:27017", "priority" : 2 },
    { "_id" : 1, "host" : "192.168.1.221:27017", "priority" : 1 },
    { "_id" : 2, "host" : "192.168.1.222:27017", "priority" : 1 },
    { "_id" : 3, "host" : "192.168.1.222:27018", "arbiterOnly" : true }
  ]
}

配置文档中的成员属性说明:

属性 说明
_id(顶层) 副本集名称,必须与各节点 replSet / replSetName 一致
members[]._id 成员编号,必须唯一且不可修改
members[].host 主机:端口,推荐使用主机名
members[].priority 优先级 0~1000,默认 1,越大越优先成为 Primary;0 表示永不参选
members[].arbiterOnly true 表示该成员为仲裁节点(不存数据,只投票)
members[].votes 投票权 0 或 1,默认 1
members[].hidden true 表示隐藏节点,客户端不可见
members[].slaveDelay 延迟复制的秒数(4.2+ 改名 secondaryDelaySecs)
members[].buildIndexes 是否构建索引,默认 true(仅备份专用节点才设 false)
members[].tags 标签,用于按机房/区域做读偏好与写关注定制
settings.chainingAllowed 是否允许从节点从其他从节点同步(级联复制),默认 true
settings.heartbeatIntervalMillis 心跳间隔,默认 2000
settings.electionTimeoutMillis 选举超时,默认 10000
settings.getLastErrorDefaults 默认写关注

4.5.3 使副本集生效

在 mongo shell 中执行:

> rs.initiate(cfg)
{ "ok" : 1 }

执行成功后,shell 提示符会从 > 变为 myreplset:OTHER>,几秒后变为 myreplset:PRIMARY>。

也可以先用 rs.initiate() 空参数初始化(只含当前节点),再用 rs.add() 逐个添加成员,见 4.7 节。

4.5.4 查看副本集状态

myreplset:PRIMARY> rs.status()

输出示例:

{
  "set" : "myreplset",
  "date" : ISODate("2018-11-28T09:09:58.524Z"),
  "myState" : 1,
  "term" : NumberLong(1),
  "heartbeatIntervalMillis" : NumberLong(2000),
  "members" : [
    {
      "_id" : 0,
      "name" : "192.168.1.220:27017",
      "health" : 1,
      "state" : 1,
      "stateStr" : "PRIMARY",
      "uptime" : 1968,
      "optime" : {
        "ts" : Timestamp(1543396143, 1),
        "t" : NumberLong(1)
      },
      "optimeDate" : ISODate("2018-11-28T09:09:03Z"),
      "infoMessage" : "could not find member to sync from",
      "electionTime" : Timestamp(1543396142, 1),
      "electionDate" : ISODate("2018-11-28T09:09:02Z"),
      "configVersion" : 1,
      "self" : true
    },
    {
      "_id" : 1,
      "name" : "192.168.1.221:27017",
      "health" : 1,
      "state" : 2,
      "stateStr" : "SECONDARY",
      "uptime" : 67,
      "optime" : {
        "ts" : Timestamp(1543396143, 1),
        "t" : NumberLong(1)
      },
      "optimeDate" : ISODate("2018-11-28T09:09:03Z"),
      "lastHeartbeat" : ISODate("2018-11-28T09:09:58.319Z"),
      "lastHeartbeatRecv" : ISODate("2018-11-28T09:09:57.152Z"),
      "pingMs" : NumberLong(0),
      "syncingTo" : "192.168.1.220:27017",
      "configVersion" : 1
    },
    {
      "_id" : 2,
      "name" : "192.168.1.222:27017",
      "health" : 1,
      "state" : 2,
      "stateStr" : "SECONDARY",
      "uptime" : 67,
      "optime" : {
        "ts" : Timestamp(1543396143, 1),
        "t" : NumberLong(1)
      },
      "optimeDate" : ISODate("2018-11-28T09:09:03Z"),
      "lastHeartbeat" : ISODate("2018-11-28T09:09:58.319Z"),
      "lastHeartbeatRecv" : ISODate("2018-11-28T09:09:57.152Z"),
      "pingMs" : NumberLong(0),
      "syncingTo" : "192.168.1.220:27017",
      "configVersion" : 1
    },
    {
      "_id" : 3,
      "name" : "192.168.1.222:27018",
      "health" : 1,
      "state" : 7,
      "stateStr" : "ARBITER",
      "uptime" : 67,
      "lastHeartbeat" : ISODate("2018-11-28T09:09:58.319Z"),
      "lastHeartbeatRecv" : ISODate("2018-11-28T09:09:58.131Z"),
      "pingMs" : NumberLong(0),
      "configVersion" : 1
    }
  ],
  "ok" : 1
}

4.5.5 rs.status() 关键字段解读

字段 含义
set 副本集名称
myState 当前连接节点的状态码(1=PRIMARY,2=SECONDARY,7=ARBITER)
term 选举任期号,每次成功选举 +1,可用于判断集群是否频繁切主
heartbeatIntervalMillis 心跳间隔(毫秒)
members[].name 成员地址
members[].health 健康状态:1 正常,0 不可达(排障首先看这个)
members[].state / stateStr 状态码 / 状态字符串
members[].uptime 该成员已运行秒数
members[].optime.ts 该成员最后一次应用的 oplog 时间戳,用于比较主从进度
members[].optimeDate 上述时间戳的可读形式,主从此值之差即复制延迟
members[].lastHeartbeat 最后一次向该成员发送心跳的时间
members[].lastHeartbeatRecv 最后一次收到该成员心跳的时间
members[].pingMs 到该成员的网络往返时延(毫秒)
members[].syncingTo 该从节点正在从哪个成员同步(可能是主,也可能是另一个从)
members[].electionTime / electionDate 当前 Primary 的当选时间
members[].configVersion 配置版本号,所有成员必须一致,不一致说明配置未同步完成
members[].infoMessage 附加信息,如 could not find member to sync from
members[].self 是否为当前连接的这个节点

4.5.6 成员状态说明

状态 状态码 含义
STARTUP 0 刚启动,正在加载副本集配置
PRIMARY 1 主节点,接收所有写操作
SECONDARY 2 复制节点,参与选主,与主保持数据同步
RECOVERING 3 正在恢复中,暂不可读写(常见于 oplog 落后过多、回滚后)
STARTUP2 5 已加载配置,正在执行初始同步(Initial Sync),数据量大时会持续很久
UNKNOWN 6 其他成员无法探知该成员状态
ARBITER 7 仲裁节点,只投票不存数据
DOWN 8 成员不可达(宕机或网络隔离)
ROLLBACK 9 正在回滚数据(旧主重新加入时回滚未被多数派确认的写)
REMOVED 10 曾属于该副本集,已被移除

4.6 副本集常用命令操作

4.6.1 添加 Secondary 节点

priority 的值越大,越优先成为主节点。

rs.add({ host: "192.168.1.222:27017", priority: 1 });

// 简写形式
rs.add("192.168.1.222:27017");

// 添加隐藏节点
rs.add({ host: "192.168.1.223:27017", priority: 0, hidden: true });

4.6.2 添加仲裁节点

rs.addArb("192.168.1.223:27019");

4.6.3 移除节点

rs.remove({ host: "192.168.1.222:27017" });

// 简写形式
rs.remove("192.168.1.222:27017");

4.6.4 其他常用命令

rs.isMaster();          // 是否为主节点(同时返回全部成员列表、主节点地址)
rs.status();            // 查看副本集状态
rs.conf();              // 查看副本集配置(等价 rs.config())
rs.help();              // 获取命令帮助
rs.initiate(cfg);       // 初始化副本集
rs.reconfig(cfg);       // 重新加载配置(修改成员属性时使用)
rs.reconfig(cfg, {force: true});   // 强制重配(多数派丢失时的应急手段,慎用)
rs.stepDown(60);        // 主节点主动降级,60 秒内不再参选
rs.freeze(120);         // 冻结当前节点 120 秒,期间不参与选举
rs.slaveOk();           // 允许当前连接从 Secondary 读(4.4+ 改名 rs.secondaryOk())
rs.syncFrom("192.168.1.220:27017");   // 指定同步源
rs.printReplicationInfo();            // 查看 oplog 信息
rs.printSlaveReplicationInfo();       // 查看各从节点复制延迟
db.isMaster();                        // 与 rs.isMaster() 等价

rs.isMaster() 输出要点:

myreplset:PRIMARY> rs.isMaster()
{
  "hosts" : [ "192.168.1.220:27017", "192.168.1.221:27017", "192.168.1.222:27017" ],
  "arbiters" : [ "192.168.1.222:27018" ],
  "setName" : "myreplset",
  "setVersion" : 1,
  "ismaster" : true,                          // 当前节点是否为主
  "secondary" : false,
  "primary" : "192.168.1.220:27017",          // 当前主节点地址
  "me" : "192.168.1.220:27017",
  "electionId" : ObjectId("7fffffff0000000000000001"),
  "maxBsonObjectSize" : 16777216,             // 单文档最大 16MB
  "maxWriteBatchSize" : 1000,
  "ok" : 1
}

4.6.5 Secondary 节点读操作

登录 Secondary 节点直接执行 show dbs; 会出现以下报错:

myreplset:SECONDARY> show dbs;
2018-11-28T17:41:18.471+0800 E QUERY [thread1] Error: listDatabases failed:{ "ok" : 0, "errmsg" : "not master and slaveOk=false", "code" : 13435 } :
_getErrorWithCode@src/mongo/shell/utils.js:25:13
Mongo.prototype.getDBs@src/mongo/shell/mongo.js:62:1
shellHelper.show@src/mongo/shell/utils.js:761:19
shellHelper@src/mongo/shell/utils.js:651:15
@(shellhelp2):1:1

执行 rs.slaveOk() 后,再执行 show dbs; 就不会报错:

myreplset:SECONDARY> rs.slaveOk();
myreplset:SECONDARY> show dbs;
local 0.000GB

版本差异:4.4 起 rs.slaveOk() 被弃用,改用 rs.secondaryOk();驱动/mongosh 中更推荐通过 readPreference 设置,见 4.9 节。

至此 MongoDB 副本集安装配置完成。

4.7 验证测试

4.7.1 测试数据备份冗余功能

在主节点创建一个数据库,验证是否会同步到 Secondary 节点。

在主节点写入数据:

myreplset:PRIMARY> db.testdb.insert({ name: 'jwiserver', age: 20 });
WriteResult({ "nInserted" : 1 })

myreplset:PRIMARY> db.testdb.find();
{ "_id" : ObjectId("5bfe642bb96cecc8f6910c88"), "name" : "jwiserver", "age" : 20 }

到 Secondary 节点查看,可以看到已同步创建的 testdb:

myreplset:SECONDARY> show dbs;
local  0.000GB
testdb 0.000GB

查看同步内容:

myreplset:SECONDARY> use testdb;
switched to db testdb

myreplset:SECONDARY> db.testdb.find();
{ "_id" : ObjectId("5bfe642bb96cecc8f6910c88"), "name" : "jwiserver", "age" : 20 }

若 Secondary 上查询报 not master and slaveOk=false,先执行 rs.slaveOk()。

4.7.2 测试故障转移功能

停止 192.168.1.220 节点上的 MongoDB 服务:

[root@server-node1 ~]# service mongod stop
Stopping mongod (via systemctl): [ OK ]

到 Secondary 节点登录查看:

[root@server-node2 etc]# mongo --host 192.168.1.221
MongoDB shell version: 3.2.9
connecting to: 192.168.1.221:27017/test
myreplset:PRIMARY>

提示符已由 SECONDARY 变为 PRIMARY。执行 rs.isMaster() 命令验证,可以看到原来的 Secondary 节点变成了 PRIMARY 节点,表示故障转移成功。

myreplset:PRIMARY> rs.isMaster().primary
192.168.1.221:27017

myreplset:PRIMARY> rs.status().members.forEach(function(m){ print(m.name, m.stateStr, m.health) })
192.168.1.220:27017 (not reachable/healthy) 0
192.168.1.221:27017 PRIMARY 1
192.168.1.222:27017 SECONDARY 1
192.168.1.222:27018 ARBITER 1

原文此步骤配有故障转移结果截图,当前文本中无图。

验证要点:整个自动切换过程通常在 10~12 秒内完成(electionTimeoutMillis 默认 10 秒 + 选举耗时)。切换期间客户端写操作会失败,应用需具备重试机制。

4.7.3 原主节点恢复后的行为

启动之前停止的主节点 MongoDB 服务:

[root@server-node1 ~]# service mongod start
Starting mongod (via systemctl): [ OK ]

登录 mongo shell:

mongo --host 192.168.1.220

执行 rs.isMaster() 测试,会发现原故障节点重新成为 PRIMARY 节点。原因是配置副本集时,220 节点的 priority 设置为 2,而其他 Secondary 节点的 priority 为 1;priority 值越高,越优先成为 PRIMARY 节点。该节点追上数据后会主动发起选举,把主位抢回来。

若不希望恢复后自动抢回主位(避免二次切换抖动),应把所有数据节点的 priority 设为相同值。

4.7.4 手动降级与成员管理

// 主节点主动让位(默认 60 秒内不再参选),用于计划内维护
myreplset:PRIMARY> rs.stepDown()
myreplset:PRIMARY> rs.stepDown(120)           // 120 秒内不再参选

// 冻结某个从节点,防止它在维护窗口内被选为主
myreplset:SECONDARY> rs.freeze(300)
myreplset:SECONDARY> rs.freeze(0)             // 解除冻结

// 移除成员
myreplset:PRIMARY> rs.remove("192.168.1.222:27017")

4.7.5 修改成员优先级、隐藏节点与延迟节点

修改成员属性必须走 rs.conf() → 改对象 → rs.reconfig() 三步,且只能在 Primary 上执行。

// 1. 取出当前配置
cfg = rs.conf()

// 2. 修改优先级:让 221 成为首选主
cfg.members[1].priority = 3
cfg.members[0].priority = 1

// 3. 配置隐藏节点(必须同时 priority=0)
cfg.members[2].priority = 0
cfg.members[2].hidden = true

// 4. 配置延迟节点:数据落后 1 小时(3600 秒),可用于误删恢复
cfg.members[2].priority   = 0
cfg.members[2].hidden     = true
cfg.members[2].slaveDelay = 3600      // 4.2+ 改名 secondaryDelaySecs

// 5. 配置无投票成员
cfg.members[2].votes    = 0
cfg.members[2].priority = 0

// 6. 打标签,用于按机房路由
cfg.members[0].tags = { dc: "sz", use: "prod" }
cfg.members[1].tags = { dc: "gz", use: "prod" }

// 7. 调整选举超时
cfg.settings.electionTimeoutMillis = 5000

// 8. 应用配置(configVersion 自动 +1,并同步给所有成员)
rs.reconfig(cfg)

// 9. 确认生效
rs.conf()

延迟节点使用要点:延迟时间必须小于 oplog 覆盖窗口,否则该节点会因 oplog 被覆盖而进入 RECOVERING。误删数据后,可在延迟窗口内从延迟节点导出被删数据再恢复。

4.8 读偏好 readPreference

读偏好决定客户端把读请求发往哪类成员,用于实现读写分离。

取值 含义 适用场景
primary 默认值。只从主节点读;主不可用时读操作失败 需要强一致读(读己之所写)
primaryPreferred 优先读主;主不可用时读从 一致性优先,但要求高可用
secondary 只从从节点读;无可用从节点时读失败 报表、离线分析,明确要求不打扰主节点
secondaryPreferred 优先读从;无可用从节点时读主 最常用的读写分离配置
nearest 按网络时延选最近的成员(不区分主从) 多地域部署,追求最低延迟
# 连接字符串中设置
mongodb://192.168.1.220:27017,192.168.1.221:27017/mydb?replicaSet=myreplset&readPreference=secondaryPreferred

# mongo shell 启动时设置
mongo "mongodb://192.168.1.220:27017,192.168.1.221:27017/?replicaSet=myreplset&readPreference=secondary"
// shell / 驱动中按查询设置
db.Student.find().readPref("secondaryPreferred")

// 配合 tag 精确路由到指定机房
db.Student.find().readPref("secondary", [{dc: "sz"}, {}])

// 限制最大可接受的复制延迟(3.4+)
db.Student.find().readPref("secondary", null, 90)   // maxStalenessSeconds=90

读关注 readConcern(控制读到的数据的可见性级别):

级别 含义
local 默认。返回节点本地最新数据,可能被回滚
available 分片集群下最快,可能返回孤儿文档
majority 只返回已被多数派确认、不会被回滚的数据(3.2+)
linearizable 线性化读,保证读到最新已确认写(3.4+,仅 primary、单文档)
snapshot 快照读,用于多文档事务(4.0+)
db.Student.find().readConcern("majority")
db.runCommand({find: "Student", readConcern: {level: "majority"}})

读写分离的代价:从节点存在复制延迟,可能读到旧数据(弱一致性)。对"写后立刻读"的场景必须用 primary 或 readConcern: majority + writeConcern: majority 组合。

4.9 写关注 writeConcern

写关注决定写操作在多少个节点确认后才向客户端返回成功,是在性能与数据安全之间做权衡。

{ w: <value>, j: <boolean>, wtimeout: <number> }

w — 需要确认的节点数:

取值 含义
0 不等待确认(fire and forget),最快但可能丢数据
1 默认值。主节点写入内存即返回;主节点宕机可能丢失该写
2 / 3 … 至少 N 个节点(含主)确认
"majority" 多数派确认,生产环境推荐,保证写入不会被回滚
<tag set> 满足指定标签的成员确认,如 w: "sz_dc"

j — 是否写入 journal(磁盘):

  • j: false(默认):只要写入内存即算成功。
  • j: true:必须落盘到 journal 才返回,进程崩溃也不丢数据,但延迟更高。

wtimeout — 超时毫秒数:

  • 等待 w 个确认的最长时间。超时后返回错误,但已写入的数据不会回滚(应用需自行判断处理)。
  • w > 1 时必须设置 wtimeout,否则节点故障时会无限阻塞。
// 单条写操作指定
db.Student.insertOne({Sno: 95007, Sname: "钱八"},
    {writeConcern: {w: "majority", j: true, wtimeout: 5000}})

db.Student.updateMany({Sdept:"CS"}, {$set:{active:true}},
    {writeConcern: {w: 2, wtimeout: 3000}})

// 连接字符串中全局设置
// mongodb://host1,host2/db?replicaSet=myreplset&w=majority&journal=true&wtimeoutMS=5000

// 副本集级别设置默认写关注
cfg = rs.conf()
cfg.settings.getLastErrorDefaults = { w: "majority", j: true, wtimeout: 5000 }
rs.reconfig(cfg)

组合建议:

业务类型 推荐配置
金融/订单等关键写 {w: "majority", j: true, wtimeout: 5000}
一般业务写 {w: "majority", wtimeout: 3000}
日志/埋点等海量写 {w: 1} 或 {w: 0}(追求吞吐,容忍少量丢失)

4.10 副本集的备份策略

策略 做法 优缺点
在隐藏节点上 mongodump 在 hidden: true, priority: 0 的成员上执行 mongodump,不影响线上读写 生产首选。逻辑备份,可跨版本恢复;大数据量时较慢
文件系统快照 确保 journal 与数据在同一卷,用 LVM / 云盘快照 速度快,适合 TB 级;需要卷级支持
fsyncLock + 复制数据目录 db.fsyncLock() 锁写并刷盘 → 拷贝 dbPath → db.fsyncUnlock() 期间该节点不可写,只能在从节点上做
oplog 增量备份 定期全量 + 持续抓取 oplog,实现 PITR(时间点恢复) 恢复粒度最细;实现较复杂
延迟节点 保留一个延迟 1~6 小时的成员 误删数据的"热"恢复通道,不是真正的备份
多机房副本 在异地机房放置 priority: 0 的成员 机房级容灾
# 在隐藏节点上做全量备份(带 oplog,可用于 PITR)
mongodump --host 192.168.1.223 --port 27017 \
          -u backup -p 'pwd' --authenticationDatabase admin \
          --oplog -o /backup/full_$(date +%F)

# fsyncLock 方式(在从节点上执行)
mongo --host 192.168.1.222 --eval "db.fsyncLock()"
tar -zcf /backup/db_$(date +%F).tar.gz /opt/truplatform/mongodb/data/db
mongo --host 192.168.1.222 --eval "db.fsyncUnlock()"

备份原则:

  1. 绝不在 Primary 上做全量备份(IO 争抢会拖慢线上业务)。
  2. 备份文件必须异地存放,并定期做恢复演练(未验证的备份等于没有备份)。
  3. 备份需与副本集配置(rs.conf())、用户角色(admin.system.users)一并保存。
  4. 使用 --oplog 备份 + --oplogReplay 恢复实现时间点一致性。

4.11 分片集群(Sharding)

4.11.1 分片的目的

当单个副本集的数据量或写入吞吐超过单机上限时,就需要分片:

  • 突破存储上限:数据水平切分到多个 shard,每个 shard 只存一部分。
  • 突破写入上限:写请求被分散到多个 shard 并行处理。
  • 突破内存上限:每个 shard 只需缓存自己那部分的工作集。

4.11.2 架构组成

            ┌──────────────┐  ┌──────────────┐
 客户端 ───▶ │   mongos     │  │   mongos     │   (路由层,无状态,可多个)
            └──────┬───────┘  └──────┬───────┘
                   │                 │
           ┌───────┴─────────────────┴────────┐
           │                                  │
   ┌───────▼────────┐              ┌──────────▼──────────┐
   │ config servers │              │   shard1 / shard2…  │
   │  (副本集 CSRS)│              │  (每个都是副本集)  │
   └────────────────┘              └─────────────────────┘
组件 作用
mongos 查询路由器。客户端只连 mongos;它从 config server 缓存元数据,把请求路由到正确的 shard,并合并结果返回。本身不存数据,无状态,通常部署 2 个以上或与应用同机部署
config server 配置服务器。存储集群元数据(分片列表、chunk 分布、分片键范围、认证信息)。3.4 起必须部署为副本集(CSRS),通常 3 个成员
shard 分片节点,存放实际数据。生产环境每个 shard 都应是一个副本集,以同时获得高可用

4.11.3 分片键的选择原则

分片键(shard key)一旦确定,改动代价极大(4.4 前不可改),必须谨慎设计:

  1. 高基数(Cardinality):取值足够多,否则无法细分(如用"性别"做分片键,最多只能分 2 片)。
  2. 写分布均匀:避免单调递增键(如时间戳、自增 ID、ObjectId),否则所有新写入都落在最后一个 chunk,形成热点分片。
  3. 查询隔离性(Query Isolation):常用查询条件应包含分片键,这样 mongos 能做定向查询(targeted query),只访问 1 个 shard;否则退化为广播查询(scatter-gather),性能差。
  4. 不可为数组字段,且必须建立以分片键为前缀的索引。

4.11.4 范围分片与哈希分片

方式 命令 特点
范围分片(Ranged) sh.shardCollection("db.coll", {key: 1}) 按键值区间切分。范围查询高效(相邻数据在同一 shard);单调递增键会造成写热点
哈希分片(Hashed) sh.shardCollection("db.coll", {key: "hashed"}) 对键值散列后切分。写入分布非常均匀,能化解单调键热点;但范围查询需广播到所有 shard
复合分片键 sh.shardCollection("db.coll", {a: 1, b: 1}) 兼顾均匀与隔离,如 {region: 1, timestamp: 1}

4.11.5 chunk 与均衡器 balancer

  • chunk(数据块):分片集合按分片键范围被切成许多 chunk,chunk 是数据迁移的最小单位。默认大小 64MB。
  • split(分裂):chunk 超过大小上限时自动一分为二(仅修改元数据,不移动数据)。
  • balancer(均衡器):后台进程(运行在 config server 的 primary 上),监测各 shard 的 chunk 数量差,超过阈值时自动迁移 chunk 使之均衡。
  • jumbo chunk:因分片键取值重复过多导致无法分裂的超大 chunk,无法迁移,是分片键选择失败的典型症状。
// 均衡器管理
sh.getBalancerState()                     // 查看是否开启
sh.isBalancerRunning()                    // 是否正在迁移
sh.stopBalancer()                         // 关闭(大批量导入前建议关闭)
sh.startBalancer()                        // 开启
sh.setBalancerState(true)

// 设置均衡窗口(只在业务低峰迁移)
use config
db.settings.updateOne(
   {_id: "balancer"},
   {$set: {activeWindow: {start: "01:00", stop: "05:00"}}},
   {upsert: true}
)

// 调整 chunk 大小(单位 MB)
use config
db.settings.updateOne({_id:"chunksize"}, {$set:{value: 128}}, {upsert:true})

// 手动切分与迁移
sh.splitAt("学生系统.Student", {Sno: 95003})
sh.splitFind("学生系统.Student", {Sno: 95003})
sh.moveChunk("学生系统.Student", {Sno: 95003}, "shard2")

4.11.6 分片集群搭建步骤

步骤 1:搭建 config server 副本集(CSRS)

三台机器上各启动一个 configsvr 实例(端口 27019):

# /etc/mongod-config.conf
systemLog:
  destination: file
  path: /data/config/logs/mongod.log
  logAppend: true
storage:
  dbPath: /data/config/db
  journal:
    enabled: true
net:
  port: 27019
  bindIp: 0.0.0.0
processManagement:
  fork: true
  pidFilePath: /data/config/mongod.pid
sharding:
  clusterRole: configsvr          # 声明为配置服务器
replication:
  replSetName: cfgReplSet         # config server 副本集名
# 三台机器分别启动
mongod -f /etc/mongod-config.conf

# 连接任一节点初始化 config 副本集
mongo --host 192.168.1.220 --port 27019
rs.initiate({
  _id: "cfgReplSet",
  configsvr: true,                 // 必须声明
  members: [
    {_id: 0, host: "192.168.1.220:27019"},
    {_id: 1, host: "192.168.1.221:27019"},
    {_id: 2, host: "192.168.1.222:27019"}
  ]
})
rs.status()

步骤 2:搭建各 shard 副本集

每个 shard 都是一个独立副本集,配置中需声明 clusterRole: shardsvr:

# /etc/mongod-shard1.conf
storage:
  dbPath: /data/shard1/db
net:
  port: 27018
  bindIp: 0.0.0.0
systemLog:
  destination: file
  path: /data/shard1/logs/mongod.log
  logAppend: true
processManagement:
  fork: true
  pidFilePath: /data/shard1/mongod.pid
sharding:
  clusterRole: shardsvr           # 声明为分片节点
replication:
  replSetName: shard1             # 第二个分片用 shard2,依此类推
mongod -f /etc/mongod-shard1.conf
mongo --host 192.168.1.220 --port 27018
// 初始化 shard1 副本集
rs.initiate({
  _id: "shard1",
  members: [
    {_id: 0, host: "192.168.1.220:27018"},
    {_id: 1, host: "192.168.1.221:27018"},
    {_id: 2, host: "192.168.1.222:27018", arbiterOnly: true}
  ]
})

// 同样方式初始化 shard2 副本集(端口可用 27020)
rs.initiate({
  _id: "shard2",
  members: [
    {_id: 0, host: "192.168.1.221:27020"},
    {_id: 1, host: "192.168.1.222:27020"},
    {_id: 2, host: "192.168.1.220:27020", arbiterOnly: true}
  ]
})

步骤 3:启动 mongos 路由

# /etc/mongos.conf
systemLog:
  destination: file
  path: /data/mongos/logs/mongos.log
  logAppend: true
net:
  port: 27017                     # 客户端连接的端口
  bindIp: 0.0.0.0
processManagement:
  fork: true
  pidFilePath: /data/mongos/mongos.pid
sharding:
  configDB: cfgReplSet/192.168.1.220:27019,192.168.1.221:27019,192.168.1.222:27019
# security:
#   keyFile: /data/mongos/mongodb.key
# 注意:启动的是 mongos,不是 mongod;mongos 没有 dbPath
mongos -f /etc/mongos.conf

# 连接 mongos
mongo --host 192.168.1.220 --port 27017

步骤 4:添加分片 sh.addShard

mongos> sh.addShard("shard1/192.168.1.220:27018,192.168.1.221:27018")
{ "shardAdded" : "shard1", "ok" : 1 }

mongos> sh.addShard("shard2/192.168.1.221:27020,192.168.1.222:27020")
{ "shardAdded" : "shard2", "ok" : 1 }

// 查看已添加的分片
mongos> db.getSiblingDB("config").shards.find().pretty()
mongos> sh.status()

步骤 5:对数据库和集合启用分片

// 1. 对数据库启用分片
mongos> sh.enableSharding("学生系统")
{ "ok" : 1 }

// 2. 分片键必须先有索引(哈希分片建 hashed 索引)
mongos> use 学生系统
mongos> db.Student.createIndex({Sno: "hashed"})

// 3. 对集合分片 —— 哈希分片(写入均匀,适合单调递增键)
mongos> sh.shardCollection("学生系统.Student", {Sno: "hashed"})
{ "collectionsharded" : "学生系统.Student", "ok" : 1 }

// 4. 或范围分片(利于范围查询)
mongos> db.SC.createIndex({Sno: 1, Cno: 1})
mongos> sh.shardCollection("学生系统.SC", {Sno: 1, Cno: 1})

// 5. 预分片:大批量导入前预先切分 chunk,避免导入时频繁 split/migrate
mongos> sh.shardCollection("学生系统.logs", {_id:"hashed"}, false, {numInitialChunks: 64})

步骤 6:查看分片状态与验证

mongos> sh.status()
--- Sharding Status ---
  sharding version: { "_id" : 1, "clusterId" : ObjectId("...") }
  shards:
        {  "_id" : "shard1",  "host" : "shard1/192.168.1.220:27018,192.168.1.221:27018",  "state" : 1 }
        {  "_id" : "shard2",  "host" : "shard2/192.168.1.221:27020,192.168.1.222:27020",  "state" : 1 }
  active mongoses:
        "4.4.0" : 1
  balancer:
        Currently enabled:  yes
        Currently running:  no
  databases:
        {  "_id" : "学生系统",  "primary" : "shard1",  "partitioned" : true }
                学生系统.Student
                        shard key: { "Sno" : "hashed" }
                        chunks:
                                shard1  2
                                shard2  2
                        { "Sno" : { "$minKey" : 1 } } -->> { "Sno" : NumberLong("-4611686018427387902") } on : shard1

其他常用分片命令:

sh.status({verbose: true})              // 详细状态(含全部 chunk 范围)
db.Student.getShardDistribution()       // 查看某集合在各分片的数据分布
db.Student.stats()                      // 在 mongos 上执行会按分片汇总
sh.removeShard("shard2")                // 移除分片(会先把数据迁走,需反复执行至完成)
sh.addShardTag("shard1", "sz")          // 打标签(区域分片 zone sharding)
sh.addTagRange("学生系统.Student", {Sno: MinKey}, {Sno: 96000}, "sz")
db.adminCommand({flushRouterConfig: 1}) // 刷新 mongos 元数据缓存
db.adminCommand({listShards: 1})

4.11.7 分片集群运维注意事项

  1. 客户端只能连 mongos,直连 shard 会看到不完整数据且可能破坏元数据。
  2. config server 必须做好备份,元数据丢失等于整个集群报废。
  3. 分片集群不能对 admin、config、local 库分片。
  4. 大批量导入数据前先 sh.stopBalancer() 并做预分片,导入后再开启。
  5. 分片后 $lookup、事务、唯一索引(非分片键前缀)等能力受限。
  6. 不要过早分片:先把副本集用到极限(垂直扩容、索引优化、读写分离)再考虑分片,分片带来的运维复杂度是数量级增长。

五、安全、运维与其他 NoSQL

5.1 安全:启用认证与用户管理

在配置文件中设置了监听 IP,意味着数据库可以被远程连接,但若未设置密码,无法用于生产环境。因此必须对数据库连接进行加密与认证。

5.1.1 启用访问控制 authorization

MongoDB 默认不开启认证,任何能连上端口的人都是超级管理员。启用步骤("localhost 例外"机制允许在未创建用户前从本机创建第一个用户):

# 1. 先以不带认证的方式启动(或临时注释掉 auth)
# 2. 从本机连接并创建管理员用户(见 5.1.2)
# 3. 再开启认证并重启
# YAML 写法
security:
  authorization: enabled
# 旧式写法
auth = true

5.1.2 创建管理员用户 db.createUser

登录 PRIMARY 节点,创建集群管理员角色,用于管理用户角色。

登录 MongoDB:

mongo --host 192.168.1.220

切换到 admin 库并创建用户:

myreplset:PRIMARY> use admin;
switched to db admin

myreplset:PRIMARY> db.createUser({
  user: "admin",
  pwd: "JWIServer27017",
  roles: [
    { role: "userAdminAnyDatabase", db: "admin" },
    { role: "clusterAdmin", db: "admin" }
  ]
});

其他常见用户创建示例:

// 超级管理员(root 包含几乎全部权限)
use admin
db.createUser({user: "root", pwd: "RootPwd@123", roles: [{role: "root", db: "admin"}]})

// 业务读写用户(只对"学生系统"库有读写权限)
use 学生系统
db.createUser({
  user: "appuser",
  pwd: "AppPwd@123",
  roles: [{role: "readWrite", db: "学生系统"}]
})

// 只读用户(用于报表、BI)
db.createUser({user: "reader", pwd: "ReadPwd@123", roles: [{role: "read", db: "学生系统"}]})

// 备份用户
use admin
db.createUser({user: "backup", pwd: "BakPwd@123",
  roles: [{role:"backup", db:"admin"}, {role:"restore", db:"admin"}]})

// 用户管理
db.getUsers()                                   // 查看当前库所有用户
db.getUser("appuser")
show users
db.updateUser("appuser", {roles: [{role:"readWrite", db:"学生系统"}, {role:"read", db:"test"}]})
db.changeUserPassword("appuser", "NewPwd@456")
db.grantRolesToUser("admin", ["clusterAdmin"])  // 追加角色
db.revokeRolesFromUser("appuser", [{role:"read", db:"test"}])
db.dropUser("reader")
db.dropAllUsers()                               // 删除当前库所有用户(危险)

5.1.3 内置角色一览

角色 作用范围 权限说明
read 单库 只读该库所有非系统集合
readWrite 单库 读写该库所有非系统集合(业务账号最常用)
dbAdmin 单库 该库的索引管理、统计、校验、profiling,但不含用户管理
dbOwner 单库 = readWrite + dbAdmin + userAdmin
userAdmin 单库 管理该库的用户与角色
readAnyDatabase 集群 读所有库
readWriteAnyDatabase 集群 读写所有库
userAdminAnyDatabase 集群 管理所有库的用户与角色(创建第一个用户时使用)
dbAdminAnyDatabase 集群 管理所有库
clusterMonitor 集群 只读监控:serverStatus、rs.status、sh.status 等
clusterManager 集群 集群管理:副本集重配、分片管理、均衡器控制
hostManager 集群 主机级操作:shutdown、logRotate、killOp
clusterAdmin 集群 = clusterMonitor + clusterManager + hostManager(副本集/分片管理必备)
backup 集群 备份所需的最小权限
restore 集群 恢复所需的最小权限
root 集群 超级权限:几乎包含以上所有角色
__system 内部 MongoDB 内部使用,切勿授予人类用户

自定义角色:

use admin
db.createRole({
  role: "readAndDropCollection",
  privileges: [
    { resource: {db: "学生系统", collection: ""}, actions: ["find", "dropCollection"] }
  ],
  roles: []
})
db.getRoles({showBuiltinRoles: false})
db.grantRolesToUser("appuser", [{role: "readAndDropCollection", db: "admin"}])
db.dropRole("readAndDropCollection")

5.1.4 认证登录

mongo
myreplset:PRIMARY> use admin;
switched to db admin

// 用户密码认证,返回 1 表示认证成功
myreplset:PRIMARY> db.auth("admin", "JWIServer27017");
1

// 查看副本集状态
rs.status();

// 查看副本集群配置
rs.config();

命令行直接带认证登录:

# 方式一:命令行参数(-p 不跟值可交互式输入,避免密码进 history)
mongo --host 192.168.1.220 --port 27017 -u admin -p --authenticationDatabase admin

mongo -u appuser -p 'AppPwd@123' --authenticationDatabase 学生系统 学生系统

# 方式二:连接字符串
mongo "mongodb://admin:JWIServer27017@192.168.1.220:27017,192.168.1.221:27017/学生系统?replicaSet=myreplset&authSource=admin"

authenticationDatabase / authSource 指定的是用户所在的库(创建用户时 use 的那个库),不是要访问的业务库。管理员通常在 admin 库。

5.1.5 rs.status() 权限报错解决

执行 rs.status() 时可能出现如下报错:

myreplset:PRIMARY> rs.status();
{
  "ok" : 0,
  "errmsg" : "not authorized on admin to execute command { replSetGetStatus: 1.0 }",
  "code" : 13
}

解决办法是为 admin 用户授予 clusterAdmin 角色:

myreplset:PRIMARY> db.grantRolesToUser("admin", ["clusterAdmin"]);

再次执行 rs.status() 即不再报权限错误:

myreplset:PRIMARY> rs.status();
{
  "set" : "myreplset",
  "date" : ISODate("2019-01-14T07:00:57.577Z"),
  "myState" : 1,
  "term" : NumberLong(139),
  "heartbeatIntervalMillis" : NumberLong(2000),
  "members" : [
    {
      "_id" : 0,
      "name" : "192.168.1.220:27017",
      ...
    }
    ...
  ]
}

原文输出在此处被截断,后续内容为其他节点状态信息。

5.2 副本集内部认证:keyFile 与 x.509

开启 authorization 后,副本集成员之间也需要相互认证,否则复制会中断。

5.2.1 创建集群 keyFile

使用密钥文件身份验证时,副本集中的每个 mongod 实例都使用密钥文件的内容作为共享密码来验证部署中的其他成员。只有持有正确密钥文件的 mongod 实例才能加入副本集。

密钥文件要求:

  • 内容长度必须介于 6 到 1024 个字符之间;
  • 对副本集的所有成员必须相同;
  • 权限应设置为仅所有者可读(400 或 600);
  • 内容只能是 base64 字符集中的字符(换行会被忽略)。

使用 openssl 生成复杂伪随机 1024 字符串作为密钥:

# 创建密码文件
openssl rand -base64 756 > mongodb.key

# 设置权限
chmod 400 mongodb.key

将 key 文件存放到每个节点的安装目录下:

# 复制文件到目标路径
cp mongodb.key /opt/truplatform/mongodb/

# 查看是否为 mongod 所有者
ls -l mongodb.key

# 若属主不对需修正(否则 mongod 无权读取,启动失败)
chown mongod:mongod /opt/truplatform/mongodb/mongodb.key
chmod 400 /opt/truplatform/mongodb/mongodb.key

# 用 scp 分发到其他节点(保证内容完全一致)
scp -p mongodb.key root@192.168.1.221:/opt/truplatform/mongodb/
scp -p mongodb.key root@192.168.1.222:/opt/truplatform/mongodb/
scp -p mongodb.key root@192.168.1.222:/opt/truplatform/mongodb-arbiter/

5.2.2 启用 keyFile 与 auth

将各节点配置文件中的 keyFile 行前的 # 去掉,并将 auth 设置为 true,确保每个节点配置一致。

最终配置文件示例:

fork = true
port = 27017
bind_ip = 0.0.0.0
quiet = true
dbpath = /opt/truplatform/mongodb/data/db
logpath = /opt/truplatform/mongodb/logs/mongod.log
logappend = true
journal = true
auth = true
replSet = myreplset
oplogSize = 100
noprealloc = true
keyFile = /opt/truplatform/mongodb/mongodb.key

Arbiter 节点的 keyFile 路径应指向其自身安装目录下的 key 文件,如 /opt/truplatform/mongodb-arbiter/mongodb.key。

修改配置后,重启所有 mongod 服务使其生效:

# 重启数据节点
service mongod restart

# 重启仲裁节点
service mongod-arbiter restart

滚动重启原则:生产环境应先逐个重启 Secondary,最后 rs.stepDown() 让 Primary 降级再重启,可将不可用时间压到最短。

5.2.3 x.509 证书认证(更高安全等级)

net:
  ssl:
    mode: requireSSL
    PEMKeyFile: /opt/truplatform/mongodb/etc/mongodb-node.pem
    CAFile: /opt/truplatform/mongodb/etc/ca.pem
security:
  clusterAuthMode: x509          # 成员间用证书互认,替代 keyFile
  authorization: enabled

x.509 相比 keyFile 的优势:可为每个成员签发独立证书、支持吊销、传输加密与身份认证一体化,适合金融等合规要求高的场景。

5.3 网络暴露风险与加固

5.3.1 bindIp 限制

# 危险:监听所有网卡,等于向公网敞开大门
net:
  bindIp: 0.0.0.0

# 正确:只监听本机回环 + 内网地址
net:
  bindIp: 127.0.0.1,192.168.1.220

MongoDB 3.6 起默认 bindIp: 127.0.0.1(只允许本机),这是官方针对大规模勒索事件做出的安全默认值改变。除非确有需要,不要改成 0.0.0.0。

5.3.2 未授权访问的风险案例

2017 年初,全球爆发大规模 MongoDB 勒索事件("MongoDB Apocalypse"):攻击者扫描互联网上开放 27017 端口且未启用认证的 MongoDB 实例,删除全部数据库并留下勒索信,要求支付比特币赎回。数万个实例受害,其中包含大量生产数据,多数受害者因无备份而永久丢失数据。Elasticsearch、Redis、Hadoop 随后遭遇同类攻击。

根因分析:

  1. MongoDB 3.4 及以前版本默认 bindIp 为全部网卡;
  2. 默认不启用认证(authorization 关闭);
  3. 默认端口 27017 众所皆知,极易被 Shodan / ZoomEye 等搜索引擎扫到;
  4. 运维人员为图方便直接把数据库暴露在公网。

加固清单(逐条落实):

措施 做法
启用认证 security.authorization: enabled,并为每个应用建独立最小权限账号
限制监听地址 bindIp 只写内网 IP 与 127.0.0.1,绝不写 0.0.0.0
防火墙白名单 仅放通应用服务器 IP 访问 27017,见 2.8 节
修改默认端口 改为非 27017 的端口,降低被扫概率(纵深防御,非根本手段)
禁用不必要功能 security.javascriptEnabled: false(不用 $where/mapReduce 时)
内部认证 keyFile 或 x.509,防止恶意节点加入副本集
传输加密 启用 TLS/SSL(net.ssl.mode: requireSSL)
静态加密 企业版支持 Encryption at Rest;社区版可用 LUKS 磁盘加密
不用 root 运行 专用 mongod 用户,限制文件系统权限
审计日志 企业版 auditLog;社区版至少开启 profiling 与日志留存
定期备份 + 演练 备份异地存放,并定期验证可恢复
及时升级 关注 CVE,及时打补丁

5.4 备份与恢复

使用 mongodump 与 mongorestore 命令进行数据备份与恢复。

5.4.1 备份和恢复所有数据库

# 备份
mongodump -h IP --port PORT -o BACKUPPATH

# 恢复
mongorestore -h IP --port PORT BACKUPPATH

5.4.2 备份和恢复指定数据库

# 备份
mongodump -h IP --port PORT -d DBNAME -o BACKUPPATH

# 恢复
mongorestore -h IP --port PORT BACKUPPATH

5.4.3 备份和恢复指定 collection

# 备份
mongodump -h IP --port PORT -d DBNAME -c COLLECTION -o xxx.bson

# 恢复
mongorestore -h IP --port PORT -d DBNAME -c COLLECTION xxx.bson

# 恢复前先删除旧数据
mongorestore -h IP --port PORT --drop -d DBNAME -c COLLECTION xxx.bson

5.4.4 mongodump / mongorestore 常用参数

参数 说明
-h / --host 指定主机(副本集写 myreplset/host1:port,host2:port)
--port 指定端口
-o / --out 输出,后面接输出路径(- 表示输出到标准输出)
-d / --db 指定数据库名称
-c / --collection 指定集合(collection)名称
--drop 恢复前先删除旧的数据库/集合(仅 mongorestore)
-u / -p 用户名 / 密码
--authenticationDatabase 认证库,通常为 admin
-q / --query 只备份满足条件的文档,如 --query '{"Sage":{"$gt":18}}'
--queryFile 从文件读取查询条件
--gzip 压缩备份文件(配合 --archive 效果最好)
--archive=file.gz 输出为单个归档文件,便于传输
--oplog 备份期间同时抓取 oplog,使备份具备时间点一致性(仅 mongodump,需副本集)
--oplogReplay 恢复时重放 oplog(仅 mongorestore)
--oplogLimit=<ts> 重放 oplog 到指定时间戳为止(实现 PITR)
--numParallelCollections 并行备份/恢复的集合数,默认 4
--numInsertionWorkersPerCollection 每集合的并行插入线程数(加速恢复)
--excludeCollection 排除指定集合
--excludeCollectionsWithPrefix 排除指定前缀的集合
--nsInclude / --nsExclude 按命名空间通配符包含/排除(4.0+)
--nsFrom / --nsTo 恢复时重命名库/集合
--noIndexRestore 不恢复索引(先导数据后建索引,可显著加速)
--dryRun 试运行,不真正写入
--readPreference 指定从哪类成员读取(备份建议 secondary)
--writeConcern 恢复时的写关注
--verbose / -v 输出详细进度(可叠加 -vvvv)

综合示例:

# 带认证、压缩归档、从从节点读取的全库备份
mongodump --host myreplset/192.168.1.220:27017,192.168.1.221:27017 \
          -u backup -p 'BakPwd@123' --authenticationDatabase admin \
          --readPreference secondary \
          --oplog --gzip --archive=/backup/all_$(date +%F).gz

# 从归档文件恢复
mongorestore --host 192.168.1.220:27017 \
             -u root -p 'RootPwd@123' --authenticationDatabase admin \
             --gzip --archive=/backup/all_2026-08-22.gz \
             --oplogReplay --drop

# 只备份一个集合并带查询条件
mongodump -h 192.168.1.220 --port 27017 -d 学生系统 -c Student \
          --query '{"Sdept":"IS"}' -o /backup/is_students

# 恢复到不同的库名(跨环境导入)
mongorestore -h 192.168.1.230 --port 27017 \
             --nsFrom '学生系统.*' --nsTo '学生系统_test.*' \
             /backup/all_2026-08-22

5.4.5 mongoexport / mongoimport:JSON 与 CSV

mongodump 产出 BSON(二进制,保真度高);mongoexport 产出 JSON/CSV(文本,便于跨系统交换,但会丢失部分类型精度)。

# 导出为 JSON(每行一个文档,即 JSON Lines)
mongoexport -h 192.168.1.220 --port 27017 -d 学生系统 -c Student -o /tmp/Student.json

# 导出为 CSV(必须用 --fields 指定字段顺序)
mongoexport -h 192.168.1.220 --port 27017 -d 学生系统 -c Student \
            --type=csv --fields Sno,Sname,Ssex,Sage,Sdept -o /tmp/Student.csv

# 带条件与投影导出
mongoexport -d 学生系统 -c SC --query '{"Grade":{"$gte":85}}' \
            --fields Sno,Cno,Grade --sort '{"Grade":-1}' --limit 100 \
            --type=csv -o /tmp/top_scores.csv

# 导出为标准 JSON 数组(便于程序解析)
mongoexport -d 学生系统 -c Student --jsonArray -o /tmp/Student_array.json

# 导入 JSON
mongoimport -h 192.168.1.220 --port 27017 -d 学生系统 -c Student --file /tmp/Student.json

# 导入 CSV(--headerline 表示首行为字段名)
mongoimport -d 学生系统 -c Student --type=csv --headerline --file /tmp/Student.csv

# 导入 CSV(手动指定字段名,文件无表头时)
mongoimport -d 学生系统 -c Student --type=csv \
            --fields Sno,Sname,Ssex,Sage,Sdept --file /tmp/Student_noheader.csv

# 导入前清空集合
mongoimport -d 学生系统 -c Student --drop --file /tmp/Student.json

# 已存在则更新(按 Sno 匹配),不存在则插入
mongoimport -d 学生系统 -c Student --upsert --upsertFields Sno --file /tmp/Student.json

# 忽略错误继续导入 + 并行加速
mongoimport -d 学生系统 -c Student --file /tmp/big.json \
            --numInsertionWorkers 8 --batchSize 1000 --maintainInsertionOrder=false
工具 格式 用途 保真度
mongodump / mongorestore BSON 数据库级备份恢复 高(完整保留类型与索引)
mongoexport / mongoimport JSON / CSV 数据交换、与其他系统对接 低(Date、ObjectId、Decimal 等可能失真)

重要:mongoexport 不能作为备份手段,因为它不保留索引定义,且 BSON 特有类型在 JSON 中会退化。

5.4.6 文件系统快照备份

适合 TB 级数据量,速度远快于 mongodump。

# 前提:journal 与数据文件必须在同一个逻辑卷上(否则快照不一致)

# ---- LVM 快照方式 ----
# 1. 锁定写并刷盘(在从节点上执行)
mongo --host 192.168.1.222 --eval "db.fsyncLock()"

# 2. 创建 LVM 快照
lvcreate --size 100G --snapshot --name mongo-snap /dev/vg0/mongodata

# 3. 立即解锁(快照创建是秒级操作)
mongo --host 192.168.1.222 --eval "db.fsyncUnlock()"

# 4. 挂载快照并归档
mkdir -p /mnt/snap
mount /dev/vg0/mongo-snap /mnt/snap
tar -zcf /backup/mongo_$(date +%F).tar.gz -C /mnt/snap .
umount /mnt/snap
lvremove -f /dev/vg0/mongo-snap

# ---- 直接拷贝数据目录(需先停服务或 fsyncLock)----
systemctl stop mongod
tar -zcf /backup/dbpath_$(date +%F).tar.gz /opt/truplatform/mongodb/data/db
systemctl start mongod

# ---- 恢复:停服务,清空 dbPath,解压回去,改属主,启动 ----
systemctl stop mongod
rm -rf /opt/truplatform/mongodb/data/db/*
tar -zxf /backup/dbpath_2026-08-22.tar.gz -C /
chown -R mongod:mongod /opt/truplatform/mongodb/data/db
systemctl start mongod

云环境可直接使用云盘快照(阿里云 ESSD 快照、AWS EBS Snapshot),效果等同且无需手工 LVM。

5.4.7 oplog 增量恢复与时间点恢复(PITR)

原理:全量备份提供基线,oplog 提供从基线到任意时刻的增量变更。重放 oplog 到指定时间戳即可恢复到误操作发生前的瞬间。

# ---- 步骤 1:定期全量备份(带 --oplog)----
mongodump --host myreplset/192.168.1.220:27017 \
          -u backup -p 'pwd' --authenticationDatabase admin \
          --oplog -o /backup/full_$(date +%F)
# 备份目录下会生成 oplog.bson,记录备份期间的增量操作

# ---- 步骤 2:持续备份 oplog(如每小时一次)----
mongodump --host myreplset/192.168.1.220:27017 \
          -u backup -p 'pwd' --authenticationDatabase admin \
          -d local -c oplog.rs \
          --query '{"ts":{"$gt":{"$timestamp":{"t":1543396143,"i":1}}}}' \
          -o /backup/oplog_incr

# ---- 步骤 3:恢复全量 + 重放 oplog ----
mongorestore --host 192.168.1.220:27017 \
             -u root -p 'pwd' --authenticationDatabase admin \
             --oplogReplay --drop /backup/full_2026-08-22

# ---- 步骤 4:定点恢复(恢复到误删操作之前)----
# 先在 oplog 中定位误操作(如 drop)的时间戳
mongo --eval 'use local; db.oplog.rs.find({op:"c"}).sort({$natural:-1}).limit(10).pretty()'

# 把增量 oplog 放到 oplogReplay 目录,用 --oplogLimit 指定停止点(误操作 ts)
mkdir -p /backup/replay
cp /backup/oplog_incr/local/oplog.rs.bson /backup/replay/oplog.bson
mongorestore --host 192.168.1.220:27017 \
             --oplogReplay --oplogLimit "1543396200:1" \
             /backup/replay
# oplogLimit 格式为 <秒时间戳>:<同秒内序号>,重放到该点(不含)为止

5.4.8 定时备份脚本

cat > /opt/scripts/mongo_backup.sh <<'EOF'
#!/bin/bash
# MongoDB 定时全量备份脚本
set -euo pipefail

# ===== 配置区 =====
MONGO_URI="myreplset/192.168.1.220:27017,192.168.1.221:27017,192.168.1.222:27017"
MONGO_USER="backup"
MONGO_PASS="BakPwd@123"
AUTH_DB="admin"
BACKUP_ROOT="/backup/mongodb"
KEEP_DAYS=7                      # 本地保留天数
LOG_FILE="/var/log/mongo_backup.log"
DATE=$(date +%Y%m%d_%H%M%S)
ARCHIVE="${BACKUP_ROOT}/mongo_${DATE}.gz"

log() { echo "[$(date '+%F %T')] $*" | tee -a "$LOG_FILE"; }

mkdir -p "$BACKUP_ROOT"
log "===== 备份开始 ====="

# ===== 执行备份(从从节点读,带 oplog,压缩归档)=====
if mongodump --host "$MONGO_URI" \
             -u "$MONGO_USER" -p "$MONGO_PASS" \
             --authenticationDatabase "$AUTH_DB" \
             --readPreference secondary \
             --oplog --gzip --archive="$ARCHIVE" >> "$LOG_FILE" 2>&1; then
    SIZE=$(du -h "$ARCHIVE" | cut -f1)
    log "备份成功:$ARCHIVE (大小 $SIZE)"
else
    log "备份失败!请检查日志"
    # 可在此接入告警:邮件 / 钉钉 / 企业微信 webhook
    exit 1
fi

# ===== 校验归档完整性 =====
if gzip -t "$ARCHIVE" 2>/dev/null; then
    log "归档校验通过"
else
    log "归档文件损坏!"
    exit 1
fi

# ===== 清理过期备份 =====
find "$BACKUP_ROOT" -name "mongo_*.gz" -type f -mtime +${KEEP_DAYS} -print -delete \
     | while read -r f; do log "已删除过期备份:$f"; done

# ===== 同步到异地(按需启用)=====
# rsync -avz --remove-source-files "$ARCHIVE" backup@192.168.1.250:/nas/mongodb/
# 或推送到对象存储:ossutil cp "$ARCHIVE" oss://mybucket/mongodb/

log "===== 备份结束 ====="
EOF

chmod 700 /opt/scripts/mongo_backup.sh

# 加入 crontab:每天凌晨 2:30 执行
crontab -e
# 30 2 * * * /opt/scripts/mongo_backup.sh

# 查看定时任务
crontab -l

脚本中含明文密码时务必 chmod 700;更安全的做法是把凭据放到 ~/.mongodb/credentials 或使用 --config 配置文件(4.2+)。

5.5 监控与运维

5.5.1 db.serverStatus() 关键指标

db.serverStatus()

// 只取关心的部分
db.serverStatus().connections
db.serverStatus().opcounters
db.serverStatus().mem
db.serverStatus().wiredTiger.cache
db.serverStatus().globalLock
db.serverStatus().metrics.document
db.serverStatus().repl
指标路径 含义与告警阈值
connections.current 当前连接数
connections.available 剩余可用连接数(接近 0 需立即扩容或排查连接泄漏)
connections.totalCreated 累计创建连接数(增长过快说明未用连接池)
opcounters.{insert,query,update,delete,getmore,command} 各类操作累计计数,差值即 QPS
opcountersRepl.* 复制产生的操作计数
mem.resident 物理内存占用(MB)
mem.virtual 虚拟内存占用
mem.mapped 映射内存(MMAPv1 时代)
globalLock.currentQueue.{total,readers,writers} 锁等待队列长度,持续 > 0 说明存在锁竞争或慢操作
globalLock.activeClients.* 活跃读写客户端数
wiredTiger.cache."bytes currently in the cache" 当前缓存使用量
wiredTiger.cache."maximum bytes configured" 缓存上限
wiredTiger.cache."tracked dirty bytes in the cache" 脏页量,超过 cache 的 20% 会触发 eviction 阻塞
wiredTiger.cache."pages read into cache" 读入缓存的页数(高说明工作集超内存)
wiredTiger.concurrentTransactions.{read,write}.available 可用读/写票据(降到 0 表示引擎层已饱和)
metrics.document.{returned,inserted,updated,deleted} 文档级操作计数
metrics.queryExecutor.scanned 扫描的索引键数
metrics.queryExecutor.scannedObjects 扫描的文档数(远大于 returned 说明索引不足)
metrics.operation.scanAndOrder 内存排序次数(高说明排序未走索引)
repl.setName / repl.ismaster 副本集名与当前角色
asserts.* 各类断言/错误计数
uptime 运行时长(秒),突然变小说明进程重启过
network.{bytesIn,bytesOut,numRequests} 网络流量

5.5.2 db.currentOp() 与 db.killOp()

// 查看当前所有正在执行的操作
db.currentOp()

// 只看正在运行的(排除空闲连接)
db.currentOp({"active": true})

// 找出执行超过 3 秒的操作(排障主力命令)
db.currentOp({"active": true, "secs_running": {"$gt": 3}})

// 找出某个库/集合上的操作
db.currentOp({"ns": /^学生系统\./, "active": true})

// 找出正在等锁的操作
db.currentOp({"waitingForLock": true})

// 找出索引创建操作
db.currentOp({"$or": [
   {"op": "command", "command.createIndexes": {$exists: true}},
   {"op": "none", "msg": /^Index Build/}
]})

// 包含空闲连接与系统操作
db.currentOp({"$all": true})

// 杀掉指定操作(用上面输出中的 opid)
db.killOp(12345)

// 批量杀掉超时的慢查询
db.currentOp({"active":true, "secs_running":{"$gt":30}, "op":"query"})
  .inprog.forEach(function(op){ print("killing " + op.opid); db.killOp(op.opid); })

currentOp 输出关键字段:

字段 含义
opid 操作 ID,killOp 的参数
active 是否正在执行
secs_running / microsecs_running 已运行时长
op 操作类型:query / insert / update / remove / getmore / command
ns 命名空间 库.集合
command 具体的命令内容(可看到完整查询条件)
planSummary 执行计划摘要,COLLSCAN 表示全表扫描
client 发起该操作的客户端地址
waitingForLock 是否在等锁
numYields 主动让出锁的次数(高说明操作很重)
locks / lockStats 持锁类型与统计

注意:不要 killOp 索引创建(会留下不完整索引)和内部复制操作(op: "none" 且 ns: local.oplog.rs 的)。

5.5.3 慢查询分析 profiling

MongoDB 内置慢查询分析器(Database Profiler),把慢操作记录到 system.profile 集合(一个 capped collection)。

三个级别:

级别 含义
0 关闭(默认),不记录任何操作
1 只记录慢操作(执行时间超过 slowms 阈值的)
2 记录所有操作(仅用于短时排障,开销大,生产慎用)
// 查看当前 profiling 状态
db.getProfilingStatus()
{ "was" : 0, "slowms" : 100, "sampleRate" : 1 }
db.getProfilingLevel()

// 开启级别 1,阈值 100 毫秒
db.setProfilingLevel(1, {slowms: 100})
db.setProfilingLevel(1, 100)              // 旧写法

// 采样:只记录 50% 的慢操作,降低开销(4.0+)
db.setProfilingLevel(1, {slowms: 100, sampleRate: 0.5})

// 记录全部操作(排障时短时开启,用完立即关闭)
db.setProfilingLevel(2)

// 关闭
db.setProfilingLevel(0)

配置文件中持久化开启:

operationProfiling:
  mode: slowOp                # off / slowOp / all
  slowOpThresholdMs: 100
  slowOpSampleRate: 1.0

查询 system.profile:

// 最近 10 条慢操作
db.system.profile.find().sort({ts: -1}).limit(10).pretty()

// 按耗时排序找最慢的
db.system.profile.find().sort({millis: -1}).limit(5).pretty()

// 只看超过 500ms 的查询
db.system.profile.find({millis: {$gt: 500}, op: "query"}).sort({millis:-1}).pretty()

// 找出全表扫描的操作
db.system.profile.find({planSummary: "COLLSCAN"}).pretty()

// 按集合聚合统计慢查询分布
db.system.profile.aggregate([
  {$group: {_id: {ns:"$ns", plan:"$planSummary"},
            次数: {$sum:1}, 平均耗时: {$avg:"$millis"}, 最大耗时: {$max:"$millis"}}},
  {$sort: {平均耗时: -1}}
])

// 找出扫描行数远大于返回行数的查询(索引缺失的信号)
db.system.profile.find({$expr: {$gt: ["$docsExamined", {$multiply: ["$nreturned", 100]}]}}).pretty()

// 调整 system.profile 集合大小(需先关闭 profiling)
db.setProfilingLevel(0)
db.system.profile.drop()
db.createCollection("system.profile", {capped: true, size: 50*1024*1024})
db.setProfilingLevel(1, {slowms: 100})

system.profile 关键字段:op、ns、command、millis(耗时)、nreturned(返回数)、docsExamined(扫描文档数)、keysExamined(扫描索引键数)、planSummary(执行计划)、ts(时间)、client、user、writeConflicts、numYield、locks。

慢查询也会写入 mongod 日志(超过 slowms 自动记录),可直接从日志分析。

5.5.4 mongostat 与 mongotop

# mongostat:每 2 秒输出一次实例级统计
mongostat --host 192.168.1.220 --port 27017 2
mongostat -u admin -p 'pwd' --authenticationDatabase admin 2

# 监控整个副本集(自动发现全部成员)
mongostat --host myreplset/192.168.1.220:27017 --discover 2

# 输出更多列
mongostat --host 192.168.1.220 -O metrics.document.inserted 2

# 输出到文件持续记录
mongostat --host 192.168.1.220 2 > /var/log/mongostat.log &

mongostat 列含义:

列 含义
insert/query/update/delete 每秒各类操作数(星号 * 前缀表示复制产生的操作)
getmore 每秒游标 getmore 次数
command 每秒命令数(本地|复制)
dirty WiredTiger 缓存脏页百分比(>20% 需警惕)
used WiredTiger 缓存使用百分比(接近 95% 会触发强制驱逐)
flushes checkpoint 次数
vsize / res 虚拟内存 / 物理内存占用
qrw 读写排队数(持续非 0 说明拥塞)
arw 活跃读写客户端数
net_in / net_out 网络流入/流出流量
conn 当前连接数
set / repl 副本集名 / 角色(PRI、SEC、ARB、RTR=mongos)
# mongotop:按集合统计读写耗时,定位热点集合
mongotop --host 192.168.1.220 --port 27017 5
mongotop -u admin -p 'pwd' --authenticationDatabase admin 5

# 只看指定库
mongotop --host 192.168.1.220 --locks 5

mongotop 输出的 total / read / write 是每个采样周期内在该集合上花费的时间(毫秒),值最大的集合就是热点。

5.5.5 日志分析

# 实时查看日志
tail -f /opt/truplatform/mongodb/logs/mongod.log

# 找出慢查询记录(日志中会打印 "xxxms" 的操作)
grep -E "[0-9]{3,}ms" /opt/truplatform/mongodb/logs/mongod.log | tail -50

# 找出全表扫描
grep "COLLSCAN" /opt/truplatform/mongodb/logs/mongod.log | tail -20

# 找出选举与副本集状态变化
grep -Ei "election|transition to (PRIMARY|SECONDARY|RECOVERING)|stepDown" \
     /opt/truplatform/mongodb/logs/mongod.log

# 找出连接数问题
grep -i "connection refused|too many open|connection accepted" \
     /opt/truplatform/mongodb/logs/mongod.log

# 找出认证失败
grep -i "Authentication failed" /opt/truplatform/mongodb/logs/mongod.log

# 找出启动警告
grep -A2 "startup warnings" /opt/truplatform/mongodb/logs/mongod.log

# 日志轮转(避免单文件过大)
mongo --eval "db.adminCommand({logRotate: 1})"
# 或用 logrotate
cat > /etc/logrotate.d/mongod <<'EOF'
/opt/truplatform/mongodb/logs/mongod.log {
    daily
    rotate 30
    compress
    missingok
    notifempty
    create 640 mongod mongod
    sharedscripts
    postrotate
        /usr/bin/mongo --quiet --eval "db.adminCommand({logRotate:1})" > /dev/null 2>&1 || true
    endscript
}
EOF

MongoDB 日志组件(可分别调整级别):ACCESS、COMMAND、CONTROL、INDEX、NETWORK、QUERY、REPL、SHARDING、STORAGE、WRITE。

// 调整某个组件的日志级别(0~5)
db.setLogLevel(2, "query")
db.adminCommand({getParameter: 1, logComponentVerbosity: 1})

5.5.6 连接数与线程模型

MongoDB 采用 thread-per-connection 模型:每个客户端连接对应服务端一个线程(约 1MB 栈空间)。连接数暴涨会造成线程过多、上下文切换开销剧增。

// 查看连接数
db.serverStatus().connections
{
  "current" : 152,          // 当前连接数
  "available" : 51048,      // 剩余可用
  "totalCreated" : 98234,   // 累计创建(增长快说明连接未复用)
  "active" : 12             // 活跃连接数
}

// 查看各客户端的连接分布
db.currentOp(true).inprog.reduce(function(acc, op){
   if (op.client) { var ip = op.client.split(":")[0]; acc[ip] = (acc[ip]||0)+1; }
   return acc;
}, {})

// 查看最大连接数配置
db.serverStatus().connections.available + db.serverStatus().connections.current
db.adminCommand({getParameter: 1, maxIncomingConnections: 1})
net:
  maxIncomingConnections: 20000     # 上限,实际还受 ulimit -n 制约

优化要点:

  1. 应用侧必须使用连接池(驱动默认带池),设置合理的 maxPoolSize(一般 100 以内 / 每应用实例)。
  2. maxIncomingConnections 不宜设过大,ulimit -n 必须大于它。
  3. 短连接风暴(如 PHP、Serverless 场景)应引入代理层或 mongos 做连接复用。
  4. 内部复制连接、心跳连接也占用连接数,副本集成员越多占用越多。

5.5.7 WiredTiger 存储引擎:缓存与压缩

storage:
  engine: wiredTiger
  wiredTiger:
    engineConfig:
      cacheSizeGB: 8                    # 核心参数
      journalCompressor: snappy         # none / snappy / zlib / zstd
      directoryForIndexes: false
    collectionConfig:
      blockCompressor: snappy           # 集合数据压缩算法
    indexConfig:
      prefixCompression: true           # 索引前缀压缩,默认开启

cacheSizeGB 设置原则:

  • 默认值 = max((物理内存 - 1GB) × 0.5, 256MB)。
  • 专用数据库服务器可适度上调到物理内存的 60%,但必须为操作系统文件缓存留出空间(WiredTiger 还依赖 OS page cache 存压缩后的数据)。
  • 容器/K8s 环境中必须显式设置,否则 WiredTiger 会读取宿主机内存导致 OOM。
  • 目标:让索引 + 热数据工作集能装进 cache。可用 wiredTiger.cache."pages read into cache" 增长速度判断是否够用。

压缩算法对比:

算法 压缩率 CPU 开销 适用场景
none 无 最低 CPU 极度受限、数据本身已压缩
snappy 中(约 2~4x) 低 默认,通用最佳平衡
zlib 高(约 4~6x) 高 冷数据、归档、磁盘紧张
zstd 高(接近 zlib) 中(优于 zlib) 4.2+ 推荐替代 zlib
// 单个集合建表时指定压缩算法
db.createCollection("logs", {
   storageEngine: {wiredTiger: {configString: "block_compressor=zstd"}}
})

// 查看集合的 WiredTiger 配置与压缩效果
db.logs.stats().wiredTiger.creationString
db.logs.stats()          // 比较 size(逻辑大小)与 storageSize(磁盘占用)

5.5.8 空间回收:compact 与 repairDatabase

MongoDB 删除文档后,磁盘空间不会自动归还操作系统,而是保留为可复用的空洞。

// ---- compact:整理单个集合,回收碎片空间 ----
// 会阻塞该库的读写,必须在从节点上执行或维护窗口内执行
db.runCommand({compact: "Student"})
db.runCommand({compact: "Student", force: true})     // 在 Primary 上强制执行(危险)

// 查看碎片程度:storageSize 远大于 size 说明碎片多
db.Student.stats()
# ---- repairDatabase:重建整个数据库文件(回收空间 + 修复损坏)----
# 需要与数据同等大小的额外磁盘空间,且期间数据库完全不可用
mongo --eval "db.getSiblingDB('学生系统').repairDatabase()"

# 命令行方式(需先停 mongod)
mongod --dbpath /opt/truplatform/mongodb/data/db --repair
mongod --dbpath /opt/truplatform/mongodb/data/db --repair --repairpath /mnt/tmp

生产环境推荐的空间回收方案(滚动初始同步):

# 对每个从节点依次执行(一次只操作一个,保证多数派存活):
# 1. 停止该从节点
systemctl stop mongod
# 2. 清空数据目录
rm -rf /opt/truplatform/mongodb/data/db/*
# 3. 重新启动,MongoDB 会自动做全量初始同步(Initial Sync),文件紧凑无碎片
systemctl start mongod
# 4. 等待 rs.status() 中该节点从 STARTUP2 变为 SECONDARY
# 5. 全部从节点做完后,rs.stepDown() 让主降级,再对原主重复以上步骤

这种方式对业务零中断,是回收大量空间的最佳实践。repairDatabase 只在数据损坏抢救时使用。

5.6 性能优化

优化方向 具体做法
索引优先 所有高频查询条件、排序字段必须有索引;用 explain 确认走 IXSCAN;遵循 ESR 原则设计复合索引
避免全表扫描 用 profiling / 日志找出 COLLSCAN 并逐一消除;db.system.profile 定期审计
投影减少传输 find({}, {需要的字段:1, _id:0}),尽量做成覆盖索引查询,避免回表
深翻分页优化 skip(1000000).limit(10) 需扫描并丢弃 100 万条,性能极差。改用游标分页:记住上一页最后一条的排序键,用 {_id: {$gt: lastId}} 定位
批量操作 用 insertMany、bulkWrite 代替循环单条写;ordered: false 提升并行度
读偏好分流 报表/分析类查询走 secondary 或专用隐藏节点,与在线业务隔离
写关注权衡 关键写用 w: "majority",日志类写用 w: 1,不要一律用最高级别
连接池 应用侧复用连接,避免短连接风暴
schema 设计 嵌入 vs 引用的取舍(见下)
控制文档大小 单文档上限 16MB;文档过大会导致更新时整体重写、缓存效率低。大字段(图片、附件)用 GridFS 或对象存储
避免无界数组 数组只增不减会导致文档不断增长搬移;用"分桶模式"把数组拆到多个文档
预分配与预热 上线前把热数据读一遍装入 cache(db.coll.find().explain() 或 touch)
硬件 SSD/NVMe 磁盘、足够内存装下工作集、关闭 THP/NUMA(见 2.9 节)

深翻分页优化示例:

// 差:深度 skip
db.Student.find().sort({_id: 1}).skip(1000000).limit(10)

// 好:游标分页(记住上一页末尾的 _id)
var lastId = ObjectId("5bfe642bb96cecc8f6910c88");
db.Student.find({_id: {$gt: lastId}}).sort({_id: 1}).limit(10)

// 按业务字段分页(需该字段有索引且唯一,否则加 _id 作 tie-breaker)
db.Student.find({$or: [
   {Sage: {$gt: 19}},
   {Sage: 19, _id: {$gt: lastId}}
]}).sort({Sage: 1, _id: 1}).limit(10)

批量操作示例:

// bulkWrite:一次网络往返完成多种操作
db.Student.bulkWrite([
  {insertOne: {document: {Sno: 95008, Sname: "吴九"}}},
  {updateOne: {filter: {Sno: 95001}, update: {$inc: {Sage: 1}}}},
  {updateMany: {filter: {Sdept: "IS"}, update: {$set: {active: true}}}},
  {deleteOne: {filter: {Sno: 95004}}},
  {replaceOne: {filter: {Sno: 95002}, replacement: {Sno: 95002, Sname: "刘晨"}}}
], {ordered: false})

schema 设计:嵌入(Embedding)与引用(Referencing)的取舍

维度 嵌入(内嵌子文档/数组) 引用(存对方的 _id)
读性能 优:一次查询拿全,无需 JOIN 差:需多次查询或 $lookup
原子性 优:单文档更新天然原子 差:需多文档事务
数据冗余 有冗余,更新需同步多处 无冗余,一处修改全局生效
文档大小 可能触及 16MB 上限 不受限
适用关系 一对一、一对少(包含关系),如订单与订单行、文章与评论(少量) 一对多(多方数量大)、多对多,如学生与课程
典型判断标准 子数据总是随主数据一起读取、且数量有界(几十以内) 子数据独立查询、数量无界或被多方共享
// 嵌入示例:博客与少量评论(一起读,数量有界)
{
  _id: 1, title: "MongoDB 教程", author: "Jane",
  comments: [
    {by: "Ada", text: "Good post", at: ISODate()},
    {by: "Bob", text: "So so",    at: ISODate()}
  ]
}

// 引用示例:学生与成绩(成绩数量无界,且需独立统计)
// Student 集合
{_id: 95001, Sname: "李勇", Sdept: "CS"}
// SC 集合
{_id: ObjectId(), Sno: 95001, Cno: 1, Grade: 92}

// 混合模式(推荐):引用 + 冗余少量高频字段,避免大部分 JOIN
{_id: ObjectId(), Sno: 95001, Sname: "李勇", Cno: 1, Cname: "数据库", Grade: 92}

5.7 排错清单

5.7.1 连不上数据库

症状:couldn't connect to server、Connection refused、No route to host、连接超时。

排查顺序:

# 1. 进程是否在跑
ps -ef | grep mongod
systemctl status mongod

# 2. 端口是否在监听,监听在哪个地址(关键!)
netstat -tunlp | grep 27017
ss -tunlp | grep 27017
# 若只看到 127.0.0.1:27017,说明 bindIp 只绑了回环,远程必然连不上

# 3. 本机能否连(排除网络因素)
mongo --host 127.0.0.1 --port 27017 --eval "db.version()"

# 4. 检查 bindIp 配置
grep -E "bind_ip|bindIp" /opt/truplatform/mongodb/etc/mongod.conf

# 5. 防火墙
firewall-cmd --list-ports                     # CentOS 7
iptables -L -n | grep 27017                   # CentOS 6

# 6. SELinux
getenforce
ausearch -m avc -ts recent | grep mongod      # 查看 SELinux 拒绝记录

# 7. 从客户端侧测试连通性
telnet 192.168.1.220 27017
nc -zv 192.168.1.220 27017

# 8. 云环境还要检查安全组 / 网络 ACL
原因 解决
bindIp 只写了 127.0.0.1 加上内网 IP:bindIp: 127.0.0.1,192.168.1.220 后重启
防火墙未放通 27017 见 2.8 节添加规则
SELinux 拦截 setenforce 0 或打标签
mongod 未启动/已崩溃 看日志找根因(见 5.7.7)
云安全组未放通 在云控制台放通端口
连接数打满 connections.available 为 0,需扩大 maxIncomingConnections 与 ulimit -n

5.7.2 认证失败

症状:Authentication failed、code: 18、not authorized on xxx to execute command。

报错 原因 解决
Authentication failed, code: 18 用户名/密码错,或 authSource 指错了库 确认用户创建时所在的库,登录时 --authenticationDatabase 必须与之一致(管理员通常是 admin)
not authorized on admin to execute command {replSetGetStatus:1} 用户缺少 clusterAdmin 角色 db.grantRolesToUser("admin", ["clusterAdmin"])(见 5.1.5 节)
not authorized on 学生系统 to execute command {find:...} 用户对该库无读权限 授予 read 或 readWrite
command createUser requires authentication 已开启 auth 但还没有任何用户 用"localhost 例外"从本机连接创建第一个用户;若已过例外期,需临时关闭 auth 重启再建用户
副本集成员间 Authentication failed keyFile 内容不一致或权限不对 确保各节点 keyFile 内容完全相同、权限 400/600、属主为 mongod
密码含特殊字符导致 URI 解析失败 @、:、/、? 等 对密码做 URL 编码(@ → %40),或改用 -u/-p 参数
// 自查:确认用户存在于哪个库、有哪些角色
use admin
db.system.users.find({}, {user:1, db:1, roles:1}).pretty()

// 查看当前连接的认证身份与权限
db.runCommand({connectionStatus: 1, showPrivileges: true})

5.7.3 副本集成员卡在 STARTUP2 或 RECOVERING、不切主

状态 含义与常见原因 解决
长期 STARTUP2 正在做初始同步(Initial Sync)。数据量大时正常,但若长时间无进度,可能是网络带宽不足、源节点压力过大、或同步中途反复失败 看日志中 initial sync 进度;确保 oplog 足够大能覆盖整个同步过程;必要时清空 dbPath 重新同步
RECOVERING 不自愈 最常见原因是 oplog 太小:节点落后太多,所需 oplog 已被覆盖(日志中出现 too stale to catch up) 清空该节点 dbPath 重新做初始同步;同时加大 oplog(见 4.2.2 节)
RECOVERING 伴随 ROLLBACK 旧主重新加入,需回滚未被多数派确认的写。回滚数据超过 300MB 时会失败 清空 dbPath 重新同步;根本上应使用 w: "majority" 避免大量可回滚写
UNKNOWN / (not reachable/healthy) 网络不通、防火墙、bindIp 未含该成员可达地址、主机名解析失败 逐个成员互 ping / telnet 测试;检查 /etc/hosts 三台一致
主节点宕机后不选主 存活的投票成员数未达 majority 检查投票成员总数与存活数;投票成员应为奇数,必要时加 Arbiter
所有节点都是 SECONDARY 同上,多数派丢失 应急可用 rs.reconfig(cfg, {force:true}) 强制重配(有丢数据风险)
频繁切主(term 快速增长) 网络抖动、主节点负载过高心跳超时、磁盘 IO 打满 排查网络与磁盘;适当增大 electionTimeoutMillis
configVersion 不一致 reconfig 未同步完成,或曾强制重配 在 Primary 上重新执行 rs.reconfig(rs.conf())
// 排查命令组合
rs.status().members.forEach(function(m){
   print(m.name, m.stateStr, "health=" + m.health,
         "optime=" + (m.optimeDate ? m.optimeDate : "-"),
         "cfgVer=" + m.configVersion);
})
rs.printSlaveReplicationInfo()        // 看复制延迟
rs.printReplicationInfo()             // 看 oplog 窗口
# 日志中定位副本集问题
grep -Ei "too stale|rollback|initial sync|replSet|election" \
     /opt/truplatform/mongodb/logs/mongod.log | tail -50

5.7.4 oplog 太小导致从节点脱队

症状:从节点变成 RECOVERING,日志出现 we are too stale to use xxx as a sync source。

// 1. 确认 oplog 窗口有多长
rs.printReplicationInfo()
// log length start to end 太短(如只有几十分钟)就是隐患

// 2. 在线加大 oplog(4.0+,需在每个成员上分别执行)
use admin
db.adminCommand({replSetResizeOplog: 1, size: 4096})     // 改为 4GB

// 3. 旧版本需停机改配置
// oplogSizeMB: 4096 后重启(3.6 及以前修改 oplog 需手工操作 local.oplog.rs,较繁琐)

修复脱队节点:

systemctl stop mongod
rm -rf /opt/truplatform/mongodb/data/db/*
systemctl start mongod
# 等待 STARTUP2 → SECONDARY

预防:oplog 至少能覆盖 24~72 小时的写入量;监控 rs.printReplicationInfo() 的窗口长度并设置告警。

5.7.5 磁盘空间与 journal

# 1. 查看磁盘占用
df -h /opt/truplatform/mongodb/data/db
du -sh /opt/truplatform/mongodb/data/db/*

# 2. journal 目录占用
du -sh /opt/truplatform/mongodb/data/db/journal

# 3. 日志文件是否过大
ls -lh /opt/truplatform/mongodb/logs/
问题 原因 解决
磁盘写满导致 mongod 崩溃 数据/journal/日志增长 紧急:清理 mongod.log(用 logRotate 而非 rm,rm 后进程仍持有 inode);长期:扩容、开启压缩、回收碎片
删数据后空间不降 WiredTiger 保留空洞 compact 或滚动初始同步(见 5.5.8)
journal 目录巨大 checkpoint 间隔长、写入量大 正常现象(默认最多 ~2GB);确认 journalCompressor 已开启
关闭 journal 后崩溃丢数据 journal.enabled: false 生产必须开启 journal,这是崩溃恢复的唯一保障
WT_CACHE_FULL / 写入卡顿 cache 脏页过多,磁盘跟不上 提升磁盘 IOPS(换 SSD)、降低写入速率、调大 cacheSizeGB

重要:journal 与数据文件必须在同一个卷上,否则文件系统快照无法保证一致性。

5.7.6 锁与慢操作

// 1. 查看锁等待队列
db.serverStatus().globalLock.currentQueue
{ "total" : 45, "readers" : 3, "writers" : 42 }     // writers 42 说明写严重排队

// 2. 找出长时间运行的操作
db.currentOp({"active": true, "secs_running": {"$gt": 5}})

// 3. 找出正在等锁的操作
db.currentOp({"waitingForLock": true})

// 4. 杀掉罪魁祸首
db.killOp(<opid>)

// 5. 查看 WiredTiger 并发票据(降到 0 说明引擎饱和)
db.serverStatus().wiredTiger.concurrentTransactions
{
  "write": {"out": 128, "available": 0, "totalTickets": 128},   // 写票据耗尽!
  "read":  {"out": 12,  "available": 116, "totalTickets": 128}
}

常见慢操作根因:

根因 识别特征 解决
缺索引导致全表扫描 planSummary: COLLSCAN、docsExamined >> nreturned 建索引
内存排序 hasSortStage: true、metrics.operation.scanAndOrder 高 为排序字段建索引
深度 skip 分页 skip 值很大 改游标分页
大批量 updateMany / deleteMany 单操作 secs_running 很长,写队列堆积 拆成小批次循环执行,中间加 sleep
前台建索引 msg: Index Build 且阻塞其他操作 用 background: true(4.2+ 已默认非阻塞)
集合扫描型聚合 管道第一阶段不是 $match 把 $match 提到最前,并确保有索引
工作集超内存 pages read into cache 持续高、磁盘读 IO 高 加内存或加大 cacheSizeGB;优化 schema 减小文档

5.7.7 mongod 启动失败的日志定位

# 第一步永远是看日志最后几十行
tail -100 /opt/truplatform/mongodb/logs/mongod.log

# 前台启动,错误直接打屏(最有效的排查方式)
mongod -f /opt/truplatform/mongodb/etc/mongod.conf --nofork

# systemd 环境下看服务日志
systemctl status mongod -l
journalctl -u mongod -n 100 --no-pager
日志关键字 原因 解决
Permission denied / Unable to create/open lock file dbPath / logpath / keyFile 属主或权限不对 chown -R mongod:mongod;keyFile 设 400
Address already in use 端口被占用(可能是残留进程) netstat -tunlp \| grep 27017 后 kill 残留进程
Unclean shutdown detected / Detected unclean shutdown 上次非正常退出,正在恢复 通常自动恢复;若失败则 mongod --repair
wiredTiger.wt: handle-open: No such file / WT_PANIC 数据文件损坏 从备份恢复或做初始同步;--repair 抢救
Insufficient free space 磁盘空间不足 清理或扩容
BadValue: security.keyFile is required when authorization is enabled with replSet 开了 auth 但副本集无 keyFile 配置 keyFile
Failed to set up listener: SocketException: Cannot assign requested address bindIp 写了本机不存在的 IP 修正 bindIp
不能以 root 运行 类警告 以 root 启动 改用 mongod 用户
exception in initAndListen: ... terminating 通用致命错误,需看前后完整堆栈 按具体异常处理
Cannot start server. Detected data files in ... created by the 'xxx' storage engine 存储引擎与数据文件不匹配 改回原引擎,或清空 dbPath 重新同步
This node was not started with the replSetName 配置中 replSet 名与副本集配置不符 统一 replSetName

5.8 其他 NoSQL 数据库

5.8.1 Redis

Redis 是一个 key-value 存储系统,即键值对非关系型数据库,和 Memcached 类似,目前被越来越多的互联网公司采用。

  • Redis 是一个高性能的 key-value 数据库。它的出现在很大程度上补偿了 Memcached 这类 key-value 存储的不足,在部分场合可以对关系数据库起到很好的补充作用。
  • Redis 提供了 Python、Ruby、Erlang、PHP 等客户端,使用方便。

Redis 支持存储的 value 类型包括:

类型 说明
string 字符串
list 链表
set 集合
zset 有序集合
hash 哈希表

这些数据类型都支持 push/pop、add/remove 以及取交集、并集、差集等丰富操作,而且这些操作都是原子性的。在此基础上,Redis 支持各种不同方式的排序。

与 Memcached 一样,为保证效率,数据都缓存在内存中。区别在于:

  • Redis 会周期性地把更新的数据写入磁盘(RDB 快照),或把修改操作写入追加的记录文件(AOF);
  • 在此基础上实现了 master-slave(主从)同步。

安装与启动

实验软件包:redis-3.2.7.tar.gz。

解压到 /usr/local/redis 并编译安装:

cd /usr/local/redis
make
make install

启动服务端:

cd /usr/local/redis
./src/redis-server

另开一个新窗口,启动客户端:

cd /usr/local/redis
./src/redis-cli

客户端连上服务器后,会显示 127.0.0.1:6379> 提示符,表示服务器 IP 为 127.0.0.1、端口为 6379。

简单读写示例

设置键为 hello、值为 world,再取出该键对应的值:

127.0.0.1:6379> set hello world
OK
127.0.0.1:6379> get hello
"world"

Redis 的表建模方式

Redis 的处理方式与关系型数据库不同。对于表之间存在关联的数据,可采用如下键值形式:

Key   = 表名:主键值:列名
Value = 列值

创建

set Student:95001:Sname 李勇
set Course:1:Cname 数据库
set SC:95001:1:Grade 92

插入

Redis 插入一条数据,只需先设计好键值,然后用 set 命令存入即可。

例如在课程表中插入新课程"算法"(4 学分):

set Course:8:Cname 算法
set Course:8:Ccredit 4

修改

Redis 没有专门的修改命令。要修改一条数据,只能在使用 set 时使用相同的键,用新的 value 覆盖旧数据。

例如将新增课程的名字由"算法"改为"编译原理"(讲义示例中旧值记为 old name,新值记为 newName):

127.0.0.1:6379> get Course:8:Cname
"old name"
127.0.0.1:6379> set Course:8:Cname newName
OK
127.0.0.1:6379> get Course:8:Cname
"newName"

删除

Redis 有专门删除数据的命令 del,用法为 del key。

要删除之前新增的课程"编译原理",需同时删除课程名与学分两个键:

127.0.0.1:6379> get Course:8:Cname
"newName"
127.0.0.1:6379> del Course:8:Cname
(integer) 1
127.0.0.1:6379> get Course:8:Cname
(nil)
127.0.0.1:6379> del Course:8:Ccredit
(integer) 1

输入 del Course:8:Cname 时返回 1,说明成功删除一条数据;再次 get 输出为空(nil),说明删除成功。

查询

最简单的查询方式是使用 get 命令:

127.0.0.1:6379> get SC:95001:1:Grade
"92"

如果要进行关系型数据库中的表连接查询,则需要多步查询。这时要先将 (姓名, 学号) 和 (课程名, 课程号) 等键值对存入数据库,用于后续查询。

MongoDB 与 Redis 操作对比

操作 MongoDB Redis
进入客户端 ./mongo ./src/redis-cli
切换/创建库 use 学生系统 无需建库,直接用键(select 0~15 切换 DB 编号)
插入 db.Student.insert(data) set key value
查询 db.Student.find(...) get key
修改 db.Student.update(...,{$set:{...}}) 用 set 同名键覆盖
删除 db.Student.remove(...) del key
建模方式 文档(BSON),自动建集合 键值对 表名:主键:列名

5.8.2 Memcached

Memcached 是一个高性能的分布式内存对象缓存系统,最初为动态 Web 应用减轻数据库负载而开发。它是纯内存的键值缓存,不支持持久化。

安装

# ---- yum 安装(CentOS 6/7 通用,需 EPEL 源)----
yum install -y epel-release
yum install -y memcached libmemcached

# ---- 配置文件 /etc/sysconfig/memcached ----
cat > /etc/sysconfig/memcached <<'EOF'
PORT="11211"
USER="memcached"
MAXCONN="4096"            # 最大并发连接数
CACHESIZE="1024"          # 分配的内存大小(MB)
OPTIONS="-l 127.0.0.1"    # 只监听本机,避免公网暴露
EOF

# ---- 启动 ----
# CentOS 7
systemctl start memcached
systemctl enable memcached
# CentOS 6
service memcached start
chkconfig memcached on

# ---- 源码编译安装 ----
yum install -y libevent libevent-devel gcc make
tar -zxvf memcached-1.6.9.tar.gz && cd memcached-1.6.9
./configure --prefix=/usr/local/memcached
make && make install
/usr/local/memcached/bin/memcached -d -m 1024 -u memcached -l 127.0.0.1 -p 11211 -c 4096 -P /var/run/memcached.pid

启动参数说明:

参数 说明
-d 以守护进程方式运行
-m <MB> 分配给 Memcached 的内存大小
-u <user> 运行用户(不能用 root,除非加 -u root)
-l <ip> 监听地址
-p <port> 监听端口,默认 11211
-c <num> 最大并发连接数,默认 1024
-P <file> pid 文件路径
-t <num> 工作线程数,默认 4
-f <factor> slab 增长因子,默认 1.25
-n <bytes> 最小分配空间,默认 48 字节
-M 内存耗尽时返回错误而非剔除数据
-vv 输出详细调试信息

常用命令(通过 telnet 或 nc 使用文本协议)

telnet 127.0.0.1 11211
# 或
nc 127.0.0.1 11211
# ---- 存储类命令:<command> <key> <flags> <exptime> <bytes> ----
set name 0 3600 5          # 无条件设置,过期时间 3600 秒,值长度 5 字节
world
STORED

add city 0 0 7             # 仅当 key 不存在时添加(0 表示永不过期)
Beijing
STORED

replace name 0 3600 5      # 仅当 key 存在时替换
redis
STORED

append name 0 3600 3       # 向已有值追加
abc
STORED

prepend name 0 3600 3      # 向已有值前置
xyz
STORED

cas name 0 3600 5 12345    # 检查并设置(乐观锁,需先 gets 拿到版本号)

# ---- 读取类命令 ----
get name                   # 获取单个
VALUE name 0 5
world
END

get name city              # 批量获取
gets name                  # 获取值 + CAS 版本号

# ---- 删除与计数 ----
delete name                # 删除
DELETED

set num 0 0 1
5
incr num 3                 # 自增(值必须是数字)
8
decr num 2                 # 自减
6

# ---- 管理命令 ----
stats                      # 查看统计信息
stats items                # 各 slab 的 item 统计
stats slabs                # slab 分配情况
stats sizes                # 对象大小分布
flush_all                  # 清空所有缓存(危险)
version                    # 查看版本
verbosity 1                # 设置日志级别
quit                       # 退出连接
# 命令行一行式操作
echo -e "set foo 0 0 3\r\nbar\r" | nc 127.0.0.1 11211
echo -e "get foo\r" | nc 127.0.0.1 11211
echo -e "stats\r" | nc 127.0.0.1 11211 | grep -E "curr_items|bytes|get_hits|get_misses"
memcached-tool 127.0.0.1:11211 stats      # 更友好的统计输出
memcached-tool 127.0.0.1:11211 display    # 查看 slab 分布

stats 关键指标:curr_connections(当前连接)、curr_items(当前对象数)、bytes(已用内存)、limit_maxbytes(内存上限)、get_hits / get_misses(命中/未命中,用于算命中率)、evictions(因内存不足被剔除的对象数,持续增长说明内存不够)、cmd_get / cmd_set。

Memcached 与 Redis 对比

维度 Memcached Redis
数据类型 只有字符串(key-value) string、list、set、zset、hash、bitmap、HyperLogLog、Stream、GEO
持久化 不支持,重启数据全丢 支持 RDB 快照 + AOF 日志
主从复制 不支持(需第三方方案) 原生支持 master-slave 复制
高可用 无内置方案 Sentinel(哨兵)自动故障转移
集群/分片 客户端一致性哈希分片 原生 Redis Cluster(3.0+)
单键值大小 默认 1MB 512MB
线程模型 多线程(多核利用率高) 单线程处理命令(6.0 起 IO 多线程),避免锁开销
内存管理 slab 预分配,可能有内部碎片浪费 按需分配(jemalloc),支持多种淘汰策略
淘汰策略 LRU 8 种:noeviction、allkeys-lru、volatile-lru、allkeys-lfu 等
事务 不支持 支持 MULTI/EXEC、Lua 脚本原子执行
发布订阅 不支持 支持 Pub/Sub
适用场景 纯粹的分布式内存缓存,简单、高吞吐 缓存 + 轻量数据库 + 消息队列 + 排行榜 + 分布式锁

结论:Redis 是"强化版的 Memcached"。新项目一般直接选 Redis;Memcached 仅在超大规模纯缓存、需要极致多核吞吐的场景仍有价值。

5.8.3 HBase

HBase 是构建在 HDFS 之上的分布式列族数据库,Google BigTable 的开源实现,属于 CP 型系统。适用于超大规模(PB 级)稀疏表存储、随机实时读写,以及与 Hadoop / Spark 生态的离线分析结合。

详见 16-05-HBase分布式数据库.md

5.8.4 Cassandra

Cassandra 由 Facebook 开发并开源(现属 Apache),融合了 Google BigTable 的列族数据模型与 Amazon Dynamo 的无中心(P2P)架构,是 AP 型系统的典型代表。

核心特点:

  • 无中心架构:所有节点地位平等,无单点故障,任意节点都可接收读写请求(通过一致性哈希环定位数据)。
  • 可调一致性:写/读时可分别指定一致性级别(ONE、QUORUM、ALL、LOCAL_QUORUM、EACH_QUORUM、ANY),实现 N/W/R 的自由权衡。
  • 线性可扩展:加节点即线性提升吞吐,运维简单。
  • 多数据中心复制:原生支持跨机房、跨地域部署,是全球化部署的首选。
  • CQL(Cassandra Query Language):类 SQL 语法,降低学习成本,但不支持 JOIN 与子查询。
  • 写优化:基于 LSM-Tree(Commit Log + MemTable + SSTable),写入极快。

适用场景:时序数据、IoT 传感器数据、消息/日志存储、需要多机房强可用的写密集型业务。使用者包括 eBay、Instagram、NASA、Twitter、Netflix、Apple。

不适用:需要强一致事务、复杂即时查询/聚合分析的场景。

5.8.5 Neo4j

Neo4j 是最流行的图数据库,用属性图模型(节点 Node + 关系 Relationship + 属性 Property + 标签 Label)直接存储和查询高度关联的数据。

核心特点:

  • 免索引邻接(Index-free Adjacency):每个节点直接持有其相邻关系的物理指针,遍历 N 跳关系的代价与图的总规模无关——这是关系数据库多表 JOIN 无法企及的地方。
  • Cypher 查询语言:声明式、图形化的模式匹配语法,直观易读。
  • 完整 ACID 事务:这在 NoSQL 中较为罕见。
  • 缺点:复杂性高,水平扩展能力有限(图天然难以分片),只能支持一定的数据规模。
// Cypher 示例:创建节点与关系
CREATE (s:Student {sno: 95001, name: '李勇', dept: 'CS'})
CREATE (c:Course  {cno: 1, name: '数据库', credit: 4})
CREATE (s)-[:SELECTED {grade: 92}]->(c)

// 查询:李勇选了哪些课,成绩多少
MATCH (s:Student {name: '李勇'})-[r:SELECTED]->(c:Course)
RETURN s.name, c.name, r.grade
ORDER BY r.grade DESC

// 图特长:查找"朋友的朋友"(二跳关系),SQL 需要多层自连接
MATCH (a:Person {name:'张三'})-[:FRIEND*2]->(fof:Person)
WHERE NOT (a)-[:FRIEND]->(fof) AND a <> fof
RETURN DISTINCT fof.name AS 可能认识的人

// 最短路径
MATCH p = shortestPath((a:Person {name:'张三'})-[:FRIEND*]-(b:Person {name:'李四'}))
RETURN p, length(p)

适用场景:社交网络(好友推荐)、知识图谱、金融反欺诈(关联团伙识别)、推荐系统、IT 拓扑与依赖分析、路径规划、权限继承计算。使用者包括 Adobe、Cisco、T-Mobile、eBay。

其他图数据库:OrientDB、InfoGrid、Infinite Graph、GraphDB、JanusGraph、阿里 GraphScope。

5.9 本章小结

  • NoSQL 数据库较好地满足了大数据时代各种非结构化数据的存储需求,开始得到越来越广泛的应用。但需要指出的是,传统关系数据库和 NoSQL 数据库各有所长,彼此都有各自的市场空间,不存在一方完全取代另一方的问题,在很长一段时期内二者都会共同存在,满足不同应用的差异化需求。
  • NoSQL 数据库主要包括键值数据库、列族数据库、文档数据库和图形数据库四种类型,不同产品各有应用场合。CAP、BASE 和最终一致性是 NoSQL 数据库的三大理论基石,是理解 NoSQL 数据库的基础。
  • NewSQL 数据库融合了传统关系数据库和 NoSQL 的优点,既保留 SQL 与 ACID,又具备水平扩展能力。
  • 本文以具有代表性的文档数据库 MongoDB 为主线,完整覆盖了安装部署、配置详解、CRUD、索引、聚合、副本集、分片集群、安全认证、备份恢复、监控排错等全流程运维内容。
  • 工程实践的三条铁律:开启认证、限制 bindIp、定期备份并演练恢复。

5.10 练习

一、选择题

1. 以下哪一项不是 NoSQL 数据库的典型特点? A. 灵活的可扩展性 B. 灵活的数据模型 C. 严格遵守 ACID 事务 D. 与云计算紧密融合

2. 按照 CAP 理论,传统关系数据库(如 MySQL 单机)通常属于哪一类? A. CA B. CP C. AP D. 三者都满足

3. HBase 属于 NoSQL 四大类型中的哪一类? A. 键值数据库 B. 列族数据库 C. 文档数据库 D. 图形数据库

4. 在 N/W/R 模型中,N=3, W=2, R=2 属于: A. 强一致性 B. 弱一致性 C. 无法判断 D. 最终一致性

5. MongoDB 中与关系数据库"表"对应的概念是: A. database B. collection C. document D. field

6. 一个支持自动故障转移的 MongoDB 副本集,投票成员数应为: A. 任意数 B. 偶数 C. 奇数 D. 必须为 3

7. 副本集中不保存数据、只参与投票的成员称为: A. Primary B. Secondary C. Arbiter D. Hidden

8. 执行 explain("executionStats") 后,winningPlan.stage 为 COLLSCAN 说明: A. 走了索引,性能良好 B. 全表扫描,应考虑建索引 C. 用了覆盖索引 D. 查询被优化器拒绝

9. 需要保证写入不会被回滚,writeConcern 应设为: A. w: 0 B. w: 1 C. w: "majority" D. j: false

10. 从节点长期处于 RECOVERING 且无法自愈,最常见的原因是: A. 磁盘满 B. oplog 太小导致节点脱队 C. 索引缺失 D. 防火墙未放通

二、填空题

  1. NoSQL 的三大理论基石是 ____、____ 和 ____。
  2. CAP 分别代表 ____、____、____。
  3. BASE 的含义是 ____、____、____。
  4. MongoDB 的数据以 ____ 二进制格式存储,客户端进程名为 ____,服务端进程名为 ____。
  5. MongoDB 的 _id 默认为 ____ 类型,长度 ____ 字节,其前 4 字节表示 ____。
  6. 副本集复制依赖 local 库中的固定集合 ____,它必须具备 ____ 性以支持重复重放。
  7. 分片集群由三部分组成:____(路由)、____(元数据)和 ____(数据)。
  8. 数据块迁移的最小单位叫 ____,默认大小为 ____ MB。
  9. 慢查询分析器有三个级别:0 表示 ____,1 表示 ____,2 表示 ____。
  10. 逻辑备份工具为 ____ / ____,数据交换工具为 ____ / ____。

三、命令与操作题

  1. 写出在 CentOS 7 上配置 MongoDB 4.4 官方 yum 源并安装、启动、设置开机自启的完整命令。
  2. 写出一份包含 systemLog、storage、net、processManagement、replication 五大段落的 YAML 配置文件,副本集名为 myreplset,端口 27017,绑定 127.0.0.1,192.168.1.220。
  3. 向 学生系统 库的 Student 集合批量插入 4 条学生记录(Sno、Sname、Ssex、Sage、Sdept)。
  4. 查询 IS 系且年龄小于 20 的学生,只返回姓名和年龄,按年龄降序,取前 2 条。
  5. 把学号 95001 的学生年龄改为 22,并给他新增一个 tags 数组字段追加 "优秀",要求一条命令完成。
  6. 为 SC 集合建立 (Sno, Cno) 复合唯一索引,并用后台方式创建。
  7. 用聚合管道统计每个系的人数与平均年龄,并按人数降序输出。
  8. 用聚合管道关联 SC、Student、Course 三个集合,输出"姓名、课程名、成绩"。
  9. 写出三节点(含一个 Arbiter)副本集的 rs.initiate() 配置对象,要求 220 优先级最高。
  10. 把某个 Secondary 改为延迟 1 小时的隐藏节点,写出完整的 rs.conf() → rs.reconfig() 命令序列。
  11. 写出创建 admin 管理员(userAdminAnyDatabase + clusterAdmin)和业务读写用户的命令。
  12. 用 openssl 生成 keyFile、设置权限、分发到三个节点的完整命令。
  13. 写出带认证、压缩归档、从 secondary 读取、包含 oplog 的全库备份命令,以及对应的恢复命令。
  14. 写出把 Student 集合导出为 CSV(字段 Sno,Sname,Sage)并再导入到另一实例的命令。
  15. 开启慢查询(阈值 100ms),并查出最慢的 5 条操作及其执行计划。
  16. 找出当前运行超过 5 秒的操作并杀掉它。
  17. 分片集群中对 学生系统.Student 按 Sno 做哈希分片,写出从 sh.enableSharding 到 sh.status 的完整命令序列。
  18. 写一个定时全量备份脚本,要求:备份到 /backup/mongodb、压缩归档、保留 7 天、失败时退出码非 0。

四、简答与分析题

  1. 为什么关系数据库无法满足 Web 2.0 的需求?请从海量数据、高并发、可扩展性三方面说明,并解释为何 MySQL 集群不能完全解决这些问题。
  2. 请证明"分布式系统无法同时满足 CAP 三者",并分别举出 CA、CP、AP 型系统的代表产品。
  3. 比较 ACID 与 BASE,说明为什么互联网业务大多选择 BASE。
  4. 列举并解释五种数据一致性模型,并说明 DNS 属于哪一种。
  5. 说明 MongoDB 副本集的选举机制,为什么投票成员必须为奇数?4 个成员相比 3 个成员容错能力有提升吗?为什么?
  6. oplog 的作用是什么?为什么它必须是幂等的?oplog 太小会导致什么后果,如何修复与预防?
  7. 解释 readPreference 的五种取值,并说明"写后立刻读"场景应如何配置才能保证读到最新数据。
  8. writeConcern 的 w、j、wtimeout 分别控制什么?为什么 w > 1 时必须设置 wtimeout?
  9. 分片键的选择应遵循哪些原则?为什么用 ObjectId 或时间戳做范围分片键是糟糕的选择?哈希分片如何化解这个问题,代价是什么?
  10. 说明范围分片与哈希分片各自的优缺点及适用场景,什么是 jumbo chunk?
  11. 结合 2017 年 MongoDB 勒索事件,分析未授权访问的成因,并列出至少 8 条加固措施。
  12. mongodump 与 mongoexport 有何区别?为什么后者不能作为备份手段?
  13. 说明如何利用全量备份 + oplog 实现时间点恢复(PITR),写出关键命令与参数。
  14. 解释 explain 输出中 nReturned、totalKeysExamined、totalDocsExamined 三者的关系,什么样的比例说明查询健康?
  15. 什么是覆盖索引?满足覆盖索引需要哪些条件?
  16. 深翻分页为什么慢?请给出游标分页的实现方案。
  17. schema 设计中"嵌入"与"引用"应如何取舍?分别举一个恰当的例子。
  18. WiredTiger 的 cacheSizeGB 应如何设置?为什么容器环境必须显式配置?
  19. MongoDB 删除数据后空间为何不释放?列举三种回收方案并比较其对业务的影响,说明生产环境的最佳实践。
  20. 比较 Memcached 与 Redis 在数据类型、持久化、高可用、线程模型四个方面的差异,并说明各自的适用场景。
  21. Cassandra 为什么被归为 AP 型系统?它的"可调一致性"如何体现 N/W/R 模型?
  22. 图数据库的"免索引邻接"特性是什么?为什么它在多跳关系查询上远优于关系数据库的 JOIN?
  23. 列出 CentOS 6 与 CentOS 7 在服务管理、防火墙、资源限制配置上的差异,并给出 MongoDB 在两个版本上的对应操作命令。
  24. MongoDB 启动失败时,你的排查步骤是什么?请按优先顺序列出,并说明如何让错误直接打印到屏幕。

五、参考答案

一、选择题

题号 1 2 3 4 5 6 7 8 9 10
答案 C A B A B C C B C B

答案要点:

  1. C。NoSQL 大多放松 ACID,遵守 BASE 模型,只保证最终一致性。
  2. A。传统关系数据库以一致性和可用性为优先,放弃分区容忍性,因此扩展性差。
  3. B。HBase 是 Google BigTable 的开源实现,属列族数据库。
  4. A。W + R = 4 > N = 3,读写节点必然重叠,为强一致性。
  5. B。table ↔ collection。
  6. C。奇数才能在选举时形成明确的多数派;偶数会浪费一个成员的成本。
  7. C。Arbiter 不保有数据、不能被选为主,只投票。
  8. B。COLLSCAN 即全表扫描,应为查询条件建立索引。
  9. C。w: "majority" 保证写入被多数派确认,不会被回滚。
  10. B。oplog 被覆盖后从节点 too stale to catch up,无法自愈,需重做初始同步。

二、填空题

  1. CAP、BASE、最终一致性
  2. 一致性(Consistency)、可用性(Availability)、分区容忍性(Tolerance of Network Partition)
  3. 基本可用(Basically Available)、软状态(Soft-state)、最终一致性(Eventual consistency)
  4. BSON;mongo(4.4+ 为 mongosh);mongod
  5. ObjectId;12;Unix 时间戳(秒)
  6. local.oplog.rs;幂等
  7. mongos;config server(CSRS 副本集);shard(每个为副本集)
  8. chunk;64
  9. 关闭不记录;只记录慢操作(超过 slowms);记录所有操作
  10. mongodump / mongorestore;mongoexport / mongoimport

三、命令与操作题(参考答案)

第 1、2 题参见 2.5.1 与 2.6 节;第 12 题参见 5.2.1 节;第 13 题参见 5.4.4 节;第 17 题参见 4.11.6 节;第 18 题参见 5.4.8 节。其余关键答案:

// 3.
use 学生系统
db.Student.insertMany([
  {Sno:95001, Sname:"李勇", Ssex:"男", Sage:20, Sdept:"CS"},
  {Sno:95002, Sname:"刘晨", Ssex:"女", Sage:19, Sdept:"IS"},
  {Sno:95003, Sname:"王敏", Ssex:"女", Sage:18, Sdept:"MA"},
  {Sno:95004, Sname:"张立", Ssex:"男", Sage:19, Sdept:"IS"}
])

// 4.
db.Student.find({Sdept:"IS", Sage:{$lt:20}}, {Sname:1, Sage:1, _id:0})
          .sort({Sage:-1}).limit(2)

// 5.
db.Student.updateOne({Sno:95001},
  {$set:{Sage:22}, $push:{tags:"优秀"}})

// 6.
db.SC.createIndex({Sno:1, Cno:1}, {unique:true, background:true})

// 7.
db.Student.aggregate([
  {$group:{_id:"$Sdept", 人数:{$sum:1}, 平均年龄:{$avg:"$Sage"}}},
  {$sort:{人数:-1}}
])

// 8.
db.SC.aggregate([
  {$lookup:{from:"Student", localField:"Sno", foreignField:"Sno", as:"stu"}},
  {$lookup:{from:"Course",  localField:"Cno", foreignField:"Cno", as:"crs"}},
  {$unwind:"$stu"}, {$unwind:"$crs"},
  {$project:{_id:0, 姓名:"$stu.Sname", 课程名:"$crs.Cname", 成绩:"$Grade"}},
  {$sort:{成绩:-1}}
])

// 9.
rs.initiate({
  _id: "myreplset",
  members: [
    {_id:0, host:"192.168.1.220:27017", priority:2},
    {_id:1, host:"192.168.1.221:27017", priority:1},
    {_id:2, host:"192.168.1.222:27018", arbiterOnly:true}
  ]
})

// 10.
cfg = rs.conf()
cfg.members[2].priority   = 0
cfg.members[2].hidden     = true
cfg.members[2].slaveDelay = 3600     // 4.2+ 用 secondaryDelaySecs
rs.reconfig(cfg)
rs.conf()

// 11.
use admin
db.createUser({user:"admin", pwd:"JWIServer27017",
  roles:[{role:"userAdminAnyDatabase", db:"admin"},
         {role:"clusterAdmin", db:"admin"}]})
use 学生系统
db.createUser({user:"appuser", pwd:"AppPwd@123",
  roles:[{role:"readWrite", db:"学生系统"}]})

// 15.
db.setProfilingLevel(1, {slowms: 100})
db.system.profile.find().sort({millis:-1}).limit(5).pretty()

// 16.
db.currentOp({"active":true, "secs_running":{"$gt":5}})
  .inprog.forEach(function(op){ db.killOp(op.opid) })
# 14.
mongoexport -h 192.168.1.220 --port 27017 -d 学生系统 -c Student \
            --type=csv --fields Sno,Sname,Sage -o /tmp/Student.csv
mongoimport -h 192.168.1.230 --port 27017 -d 学生系统 -c Student \
            --type=csv --headerline --file /tmp/Student.csv

四、简答与分析题:答案分别对应正文以下小节,可直接回顾。

题号 参考小节 题号 参考小节
1 1.2.1 13 5.4.7
2 1.5.1 14 3.6(2)
3 1.5.2 15 3.6(2)(3)
4 1.5.3 16 5.6 深翻分页优化
5 4.2.3 17 5.6 schema 设计
6 4.2.2、5.7.4 18 5.5.7
7 4.8 19 5.5.8
8 4.9 20 5.8.2 对比表
9 4.11.3、4.11.4 21 5.8.4、1.5.3
10 4.11.4、4.11.5 22 5.8.5
11 5.3.2 23 2.7、2.8、2.9
12 5.4.5 24 5.7.7

六、使用 Java 程序访问 MongoDB

除 mongo shell 与各语言驱动外,MongoDB 官方提供 Java 驱动。MongoDB 支持 Ruby、Python、Java、C++、PHP、C# 等多种语言。

版本提示:本节代码来自原讲义,使用的是 MongoDB Java 驱动 2.x / 3.x 的旧版 API(MongoClient(host, port)、DB、DBCollection、BasicDBObject、db.authenticate())。这套 API 在 4.x 驱动中已被移除,现代写法改用 MongoClients.create(connectionString) 取得 MongoClient,用 MongoDatabase / MongoCollection<Document> 替代 DB / DBCollection,用 org.bson.Document 替代 BasicDBObject,认证信息写在连接串里(mongodb://user:pass@host:27017/db)而不再调用 authenticate()。原文保留以对应讲义与旧项目代码,新项目请使用新 API。

6.1 环境配置

在 Java 程序中使用 MongoDB,需要确保已经安装 Java 环境及 MongoDB 驱动。下载 mongo jar 包(https://github.com/mongodb/mongo-java-driver),并将 mongo.jar 包含在 classpath 中:

# 编译与运行时都要带上驱动 jar
javac -cp .:mongo.jar MongoDBJDBC.java
java  -cp .:mongo.jar MongoDBJDBC

Maven 项目改用依赖坐标即可,无需手工管理 jar:

<!-- 旧版驱动(对应本节代码) -->
<dependency>
    <groupId>org.mongodb</groupId>
    <artifactId>mongo-java-driver</artifactId>
    <version>3.12.11</version>
</dependency>

6.2 连接数据库

import com.mongodb.MongoClient;
// 这里省略其他需要导入的包

public class MongoDBJDBC {
    public static void main(String args[]) {
        try {
            // 连接到 mongodb 服务
            MongoClient mongoClient = new MongoClient("localhost", 27017);
            // 连接到数据库
            DB db = mongoClient.getDB("test");
            System.out.println("Connect to database successfully");
            boolean auth = db.authenticate(myUserName, myPassword);
            System.out.println("Authentication: " + auth);
        } catch (Exception e) {
            System.err.println(e.getClass().getName() + ": " + e.getMessage());
        }
    }
}

若 MongoDB 未开启认证(见「五、安全、运维与其他 NoSQL」中的安全加固部分),authenticate() 会失败或不必调用;生产环境务必开启认证并绑定内网地址。

6.3 创建集合

可以使用 com.mongodb.DB 类中的 createCollection() 来创建集合:

public class MongoDBJDBC {
    public static void main(String args[]) {
        try {
            MongoClient mongoClient = new MongoClient("localhost", 27017);
            DB db = mongoClient.getDB("test");
            System.out.println("Connect to database successfully");
            boolean auth = db.authenticate(myUserName, myPassword);
            System.out.println("Authentication: " + auth);
            DBCollection coll = db.createCollection("mycol");
            System.out.println("Collection created successfully");
        } catch (Exception e) {
            System.err.println(e.getClass().getName() + ": " + e.getMessage());
        }
    }
}

6.4 插入文档

可以使用 com.mongodb.DBCollection 类的 insert() 方法插入一个文档:

public class MongoDBJDBC {
    public static void main(String args[]) {
        try {
            MongoClient mongoClient = new MongoClient("localhost", 27017);
            DB db = mongoClient.getDB("test");
            System.out.println("Connect to database successfully");
            boolean auth = db.authenticate(myUserName, myPassword);
            System.out.println("Authentication: " + auth);
            DBCollection coll = db.getCollection("mycol");
            System.out.println("Collection mycol selected successfully");
            BasicDBObject doc = new BasicDBObject("title", "MongoDB")
                .append("description", "database")
                .append("likes", 100)
                .append("url", "http://www.w3cschool.cc/mongodb/")
                .append("by", "w3cschool.cc");
            coll.insert(doc);
            System.out.println("Document inserted successfully");
        } catch (Exception e) {
            System.err.println(e.getClass().getName() + ": " + e.getMessage());
        }
    }
}

6.5 旧新 API 对照

用途 旧 API(2.x / 3.x,本节代码) 新 API(4.x 及以后)
创建客户端 new MongoClient("localhost", 27017) MongoClients.create("mongodb://localhost:27017")
取得库 DB db = client.getDB("test") MongoDatabase db = client.getDatabase("test")
取得集合 DBCollection coll = db.getCollection("mycol") MongoCollection<Document> coll = db.getCollection("mycol")
文档对象 BasicDBObject org.bson.Document
插入 coll.insert(doc) coll.insertOne(doc)
认证 db.authenticate(user, pass) 写在连接串或用 MongoCredential

附:相关文档

  • > 详见 11-09-云数据库.md —— 云数据库与 DBaaS
  • > 详见 16-05-HBase分布式数据库.md —— HBase 与 Hadoop 生态、MapReduce
最后修改:2026 年 09 月 20 日
如果觉得我的文章对你有用,请随意赞赏