/

MongoDB 高手课

04 特色及优势

对象模型,快速响应业务变化:

  • 多形性:同一个集合中可以包含不同字段(类型)的文档对象。
  • 动态性:线上修改数据模式,修改是应用与数据库均无须下线。
  • 数据治理:支持使用JSONSchema 来规范数据模式。在保证模式灵活动态的前提下,提供数据治理能力。

快速的开发:

  • 只存储在一个存储区读写。
  • 反范式、无关联的组织极大优化查询速度。
  • 程序 API 自然,开发速度快。

原生的高可用:

  • Replica Set - 2 to 50 个成员,建议单数。
  • 自恢复。
  • 多中心容灾能力。
  • 滚动服务,最小化服务终端。

横向扩展能力:

  • 需要的时候无缝扩展。
  • 应用全透明。
  • 多种数据分布策略。
  • 轻松支持 TB-PB 数量级。

06 基本操作

tar -xvf dump.tar.gz
mongorestore --uri="mongodb://root:root@10.130.0.12/?&authMechanism=SCRAM-SHA-1"
// 插入
db.fruit.insertOne({name: "apple"})
db.fruit.insertMany([
{name: "apple"},
{name: "pear"},
{name: "orange"},
])
// 查询
db.customers.find({username: "fmiller", name: "Elizabeth Ray"})
db.customers.find({username: /^f/})
db.customers.find({$or: [{username: /^f/}, {name: /^E/}]})
  • a <> 1 {a: {$ne: 1}}
  • a > 1 {a: {$gt: 1}}
  • a >= 1 {a: {$gte: 1}}
  • a < 1 {a: {$lt: 1}}
  • a <= 1 {a: {$lte: 1}}
  • a=1 OR b=1 {$or: [{a: 1}, {b: 1}]}
  • a IS NULL {a: {$exists: false}}
  • a IS (1,2,3) {a: {$in: [1, 2, 3]}}
// 同时满足
{$eleMatch: {"city": "Rome", "country": "USA"}}
// 投影 projection
// like select
db.customers.find({username: /^f/}, {name: 0, email: 0})
db.customers.find({username: /^f/}, {_id: 1, name: 1})
// remove
db.customers.remove({username: "abrown"})

// update
db.customers.updateOne({username: "fmiller"}, {$set: {from: "China"}})
db.customers.updateMany({username: "fmiller"}, {$set: {from: "China"}})
// $set $unset
// $push $pushAll $pop 数组操作
// $pull $pullAll 如果匹配,从数组中删除相应的对象
// $addToSet 如果不存在则增加一个值到数组

// drop
db.fruit.drop()
show collections

db
db.dropDatabase()
show dbs

08 聚合查询

Aggregation Framework

  • Pipeline
  • Stage
pipeline = [stage1, stage2, ...]
db.<collection>.aggregate(
pipeline,
{ option }
)
  • $match 过滤
  • $project 投影
  • $sort 排序
  • $group 分组
  • $skip $limit 结果限制
  • $lookup 左外连接

09 聚合查询实验

// 计算总合计
db.orders.aggregate([
{
$group: {
_id: null,
total: {$sum: "$total"}
}
}
])
// {
// "_id": null,
// "total": NumberDecimal("44019609")
// }

// 查询 2019 第一季度,已完成订单(completed)总金额(金额+运费)和订单总数
db.orders.aggregate([
{
$match: {
status: "completed",
orderDate: { $gte: ISODate("2019-01-01"), $lt: ISODate("2019-04-01") }
}
},
{
$group: {
_id: null,
total: { $sum: "$total" },
shippingFee: { $sum: "$shippingFee" },
count: { $sum: 1 }
}
},
{
$project: {
grandTotal: { $add: ["$total", "$shippingFee"] },
count: 1,
_id: 0
}
}
])
// {
// "count": 5875,
// "grandTotal": NumberDecimal("2636376")
// }

10 复制集机制及原理

由3个以上具有投票权的节点组成:

  • 1个主节点 PRIMARY:接收写入操作和选举时投票。
  • 两个或多个从节点 SECONDARY:复制主节点上的新数据和选举时投票。

