Mongodb架构设计浅谈

10,237 阅读8分钟

Mongodb架构设计

参考链接:learnmongodbthehardway.com/schema/sche…

概览

Mongodb是文档型数据库,由于其不属于关系型数据库,不必遵守三大范式,而且也没有Join关键字来支持表连接,所以Mongodb的表结构设计跟Oracle、MySQL很不一样。下面针对几种不同的表设计结构分别举例:

1对1关系模型

在关系型数据库中,1对1关系模型通常是通过外键的形式进行处理。我们以作家跟地址来举例,假设这两个实体的关系是1对1,那么我们可能会像下面这样子建表

但是,为了方便,其实我们在设计表的时候不会严格遵守三大范式,会做一定的冗余数据,实际情况下可能就是这样子的表

那么,我们回到Mongodb,在这张非关系型的NoSQL数据库里,没有标准的外键(虽然我们可以手工建立连接,但是这种表之间的字段关联关系只能存在程序级别,数据库本身并没有提出外键约束的概念),我们可以怎么来建立表并处理表之间的关系呢?

  1. 建立连接

    这种方式可以理解为建立外键,在其中一个表里建立对方的id字段

     用户信息的文档设计    
     {
       _id: 1,
       name: "Peter Wilkinson",
       age: 27
     }
    
     保留外键的地址信息的文档设计
     {
       user_id: 1,
       street: "100 some road",
       city: "Nevermore"
     }
  1. 内嵌文档

    直接把地址信息的文档作为用户信息文档的一个字段储存进去

     {
        name: "Peter Wilkinson",
       age: 27,
       address: {
         street: "100 some road",
         city: "Nevermore"
       }
     }

    直接内嵌文档的好处就是我们可以在单次读操作就可以特定用户的用户信息文档以及对应的地址信息文档,当然了,这是在用户信息和地址信息强关联的时候,这样子直接内嵌才显得有意义。

     官方文档推荐1对1的数据模型尽量使用内嵌的方式,这样子会提高读操作的效率,更快地获取文档信息

1对多关系模型

1对多的关系模型,我们可以简单地以博客和对应的评论信息来举例

对应的Mongodb的表模型如下

博客信息的文档设计
{
  title: "An awesome blog",
  url: "http://awesomeblog.com",
  text: "This is an awesome blog we have just started"
}

评论信息的文档设计
{
  name: "Peter Critic",
  created_on: ISODate("2014-01-01T10:01:22Z"),
  comment: "Awesome blog post"
}
{
  name: "John Page",
  created_on: ISODate("2014-01-01T11:01:22Z"),
  comment: "Not so awesome blog"
}

在关系型数据库里,我们通常是分别建立两张表:一个Blog表、一个Comments表(从表,带有blog_id外键),然后通过join操作把两个表关联起来

但是在Mongodb里由于没有Join关键字,但是我们可以根据Mongodb的特点,得出以下三个解决方式:

  1. 内嵌

     内嵌了评论信息的博客文档设计
     {
       title: "An awesome blog",
       url: "http://awesomeblog.com",
       text: "This is an awesome blog we have just started",
       comments: [{
         name: "Peter Critic",
         created_on: ISODate("2014-01-01T10:01:22Z"),
         comment: "Awesome blog post"
       }, {
         name: "John Page",
         created_on: ISODate("2014-01-01T11:01:22Z"),
         comment: "Not so awesome blog"
       }]
     }

    上面这种表设计的好处是,我们可以直接获取指定博客下的评论信息,用户新增评论的话,直接在blog文档下的comments数组字段插入一个新值即可。

    但是这种表设计至少有三个如下的潜在问题需要注意:

    1. 博客下的评论数组可能会逐渐扩增,甚至于超过了文档的最大限制长度:16MB
    2. 第二个问题是跟写性能相关,由于评论是不停地添加至博客文档里,当有新的博客文档插入集合的时候,MongoDB会变得比较困难定位到原来的博客文档位置,另外,数据库还需要额外开辟新的内存空间并复制原来的博客文档、更新所有索引,这需要更多的IO交互,可能会影响写性能

        必须注意的是,只有高写入流量的情况下才可能会影响写性能,而对于写入流量较小的程序反而没有那么大的影响。视具体情况而定。

3. 第三个问题是当你尝试去进行评论分页的时候,你会发觉通过常规的find查询操作,我们只能先读取整个文档信息(包括所有评论信息),然后在程序里进行评论信息的分页
  1. 连接

    第二个方式是通过建立类似外键的id来进行文档间的关联

     博客的文档设计        
     {
       _id: 1,
       title: "An awesome blog",
       url: "http://awesomeblog.com",
       text: "This is an awesome blog we have just started"
     }
    
     评论的文档设计
     {
       blog_entry_id: 1,
       name: "Peter Critic",
       created_on: ISODate("2014-01-01T10:01:22Z"),
       comment: "Awesome blog post"
     }
     {
       blog_entry_id: 1,
       name: "John Page",
       created_on: ISODate("2014-01-01T11:01:22Z"),
       comment: "Not so awesome blog"
     }
