MongoDB开发规范
1.命名原则
2.集合设计
3.文档设计
4.索引设计
5.分片设计
6.升级设计
7.考虑数据老化问题
8.数据一致性方面
9.使用update、findAndUpdate对数据进行更新时,如果使用过了upset: true,则必须使用唯一性索引避免产生重复数据
10.业务上尽量避免短连接,使用官方最新驱动的连接池实现,控制客户端连接的数量,最大不建议超过200
11.对大量数据写入使用Bulk Write批量化API,建议使用无序批次更新
12.优先使用单文档事务保证原子性,如果需要使用多文档事务,则必须保证事务尽可能小,一个事务的执行时间最长不超过60s
13.在条件允许的情况下,使用读写分离降低primary节点的压力。对于一些统计分析类的查询可优先从节点上读取
14.考虑业务数据的隔离,例如将配置项数据、历史数据存放在不同的数据库中,微服务之间使用单独的数据库,尽量避免垮库访问
15.维护数据字典文档,并保持更新,提前按不同的业务进行数据容量规划
MongoDB数据建模
嵌入式文档 一对一关系模型
嵌入式文档模型
以下映射客户和地址关系的示例。对于这种数据量较小的文档使用嵌入式文档更好
1 2 3 4 5 6 7 8 9 10 11 12 13 | // patron document
{
_id: "joe" ,
name : "Joe Bookreader"
}
// address document
{
patron_id: "joe" , // reference to patron document
street: "123 Fake Street" ,
city: "Faketon" ,
state: "MA" ,
zip: "12345"
}
|
如果经常将address
数据与name
信息一起检索,更好的Realm 数据模型是将address
数据嵌入到patron
数据中,如以下文档所示:
1 2 3 4 5 6 7 8 9 10 | {
_id: "joe" ,
name : "Joe Bookreader" ,
address: {
street: "123 Fake Street" ,
city: "Faketon" ,
state: "MA" ,
zip: "12345"
}
}
|
子集模式
嵌入式文档模型的一个潜在问题是,它可能会导致大型文档包含应用程序不需要的字段。 这些不必要的数据可能会给服务器造成额外负载,并减慢读取操作的速度。相反,可以使用子集模式来检索在单个数据库调用中访问最频繁的数据子集。
考虑一个显示电影信息的应用程序。 movie
数据库包含具有以下模式的collection集合:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | {
"_id" : 1,
"title" : "The Arrival of a Train" ,
"year" : 1896,
"runtime" : 1,
"released" : ISODate( "01-25-1896" ),
"poster" : "http://ia.media-imdb.com/images/M/MV5BMjEyNDk5MDYzOV5BMl5BanBnXkFtZTgwNjIxMTEwMzE@._V1_SX300.jpg" ,
"plot" : "A group of people are standing in a straight line along the platform of a railway station, waiting for a train, which is seen coming at some distance. When the train stops at the platform, ..." ,
"fullplot" : "A group of people are standing in a straight line along the platform of a railway station, waiting for a train, which is seen coming at some distance. When the train stops at the platform, the line dissolves. The doors of the railway-cars open, and people on the platform help passengers to get off." ,
"lastupdated" : ISODate( "2015-08-15T10:06:53" ),
"type" : "movie" ,
"directors" : [ "Auguste Lumière" , "Louis Lumière" ],
"imdb" : {
"rating" : 7.3,
"votes" : 5043,
"id" : 12
},
"countries" : [ "France" ],
"genres" : [ "Documentary" , "Short" ],
"tomatoes" : {
"viewer" : {
"rating" : 3.7,
"numReviews" : 59
},
"lastUpdated" : ISODate( "2020-01-09T00:02:53" )
}
}
|
如果应用程序显示电影简单概述时不需要的多个字段,我们就可以将该collection分割为两个collection,而不是将所有电影数据存储在单个collection中:
电影的基本信息。应用程序默认加载的数据如下:
1 2 3 4 5 6 7 8 9 10 11 12 | // movie collection
{
"_id" : 1,
"title" : "The Arrival of a Train" ,
"year" : 1896,
"runtime" : 1,
"released" : ISODate( "1896-01-25" ),
"type" : "movie" ,
"directors" : [ "Auguste Lumière" , "Louis Lumière" ],
"countries" : [ "France" ],
"genres" : [ "Documentary" , "Short" ],
}
|
每部电影的其他不常访问的数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | // movie_details collection
{
"_id" : 156,
"movie_id" : 1, // 通过这个字段进行关联
"poster" : "http://ia.media-imdb.com/images/M/MV5BMjEyNDk5MDYzOV5BMl5BanBnXkFtZTgwNjIxMTEwMzE@._V1_SX300.jpg" ,
"plot" : "A group of people are standing in a straight line along the platform of a railway station, waiting for a train, which is seen coming at some distance. When the train stops at the platform, ..." ,
"fullplot" : "A group of people are standing in a straight line along the platform of a railway station, waiting for a train, which is seen coming at some distance. When the train stops at the platform, the line dissolves. The doors of the railway-cars open, and people on the platform help passengers to get off." ,
"lastupdated" : ISODate( "2015-08-15T10:06:53" ),
"imdb" : {
"rating" : 7.3,
"votes" : 5043,
"id" : 12
},
"tomatoes" : {
"viewer" : {
"rating" : 3.7,
"numReviews" : 59
},
"lastUpdated" : ISODate( "2020-01-29T00:02:53" )
}
}
|
嵌入式文档 一对多关系模型
嵌入式文档模型
客户和多个地址关系的示例如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | // patron document
{
_id: "joe" ,
name : "Joe Bookreader"
}
// address documents
{
patron_id: "joe" , // reference to patron document
street: "123 Fake Street" ,
city: "Faketon" ,
state: "MA" ,
zip: "12345"
}
{
patron_id: "joe" ,
street: "1 Some Other Street" ,
city: "Boston" ,
state: "MA" ,
zip: "12345"
}
|
如果经常检索带有name
信息的address
数据,那么就需要发出多个查询来解析引用。 更优化的模式是将address
数据实体嵌入到patron
数据中,如以下文档所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | {
"_id" : "joe" ,
"name" : "Joe Bookreader" ,
"addresses" : [
{
"street" : "123 Fake Street" ,
"city" : "Faketon" ,
"state" : "MA" ,
"zip" : "12345"
},
{
"street" : "1 Some Other Street" ,
"city" : "Boston" ,
"state" : "MA" ,
"zip" : "12345"
}
]
}
|
借助嵌入式数据模型,应用程序可以通过一次查询检索完整的客户信息。
子集模式
嵌入式文档模式的一个潜在问题是,它可能导致文档过大,尤其是在嵌入式字段没有限制的情况下。在这种情况下,您可以使用子集模式仅访问应用程序所需的数据,而不是访问整个嵌入数据集
例如,产品评论列表的电商站点,reviews字段中保存着所有的评论数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | {
"_id" : 1,
"name" : "Super Widget" ,
"description" : "This is the most useful item in your toolbox." ,
"price" : { "value" : NumberDecimal( "119.99" ), "currency" : "USD" },
"reviews" : [
{
"review_id" : 786,
"review_author" : "Kristina" ,
"review_text" : "This is indeed an amazing widget." ,
"published_date" : ISODate( "2019-02-18" )
},
{
"review_id" : 785,
"review_author" : "Trina" ,
"review_text" : "Nice product. Slow shipping." ,
"published_date" : ISODate( "2019-02-17" )
},
...
{
"review_id" : 1,
"review_author" : "Hans" ,
"review_text" : "Meh, it's okay." ,
"published_date" : ISODate( "2017-12-06" )
}
]
}
|
评论按时间倒序排列。用户访问产品页面时,应用程序会加载最近十条评论。
您可以将该集合拆分为两个集合,而不存储该产品的所有评论:
product
collection 存储每个产品的信息,包括该产品的 10 条最新评论:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | {
"_id" : 1,
"name" : "Super Widget" ,
"description" : "This is the most useful item in your toolbox." ,
"price" : { "value" : NumberDecimal( "119.99" ), "currency" : "USD" },
"reviews" : [
{
"review_id" : 786,
"review_author" : "Kristina" ,
"review_text" : "This is indeed an amazing widget." ,
"published_date" : ISODate( "2019-02-18" )
}
...
{
"review_id" : 777,
"review_author" : "Pablo" ,
"review_text" : "Amazing!" ,
"published_date" : ISODate( "2019-02-16" )
}
]
}
|
review
collection 存储所有评论。每条评论都包含对相应产品的引用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | {
"review_id" : 786,
"product_id" : 1, // 通过该字段进行关联
"review_author" : "Kristina" ,
"review_text" : "This is indeed an amazing widget." ,
"published_date" : ISODate( "2019-02-18" )
}
{
"review_id" : 785,
"product_id" : 1,
"review_author" : "Trina" ,
"review_text" : "Nice product. Slow shipping." ,
"published_date" : ISODate( "2019-02-17" )
}
...
{
"review_id" : 1,
"product_id" : 1,
"review_author" : "Hans" ,
"review_text" : "Meh, it's okay." ,
"published_date" : ISODate( "2017-12-06" )
}
|
文档引用 一对多关系模型
以下示例展示如何映射出版商和图书关系。该示例说明在避免出版商信息冗余方面,引用比嵌入更有优势。
将出版商文档嵌入图书文档会导致出版商数据重复,如以下文档所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | // 各个文档中都保存publisher 出版商信息,造成了出版商信息冗余
{
title: "MongoDB: The Definitive Guide" ,
author: [ "Kristina Chodorow" , "Mike Dirolf" ],
published_date: ISODate( "2010-09-24" ),
pages: 216,
language: "English" ,
publisher: {
name : "O'Reilly Media" ,
founded: 1980,
location: "CA"
}
}
{
title: "50 Tips and Tricks for MongoDB Developer" ,
author: "Kristina Chodorow" ,
published_date: ISODate( "2011-05-06" ),
pages: 68,
language: "English" ,
publisher: {
name : "O'Reilly Media" ,
founded: 1980,
location: "CA"
}
}
|
使用引用并将出版商信息保存在图书集合之外的单独集合中。
使用引用时,关系的增长将决定引用的存储方式。
如果每个出版商的图书数量较少且增长有限,则将图书引用存储在出版商文档中有时可能十分有用。相反,当每个出版商的图书数量没有限制时,此数据模型将导致可变且不断增长的数组,如以下示例所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | // 出版商信息
// 如果出版商的图书数量没有限制时,那么下面的books数组将会非常大
{
name : "O'Reilly Media" ,
founded: 1980,
location: "CA" ,
books: [123456789, 234567890, ...]
}
// 图书信息
{
_id: 123456789,
title: "MongoDB: The Definitive Guide" ,
author: [ "Kristina Chodorow" , "Mike Dirolf" ],
published_date: ISODate( "2010-09-24" ),
pages: 216,
language: "English"
}
{
_id: 234567890,
title: "50 Tips and Tricks for MongoDB Developer" ,
author: "Kristina Chodorow" ,
published_date: ISODate( "2011-05-06" ),
pages: 68,
language: "English"
}
|
为避免出现可变且不断增长的数组,请将出版商的引用存储在图书文档中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | // 出版商信息
{
_id: "oreilly" ,
name : "O'Reilly Media" ,
founded: 1980,
location: "CA"
}
// 图书信息
{
_id: 123456789,
title: "MongoDB: The Definitive Guide" ,
author: [ "Kristina Chodorow" , "Mike Dirolf" ],
published_date: ISODate( "2010-09-24" ),
pages: 216,
language: "English" ,
publisher_id: "oreilly" // 将出版商的引用存储在图书文档中
}
{
_id: 234567890,
title: "50 Tips and Tricks for MongoDB Developer" ,
author: "Kristina Chodorow" ,
published_date: ISODate( "2011-05-06" ),
pages: 68,
language: "English" ,
publisher_id: "oreilly" // 将出版商的引用存储在图书文档中
}
|
物联网时序数据建模
需求
美国州际公路的流量统计。数据库需要提供的能力:
存储事件数据
提供分析查询能力
理想的平衡点:
可以部署在常见的硬件平台上
每个事件用一个独立的文档存储
1 2 3 4 5 | {
segId: "I80_mile23" ,
speed: 63,
ts: ISODate( "2013-10-16T22:07:38.000-0500" )
}
|
每分钟的信息用一个独立的文档存储(存储平均值)
1 2 3 4 5 6 | {
segId: "I80_mile23" ,
speed_num: 18,
speed_sum: 1134,
ts: ISODate( "2013-10-16T22:07:00.000-0500" )
}
|
每分钟的信息用一个独立的文档存储(秒级记录)
1 2 3 4 5 | {
segId: "I80_mile23" ,
speed: {0:63, 1:58, ... , 58:66, 59:64},
ts: ISODate( "2013-10-16T22:07:00.000-0500" )
}
|
每秒的数据都存储在一个文档中;
数据采集操作基本是Update语句;
每小时的信息用一个独立的文档存储(秒级记录)
1 2 3 4 5 | {
segId: "I80_mile23" ,
speed: {0:63, 1:58, ... , 3598:54, 3599:55},
ts: ISODate( "2013-10-16T22:00:00.000-0500" )
}
|
相比上面的方案更进一步,从分钟到小时:
进一步优化
1 2 3 4 5 6 7 8 9 | {
segId: "I80_mile23" ,
speed: {
0: {0:47, ..., 59:45},
...,
59: {0:65, ... , 59:56}
}
ts: ISODate( "2013-10-16T22:00:00.000-0500" )
}
|
嵌套结构正是MongoDB的魅力所在,稍动脑筋把一维拆成二维,大幅度减少了迭代次数;
每个事件用一个独立的文档存储VS每分钟的信息用一个独立的文档存储
从写入上看:后者每次修改的数据量要小很多,并且在WiredTiger引擎下,同一个文档的修改一定时间窗口下是可以在内存中合并的;
从读取上看:查询一个小时的数据,前者需要返回3600个文档,而后者只需要返回60个文档,效率上的差异显而易见;
从索引上看:同样,因为稳定数量的大幅度减少,索引尺寸也是同比例降低的,并且segId,ts这样的冗余数据也会减少冗余。容量的降低意味着内存命中率的上升,也就是性能的提高;
每小时的信息用一个独立的文档存储VS每分钟的信息用一个独立的文档存储
从写入上看:因为WiredTiger是每分钟进行一次刷盘,所以每小时一个文档的方案,在这一个小时内要被反复的load到PageCache中,再刷盘;所以,综合来看后者相对更合理;
从读取上看:前者的数据信息量较大,正常的业务请求未必需要这么多的数据,有很大一部分是浪费的;
从索引上看:前者的索引更小,内存利用率更高;
总结
那么到底选择哪个方案更合理呢?从理论分析上可以看出,不管是小时存储,还是分钟存储,都是利用了MongoDB的信息聚合的能力。
落实到现实的业务上,哪种是最优的?最好的解决方案就是根据自己的业务情况进行性能测试,以上的分析只是“理论”基础,给出“实践”的方向,但千万不可以此论断。