数据是如何复制的:

  • 当一个修改操作,无论是插入、更新或删除,到达主节点时它对数据的操作将被记录下来(经过些必要的转换),这些记录称为 oplog。
  • 从节点通过在主节点上打开一个 tailable 游标不断获取新进入主节点的 oplog,并在自己的数据上回放,以此保持跟主节点的数据一致。

通过选举完成故障恢复:

  • 具有投票权的节点之间两两互相发送心跳。
  • 当5次心跳未收到时判断为节点失联。
  • 如果失联的是主节点,从节点会发起选举,选出新的主节点。
  • 如果失联的是从节点则不会产生新的选举。
  • 选举基于RAFT一致性算法实现,选举成功的必要条件是大多数投票节点存活。
  • 复制集中最多可以有50个节点,但具有投票权的节点最多7个。

影响选举的因素:

  • 整个集群必须有大多数节点存活。
  • 被选举为主节点的节点必须:
    • 能够与多数节点建立连接
    • 具有较新的 oplog
    • 具有较高的优先级(如果有配置)

复制集节点有以下常见的选配项:

  • 是否具有投票权(v 参数):有则参与投票。
  • 优先级(priority 参数):优先级越高的节点越优先成为主节点。优先级为0的节点无法成为主节点。
  • 隐藏(hidden 参数):复制数据,但对应用不可见。隐藏节点可以具有投票仅,但优先级必须为0。
  • 延迟(slaveDelay 参数):复制 n 秒之前的数据,保持与主节点的时间差。

复制集注意事项:

  • 关于硬件:
    • 因为正常的复制集节点都有可能成为主节点,它们的地位是一样的,因此硬件配置上必须致;
    • 为了保证节点不会同时岩机,各节点使用的硬件必须具有独立性。
  • 关于软件:
    • 复制集各节点软件版本必须一致,以避免出现不可预知的问题。
  • 增加节点不会增加系统写性能!

11 搭建 MongoDB 复制集

mkdir -p runtime/data_db{1,2,3} && \
mkdir -p runtime/data_configdb{1,2,3}

hostname -f

rs.status()

12 全家桶

13 模型设计基础

  • Entity
  • Attribute
  • Relationship

概念模型 CDM -> 逻辑模型 LDM -> 物理模型 PDM
对象 -> 实体、属性、关系 -> 表结构、字段列表、主外建

14 JSON 文档模型设计

无模式的由来:可以省略无论建模的具体过程,物理模型可省。

设计原则:

  • 性能 Performance
  • 开发易用 Ease of Development

15 基础设计

集合、字段、基础形状 -> 引用及关联 -> 最终模式

业务需求及逻辑模型 –逻辑导向-> 基础建模 —> 集合、字段、基础形状

一个文档 16MB max.

内嵌为主。

16 工况细化

技术需求、读写比例、方式及数据 –技术导向-> 工况细化 —> 引用及关联

引用模式 $lookup:

db.contacts.aggregate([
$lookup: {
form: "groups",
localField: "groups_ids",
foreignField: "groups_id",
as: "groups",
}
])

使用引用方式:

  • 内嵌文档太大
  • 内嵌文档或数组元素频繁修改
  • 内嵌文档数组元素持续增长且没有封顶

使用引用的设计:

  • 没有主外键的检查
  • $lookup 只支持 left outer join
  • $lookup 的关系目标(from)不能是分片表

17 模式套用

经验和学习 –模式导向-> 套用设计模式 -> 优化的模型

时序数据,分桶设计:利用文档内嵌组,将一个时间段的数据聚合到一个文档里。大量减少文档数据量,大量减少索引占用空间。

18 设计模式集锦

大文档,很多字段,很多索引。列转行。列数据变化为数组。多语言多国家属性,类似字段需要建立很多索引。转化为数组,一个索引解决所有查询问题。

模型灵活了,如何管理文档不同版本?增加一个版本字段。schema_version。

统计网页点击流量。近似计算。if random(0,9)==0 increment by 10。

业绩排名、游戏排名等精确统计。消耗资源多,聚合计算时间长。用预聚合字段。模型中直接增加统计字段,每次更新数据时同时更新统计值。