这样子设计模型有个好处是当评论信息逐渐增长的时候并不会影响原始的博客文档,从而避免了单个文档超过16MB的情况出现。而且这样子设计也比较容易返回分页评论。但是坏处的话,就是假设我们在一个博客文档下拥有非常多的评论时(比如1000条),那我们获取所有评论的时候会引起数据库很多的读操作
  1. 分块

    第三个方法就是前面两种方法的混合,理论上,尝试去平衡内嵌策略和连接模式,举个例子,我们可能会根据实际情况,把所有的评论切分成最多50条评论的分块

     博客的文档设计        
     {
       _id: 1,
       title: "An awesome blog",
       url: "http://awesomeblog.com",
       text: "This is an awesome blog we have just started"
     }
    
     评论信息的文档设计
     {
       blog_entry_id: 1,
       page: 1,
       count: 50,
       comments: [{
         name: "Peter Critic",
         created_on: ISODate("2014-01-01T10:01:22Z"),
         comment: "Awesome blog post"
       }, ...]
     }
     {
       blog_entry_id: 1,
       page: 2,
       count: 1,
       comments: [{
         name: "John Page",
         created_on: ISODate("2014-01-01T11:01:22Z"),
         comment: "Not so awesome blog"
       }]
     }

    这样子设计最大的好处是我们可以单次读操作里一次性抓出50条评论,方便我们进行评论分页

     什么时候使用分块策略?
     当你可以将文档切割成不同的批次时,那么采用这种策略可以加速文档检索
     典型的例子就是根据小时、天数或者数量进行评论分页(类似评论分页)

多对多关系模型

多对多关系模型,我们以作者跟创作的书籍来举例


在关系型数据库里,我们可以通过中间表的方式来处理

  1. 双向嵌套

    在MongoDB里我们可以通过双向嵌套,把两个文档的外键通过数组字段添加到彼此的文档里

     作者信息的文档设计
     {
       _id: 1,
       name: "Peter Standford",
       books: [1, 2]
     }
     {
       _id: 2,
       name: "Georg Peterson",
       books: [2]
     }
    
     书籍信息的文档设计
     {
       _id: 1,
       title: "A tale of two people",
       categories: ["drama"],
       authors: [1, 2]
     }
     {
       _id: 2,
       title: "A tale of two space ships",
       categories: ["scifi"],
       authors: [1]
     }
当我们进行查询的时候,可以通过两个维度互相进行查询

    通过指定的作者搜索对应的书籍
    var db = db.getSisterDB("library");
    var booksCollection = db.books;
    var authorsCollection = db.authors;

    var author = authorsCollection.findOne({name: "Peter Standford"});
    var books = booksCollection.find({_id: {$in: author.books}}).toArray();

    通过指定的书籍搜索对应的作者
    var db = db.getSisterDB("library");
    var booksCollection = db.books;
    var authorsCollection = db.authors;

    var book = booksCollection.findOne({title: "A tale of two space ships"});
    var authors = authorsCollection.find({_id: {$in: book.authors}}).toArray();
  1. 单向嵌套

    单向嵌套策略是用来优化多对多关系模型里的读性能,通过将双向引用转移为类似一对多的单向引用。这种策略是有特定场景的,比如在我们这个案例中,我们设计的作者信息文档里,将书籍信息作为数组字段嵌入作者文档,但是实际情况下,书籍的数量是会快速地增长,很可能会突破单个文档16MB的限制。

    在这个案例中,我们可以看到书籍数量是快速增长的,但是书籍分类确实比较固定,通常不会有太大改动,所以我们把书籍分类信息单独设计成文档,然后作者信息作为书籍信息的嵌入数组引用,书籍分类也作为嵌入数组引用。以相对变化不大的书籍分类作为主表,把相对变化较大的书籍信息作为从表,储存主表id作为外键。

     书籍分类的文档设计
     {
      _id: 1,
      name: "drama"
     }
    
     通过外键关联对应分类的书籍信息文档设计
     {
       _id: 1,
       title: "A tale of two people",
       categories: [1],
       authors: [1, 2]
     }

    相对应的查询语句如下

     通过指定书籍来查找对应的书籍分类
     var db = db.getSisterDB("library");
     var booksCol = db.books;
     var categoriesCol = db.categories;
    
     var book = booksCol.findOne({title: "A tale of two space ships"});
     var categories = categoriesCol.find({_id: {$in: book.categories}}).toArray();    
    根据指定书籍分类来查找对应书籍
    var db = db.getSisterDB("library");
    var booksCollection = db.books;
    var categoriesCollection = db.categories;

    var category = categoriesCollection.findOne({name: "drama"});
    var books = booksCollection.find({categories: category.id}).toArray();

需要注意的地方:

    保持关联关系的平衡
    当多对多关系模型里,有一个模型数量级别特别大(比如最多可达500000个),另一个数量级别特别小(比如最多3个),像这个案例中,可能才3个左右的书籍分类就可以对应到高达500000本书。在这张数量级别悬殊的情况下,就应该采用这种单向嵌套的策略。如果双方书籍级别都比较小(可能最多也就是5个左右)的时候,采用双向嵌套的策略可能会好一点。