MongoDB 存储引擎和数据模型设计

标签: MongoDB NoSQL

1. 存储引擎

1.1 存储引擎是什么

存储引擎是位于持久化数据(通常是放在磁盘或者内存中)和数据库之间的一个操作接口,它负责数据的存储和读取方式。MongoDB数据库通过存储引擎在磁盘中读取数据,而假设我们的应用是ASP.NET MVC,我们可以使用官方的Mongo.Driver驱动,通过通信协议(如TCP)向MongoDB数据库发送各种请求。以下是一个简单的运行图示

1.2 MongoDB中的默认存储引擎

自MongoDB 3.2 Release版本起,MongoDB默认的存储引擎就成了WiredTiger。而在之前的版本中,它还是MMAPv1。但由于,ongoDB架构支持可插拔的存储引擎,所以使用中即便要更换也是可以做到的。至于其他的功能比较大家可以参阅官方文档,如不再是In-Place Update,新增Compression等。

我们可以在开启mongod服务时输入相关参数调整存储引擎,如mongod --storageEngine MMAPv1|wiredTiger 我们也可以使用db.collections.stats()查看当前的引擎名称

  • MMAPv1 MMAPv1 提供集合级别锁(实际上称为collection-level locking)
  • WiredTiger WiredTiger 对于写操作提供文档级别并发控制(实际上称为document-level concurrency),因此,不同的客户端请求可以在同一时间针对一个集合中的不同文档记性修改

2. 数据模型设计

2.1 内嵌和引用

在MongoDB中,数据的表示方式有内嵌和引用两种。

“引用”我们比较好理解,是指将不同实体的数据分散不到不同的集合中,而在关系型数据库设计中就是将实体分别建立相应的模型表。如常见的“老师-学生”,“产品-标签”关系,只要实体间存在关系,就可以使用“引用”思想。

“内嵌”是一种反范式化的设计,指的是将每个文档所需的数据都嵌入到文档内部,我想举一个“用户-账户”的关系。我们知道在领域驱动设计中,“用户”是一个聚合根,每个用户对应一个账户,所以是“1对1”的一种关系,在关系型数据库设计中,大部分时候都会将这两者严格区分开来。但是在MongoDB中,却不然,我们可以直接选择将“用户”需要的“账户”数据内嵌到用户文档中,便于我们的增删改查。这是一种反范式化的设计。

设计MongoDB数据模型的时候,我们需要转变以往设计关系型数据模型时的思维。即便是针对一个关系中不同集合的数量规模,我们的模型也将有很大的不同。

2.2 设计原则

**

A. 1 - 1 或者 1 - *(较少)

**

用户与账户,以及用户与收货地址都是这样情况,在这样的情况下,显而易见我们可以采取内嵌的方式来进行数据管理。

> db.person.findOne()
{
    _id:ObjectId("cccc"),
    name:"wddpct",
    age:22,
    location:"wenzhou",
    addresses:[
        {country:"china",city:"wenzhou",street:"chashan road"}
        {country:"china",city:"wenzhou",street:"north center road"}
    ]
}

这也引伸出一个问题,除了“1”以外的另一端的实体是否有必要在数目较少的时候进行单独集合的储存。如用户和任务模块,任务是系统定期发布,分配给相应用户完成,这意味着我们对任务的操作也将比较复杂。这样的情况下,显然是分开不同集合进行存储,然后让person集合引用task_id数组。

> db.person.findOne()
{
    _id:ObjectId("cccc"),
    name:"wddpct",
    age:21,
    location:"wenzhou",
    tasks:[
        ObjectId("xxxx"),
        ObjectId("yyyy"),
        ……
    ]
}

所以针对刚才提到的情况,我们大可以借鉴领域驱动模式中的“实体”和“值对象”的部分概念,主要还是看这些数据模型在系统中是否有较大较复杂的操作可能。

**

B. 1 - *(较多)

**

博主之前负责过一个市级地区中小学眼视光筛查系统,里面的简化模型就比较适合拿来做例子。如学校与学生,数目多也不过数千。这样的情况下,自然也是使用引用的方式更容易接受

> db.school.findOne()
{
    _id:ObjectId("cccc"),
    name:"middle1",
    location:"wenzhou",
    students:[
        ObjectId("xxxx"),
        ObjectId("yyyy"),
        ……
    ]
}