19 写操作事务 writeConcern

write-concern | mongodb

{ w: <value>, j: <boolean>, wtimeout: <number> }
rs.status();
db.test.drop()
db.test.insert({count:2}, {writeConcern: {w: "majority"}})
db.test.insert({count:2}, {writeConcern: {w: 3}})
db.test.insert({count:2}, {writeConcern: {w: 1}})
// WriteResult({ "nInserted" : 1, "writeConcernError" : [ ] })

db.test.insert({count:2}, {writeConcern: {w: 4}})
// [Error] 100 - Not enough data-bearing nodes

db.test.find()
// 配置延迟节点,模拟网络延迟
conf=rs.conf()
// {
// "_id": 3,
// "host": "mongo3:27017",
// "arbiterOnly": false,
// "buildIndexes": true,
// "hidden": false,
// "priority": 1,
// "tags": { },
// "slaveDelay": NumberLong("0"),
// "votes": 1
// }
conf.members[2].slaveDelay=5
// 没有选举权
conf.members[2].priority=0
rs.reconfig(conf)
// {
// "_id": 3,
// "host": "mongo3:27017",
// "arbiterOnly": false,
// "buildIndexes": true,
// "hidden": false,
// "priority": 0,
// "tags": { },
// "slaveDelay": NumberLong("5"),
// "votes": 1
// }

db.test.insert({count:2}, {writeConcern: {w: 3}})
// Result Time 5s

// 3s 超时
db.test.insert({count:5}, {writeConcern: {w: 3, wtimeout:3000}})
// writeResult({
// "nInserted" : 1,
// "writeConcernError" : {
// "code" : 64,
// "codeName" : "WriteConcernFailed",
// "errmsg" : "waiting for replication timed out",
// "errInfo" : {
// "wtimeout" : true,
// "writeConcern" : {
// "w" : 3,
// "wtimeout" : 3000,
// "provenance" : "clientSupplied"
// }
// }
// }
// })

20 读操作事务 readPreference

配置:

  • mongodb://…/?replicaSet=rs&readPrefence=secondary
  • 通过驱动API,MongoCollection.withReadPrefence(ReadPrefence readPref)
  • Mongo Shell: db.collection.find({}).readPref(“secondary”)

实验:

// 主节点
db.test.drop()
db.test.insert({count:2}, {writeConcern: {w: 3}})
db.test.find({})
// 1 line
db.test.find({}).readPref("secondary")
// 1 line

// 从节点 1、2
db.test.find({})
// 1 line
db.fsyncLock()
// now locked against writes, use db.fsyncUnlock() to unlock 锁住写入

// 主节点
db.test.insert({count:3})
db.test.find({})
// 2 line
db.test.find().readPref("secondary")
// 1 line

// 从节点 1、2
db.fsyncUnlock()
// 主节点
db.test.find().readPref("secondary")

21 读操作事务 readConcern

enableMajorityReadConcern: true

// 从节点 1、2
db.fsyncLock()
// 主节点
db.test.insert({count:3})
db.test.find().readConcern("local")
// 如果在一个写操作到达大多数节点前读取了这个写操作,然后因为系统故障该操作回滚了,则发生了脏读
// {readConcern: "majority"} 可以避免脏读
db.test.find().readConcern("majority")

majority 约为:Read Committed。

22 多文档事务

  • Atomocity 原子性
  • Consistency 一致性 writeConcern,readConcern
  • Isolation 隔离性 readConcern
  • Durability 持久性 Journal and Replication

实验启用事务后的隔离性:

db.tx.drop();
db.tx.insertMany([{x:1},{x:2}]);
var session = db.getMongo().startSession();
session.startTransaction();
var coll = session.getDatabase('test').getCollection('tx');
coll.updateOne({x:2}, {$set: {y:1}});
// {
// acknowledged: true,
// insertedId: null,
// matchedCount: 1,
// modifiedCount: 1,
// upsertedCount: 0
// }
coll.find();
// [
// { _id: ObjectId("64478074fcfac90fb90f1a65"), x: 1 },
// { _id: ObjectId("64478074fcfac90fb90f1a66"), x: 2, y: 1 }
// ]
db.tx.find();
// [
// { _id: ObjectId("64478074fcfac90fb90f1a65"), x: 1 },
// { _id: ObjectId("64478074fcfac90fb90f1a66"), x: 2 }
// ]
session.commitTransaction();
db.tx.find();

实验可重复读 Repeatable Read:

db.tx.drop();
db.tx.insertMany([{x:1},{x:2}]);
var session = db.getMongo().startSession();
session.startTransaction({
readConcern: {"level": "snapshot"},
writeConcern: {"w": "majority"}
});
var coll = session.getDatabase('test').getCollection('tx');
coll.findOne({x: 1});
// 模拟事务之外的操作
db.tx.updateOne({x:1}, {$set: {y:1}});
db.tx.find();
// [
// { _id: ObjectId("644782c3fcfac90fb90f1a69"), x: 1, y: 1 },
// { _id: ObjectId("644782c3fcfac90fb90f1a6a"), x: 2 }
// ]
coll.findOne({x: 1});
// { _id: ObjectId("644782c3fcfac90fb90f1a69"), x: 1 }
session.abortTransaction();

实验写冲突:

db.tx.drop();
db.tx.insertMany([{x:1},{x:2}]);
// shell 1、2
var session = db.getMongo().startSession();
session.startTransaction({
readConcern: {"level": "snapshot"},
writeConcern: {"w": "majority"}
});
var coll = session.getDatabase('test').getCollection('tx');
// shell 1
coll.updateOne({x:1}, {$set: {y:1}});
// ok

// shell 2
coll.updateOne({x:1}, {$set: {y:1}});
// MongoServerError: WriteConflict error: this operation conflicted with another operation. Please retry your operation or multi-document transaction.
// session.abortTransaction();
  • 事务默认 60s 内完成。
  • 多文档事务中的读操作必须使用主节点读。

23 Change Stream

类似触发器。

  • 触发方式:异步 | 同步(事务保证)
  • 触发位置:回调事件 | 数据库触发器
  • 触发次数:每个订阅事件的客户端 | 1次
  • 故障恢复:从上此断点重新触发 | 事务回滚

基于 oplog 实现。被追踪的变更事件主要包括:

  • insert/update/delete
  • drop
  • rename
  • dropDatabase
  • invalidate:drop/rename/dropDatabase 将导致 invalidate 被触发,并关闭 chage stream

可重复读。未开启 majority readConcern 的集群无法使用 Change Stream。当集群无法满足 {w: “majority”} 时,不会触发 Change Stream。

可以使用集合管道的过滤步骤过滤事件。

25 分片集群机制及原理

  • 路由节点 mongos
  • 配置节点 mongod
  • 数据节点 mongod

分片集群数据发布方式

  • 基于范围 查询性能好,数据分布可能不均匀
  • 基于哈希 发布均匀,范围查询效率低;日志、物联网
  • Zone 数据天然分区

25 分片集群设计

  • 片键 shard key 文档中的一个字段
  • 文档 doc
  • 块 chunk
  • 分片 shard
  • 集群 cluster

片键:

  • 取值基数范围要大
  • 取值范围应尽可能均匀
  • 对主要查询要具有定向能力

组合片键。{user_id:1, time:1}

27 分片集群搭建及扩容

# 1 配置域名解析
# 2 准备分片目录

# 3 创建第1个分片复制集 member1:27010 member3:27010 member5:27010
mongod --bind_ip_all --replSet shard1 --shardsvr --wiredTigerCacheSizeGB 1
# --shardsvr 标注为分片节点
# --wiredTigerCacheSizeGB 1 缓存大小
# 4 初始化分片复制集

# 5 创建 config server 复制集 member1:27019 member3:27019 member5:27019
mongod --bind_ip_all --replSet config --configsvr --wiredTigerCacheSizeGB 1
# --configsvr 标注为配置节点
# 6 创建 config server 复制集

# 7 搭建 mongos member1:27017
mongos --bind_ip_all --configdb config/member1:27019,member3:27019,member5:27019,
# 连接到 mongos 添加分片
sh.addShard("shard1/member1:27010,member3:27010,member5:27010")