这里同样也引伸出一个“冗余”的问题,我们知道大多时候我们需要查询的数据属性数目是比较少的,比如对于学生而言,我们可能只需要知道他的身高体重,所以我们可以使用“冗余”思想简单修改刚才的集合成以下格式来应付

> db.school.findOne()
{
    _id:ObjectId("cccc"),
    name:"middle1",
    location:"wenzhou",
    students:[
        {ObjectId("xxxx"),name:"wddpct",height:233,weight:233},
        {ObjectId("yyyy"),name:"wddmd",height:233,weight:233}
        ……
    ]
}

不过也要注意的一点是,这样每次更新student的信息时,不免又要对school中的冗余信息进行更新,所以也要结合具体场景使用

**

C. 1 - *(非常多)

**

地区和车牌的关系勉强属于此类,一个地区可能有几十上百万车牌,我们不可能再像刚才那样在area中加入所有的license_id,不然可能光是单个文档大小就超过MongoDB的16MB限制了,而且对于查询也存在很大的负担。

这里我们可以直接套用关系型数据库中的外键思想,在license集合的末尾加入area_id就可以方便解决此类关系

> db.license.findOne()
{
    _id:ObjectId("cccc"),
    license:"middle1",
    area:ObjectId("xxxx")
}

当然,我们也可以对area进行进一步冗余,所以就不额外说明了。

**

D. * - *

**

对于多对多关系模型,可能又要祭出那句老话——“视具体情况而定”。不过一般情况下,它不过就是一对多关系的几个变种。一个基本的原则是考虑两边统一引用对方的ObjectId,适当冗余部分信息。

除此以外,我们还可以从以下几个原则去考虑

  1. 两边的数量比(较大方更适合引用)
  2. 两边的更新频率比(较大方更适合引用)
  3. 两边的读取频率比(较大方更适合内嵌) ……

E. 通用建议

以下给出一张较通用的建议表,仅供参考

内嵌

引用

子文档较小

子文档较大

数据不会定期更改

数据经常改变

最终数据一致即可

中间阶段数据也必须一致

文档数据小额增加

文档数据大幅增加

数据通常需要执行二次查询

数据通常不包含在查询结果中

快速读取

快速写入

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏互联网技术栈

消息队列MQ选型 - Kafka、RabbitMQ对比

48940
来自专栏腾讯Bugly的专栏

《手Q Android线程死锁监控与自动化分析实践》

一、问题背景 手Q每个版本上线以后研发同学都会收到各种问题反馈。在跟进手Q内部用户反馈的问题时,发现多例问题,其表象和原因如下: 1、问题表象:“未读不消失”、...

50690
来自专栏大数据文摘

技术连载:LinkedIn大数据后台如何运作-1

183110
来自专栏玉树芝兰

如何用 pipenv 克隆 Python 教程代码运行环境?(含视频讲解)

咱们的 Python 教程代码已经可以免安装在线运行了。但如果你希望在本地克隆运行环境,请参考本文的步骤说明。

14030
来自专栏后端技术探索

另一篇mysql防止库存超卖

今天王总又给我们上了一课,其实MySQL处理高并发,防止库存超卖的问题,在去年的时候,王总已经提过;但是很可惜,即使当时大家都听懂了,但是在现实开发中,还是没这...

22210
来自专栏QQ音乐前端团队专栏

自己动手打造前端性能监控系统

我们从三个各方面,前端上报,数据收集和入库,数据展示来介绍了如何打造一个测速系统。

1.3K100
来自专栏Golang语言社区

Golang适合高并发场景的原因分析

典型的两个现实案例: 我们先看两个用Go做消息推送的案例实际处理能力。 360消息推送的数据: 16台机器,标配:24个硬件线程,64GB内存 Linux K...

63770
来自专栏java思维导图

分布式之消息队列复习精讲

庆幸的是两位朋友都很有上进心,于是博主写这篇文章,帮助他们复习一下关于消息队列中间件这块的要点

14230
来自专栏C/C++基础

C/C++代码调试的几点建议

代码调试在程序开发阶段占有举足轻重的地位,可见代码调试的重要性。但是有一点必须强调:程序是设计出来的,而不是调试出来的。这是所有程序员必须牢记在心的一条准则。一...

9910
来自专栏FreeBuf

子域名枚举的艺术

写在前面的话 当我们在查找某个域名的有效子域名时,我们通常需要使用子域名枚举这项技术。但是,除非DNS服务器暴露了完整的DNS空间(涉及到AXFR协议),否则我...

51190

扫码关注云+社区

领取腾讯云代金券