# 8 创建分片表
# 连接到 mongos member1:27017
sh.enableSharding("foo");
sh.shardCollection("foo.bar", {_id: "hashed"});
sh.status();
# 插入测试数据
use foo
for(var i=0;1<10000;i++) {
db.bar.insert({i:i))
}

# 9 创建第2个分片复制集 member2:27011 member4:27011 member6:27011
# 10 初始化第2个分片复制集
# 11 加入第2个分片
# 连接到 mongos 添加分片
sh.addShard("shard2/member2:27011,member4:27011,member6:27011")

28 监控最佳实践

  • MongoDB Ops Manager
  • Percona
  • 程序脚本

如何获取监控数据:

  • db.serverStatus() 从上次开机到现在为止
  • db.isMaster()
  • monogostats
  • db.serverStatus().opcounters

29 备份与恢复

  • 延迟节点备份
  • 全量备份+Oplog增量,Oplog 幂等性 支持重放
  • mongodump –oplog;不遗漏 dump 时的数据
  • mongorestore –oplogReplay

31 安全架构

db.createRole({
role: "readWriteRole",
privileges: [
{
resource: { db: "myDatabase", collection: "sample" },
actions: [ "find", "insert", "update", "remove" ]
}
],
roles: [{
role: 'read',
db: 'sampledb',
}]
})
db.createUser({
user: 'sampleUser',
pwd: 'pwd',
roles: [{
role: 'readWriteRole',
db: 'admin',
}]
})

32 安全加固实践

# 禁止脚本执行
--noscripting
# 必须鉴权
--auth

33 索引机制(一)

  • Index、Key、DataPage
  • Covered Query/FETCH
  • IXSCAN/COLLSCAN 索引扫描/集合扫描
  • Big O Notation 时间复杂度
  • Query Shape 查询的形状
  • Index Prefix 索引前缀
  • Selectivity 过滤器 选择区别度大的
  • B-数结构

B-树和B+树都是常见的多路搜索树结构,常用于数据库索引和文件系统中。它们的主要区别在于如何存储和检索数据。

B-树是一种自平衡的搜索树,其中每个节点可以存储多个键和对应的值,并支持在O(log n)时间内进行搜索、插入和删除操作。B-树的每个节点都包含了一个子节点数组,可以用来搜索和遍历树。在B-树中,所有节点都可以存储键和值,而非仅仅是叶子节点。

B+树与B-树非常相似,但是只有叶节点包含了所有的键和值,而且所有叶节点都通过指针链接在一起。这意味着在B+树上进行查找只需要搜索一条从根节点到叶节点的路径,而在B-树中可能需要搜索多个节点。B+树的非叶子节点只包含键,而不包含值,这使得B+树在维护索引时更加高效。

因此,B+树比B-树更适用于存储和检索大量数据,尤其是数据库和文件系统中的索引。B+树的叶子节点形成了一个有序链表,可以方便地进行区间查找和遍历。而B-树则更适合内存较小的情况下,例如缓存。

34 索引机制(二)

.explain(true) 查看:

  • executionTimeMillis
  • totalDocsExamined
  • executionStages.docsExamined
  • executionStages.inputStage.stage

组合索引 ESR 原则,Equal(最前) Sort(中间) Range(最后)

db.member.createIndex({city:1}, {background: true})

36 性能诊断工具

mongostat 了解 MongoDB 运行状态的工具。

  • dirty 没有刷盘的比例 <5%
  • used
  • qrw 排队的请求
  • con 连接数量

mongotop 了解集合压力状态

mtools

41 应用场景及选型

优点:

  • 横向扩展能力,数据量或并发量增加时候架构可以自动扩容
  • 灵活模型,适合迭代开发,数据模型多变场景
  • JSON 数据结构,适合微服务/REST API

44 关系型数据库迁移

从基于关系型数据库应用迁移到 MongoDB 的理由:

  • 高并发需求(数千 - 数十万 ops),关系型数据库不容易扩展
  • 快速迭代 - 关系型模式太严谨
  • 灵活的 JSON 模式
  • 大数据量需求
  • 地理位置查询
  • 多数据中心跨地域部署

References

– EOF –