MongoDB数据建模小案例

MongoDB数据建模小案例

朋友圈评论内容管理

需求

社交类的APP需求,一般都会引入"朋友圈"功能,这个产品特性有一个非常重要的功能就是评论体系。

先整理下需求:

  • 这个APP希望点赞和评论信息都要包含头像信息:
    1. 点赞列表,点赞用户的昵称,头像;
    2. 评论列表,评论用户的昵称,头像;
  • 数据查询则相对简单:
    1. 根据分享ID,批量的查询出10条分享里的所有评论内容;

建模

不好的设计

跟据上面的内容,先来一个非常非常"朴素"的设计:

java 复制代码
{

  "_id": 41,

  "username": "小白",

  "uid": "100000",

  "headurl": "http://xxx.yyy.cnd.com/123456ABCDE",

  "praise_list": [

    "100010",

    "100011",

    "100012"

  ],

  "praise_ref_obj": {

    "100010": {

      "username": "小一",

      "headurl": "http://xxx.yyy.cnd.com/8087041AAA",

      "uid": "100010"

    },

    "100011": {

      "username": "mayun",

      "headurl": "http://xxx.yyy.cnd.com/8087041AAB",

      "uid": "100011"

    },

    "100012": {

      "username": "penglei",

      "headurl": "http://xxx.yyy.cnd.com/809999041AAA",

      "uid": "100012"

    }

  },

  "comment_list": [

    "100013",

    "100014"

  ],

  "comment_ref_obj": {

    "100013": {

      "username": "小二",

      "headurl": "http://xxx.yyy.cnd.com/80232041AAA",

      "uid": "100013",

      "msg": "good"

    },

    "100014": {

      "username": "小三",

      "headurl": "http://xxx.yyy.cnd.com/11117041AAB",

      "uid": "100014",

      "msg": "bad"

    }

  }

}

可以看到,comment_ref_obj与praise_ref_obj两个字段,有非常重的关系型数据库痕迹,猜测,这个系统之前应该是放在了普通的关系型数据库上,或者设计者被关系型数据库的影响较深。而在MongoDB这种文档型数据库里,实际上是没有必要这样去设计,这种建模只造成了多于的数据冗余。

另外一个问题是,url占用了非常多的信息空间,这点在压测的时候会有体现,带宽会过早的成为瓶颈。同样username信息也是如此,此类信息相对来说是全局稳定的,基本不会做变化。并且这类信息跟随评论一起在整个APP中流转,也无法处理"用户名修改"的需求。

根据这几个问题,重新做了优化的设计建议。

推荐的设计
java 复制代码
{

  "_id": 41,

  "uid": "100000",

  "praise_uid_list": [

    "100010",

    "100011",

    "100012"

  ],

  "comment_msg_list": [

    {

      "100013": "good"

    },

    {

      "100014": "bad"

    }

  ]

}

对比可以看到,整个结构要小了几个数量级,并且可以发现url,usrname信息都全部不见了。那这样的需求应该如何去实现呢?

从业务抽象上来说,url,username这类信息实际上是非常稳定的,不会发生特别大的频繁变化。并且这两类信息实际上都应该是跟uid绑定的,每个uid含有指定的url,username,是最简单的key,value模型。所以,这类信息非常适合做一层缓存加速读取查询。

进一步的,每个人的好友基本上是有限的,头像,用户名等信息,甚至可以在APP层面进行缓存,也不会消耗移动端过多容量。但是反过来看,每次都到后端去读取,不但浪费了移动端的流量带宽,也加剧了电量消耗。

总结

MongoDB的文档模型固然强大,但绝对不是等同于关系型数据库的粗暴聚合,而是要考虑需求和业务,合理的设计。有些人在设计时,也会被文档模型误导,三七二十一一股脑的把信息塞到一个文档中,反而最后会带来各种使用问题。

多列数据结构

需求

需求是基于电影票售卖的不同渠道价格存储。某一个场次的电影,不同的销售渠道对应不同的价格。整理需求为:

  • 数据字段:
    1. 场次信息;
    2. 播放影片信息;
    3. 渠道信息,与其对应的价格;
    4. 渠道数量最多几十个;
  • 业务查询有两种:
    1. 根据电影场次,查询某一个渠道的价格;
    2. 根据渠道信息,查询对应的所有场次信息;

建模

不好的模型设计

我们先来看其中一种典型的不好建模设计:

java 复制代码
{

  "scheduleId": "0001",

  "movie": "你的名字",

  "price": {

    "gewala": 30,

    "maoyan": 50,

    "taopiao": 20

  }

}

数据表达上基本没有字段冗余,非常紧凑。再来看业务查询能力:

  1. 根据电影场次,查询某一个渠道的价格;
    1. 建立createIndex({scheduleId:1, movie:1})索引,虽然对price来说没有创建索引优化,但通过前面两个维度,已经可以定位到唯一的文档,查询效率上来说尚可;
  2. 根据渠道信息,查询对应的所有场次信息;
    1. 为了优化这种查询,需要对每个渠道分别建立索引,例如:
      • createIndex({"price.gewala":1})
      • createIndex({"price.maoyan":1})
      • createIndex({"price.taopiao":1})
    2. 但渠道会经常变化,并且为了支持此类查询,肯能需要创建几十个索引,对维护来说简直就是噩梦;

此设计行不通,否决。

一般般的设计
java 复制代码
{

  "scheduleId": "0001",

  "movie": "你的名字",

  "channel": "gewala",

  "price": 30

}

 

{

  "scheduleId": "0001",

  "movie": "你的名字",

  "channel": "maoyan",

  "price": 50

}

 

{

  "scheduleId": "0001",

  "movie": "你的名字",

  "channel": "taopiao",

  "price": 20

}

与上面的方案相比,把整个存储对象结构进行了平铺展开,变成了一种表结构,传统的关系数据库多数采用这种类型的方案。信息表达上,把一个对象按照渠道维度拆成多个,其他的字段进行了冗余存储。如果业务需求再复杂点,造成的信息冗余膨胀非常巨大。膨胀后带来的副作用会有磁盘空间占用上升,内存命中率降低等缺点。对查询的处理呢:

  1. 根据电影场次,查询某一个渠道的价格;
    1. 建立createIndex({scheduleId:1, movie:1, channel:1})索引;
  2. 根据渠道信息,查询对应的所有场次信息;
    1. 建立createIndex({channel:1})索引;

更进一步的优化呢?

合理的设计
java 复制代码
{

  "scheduleId": "0001",

  "movie": "你的名字",

  "provider": [

    {

      "channel": "gewala",

      "price": 30

    },

    {

      "channel": "maoyan",

      "price": 50

    },

    {

      "channel": "taopiao",

      "price": 20

    }

  ]

}

这里使用了在MongoDB建模中非常容易忽略的结构------"数组"。查询方面的处理,是可以建立Multikey Index索引

  1. 根据电影场次,查询某一个渠道的价格;
    1. 建立createIndex({scheduleId:1, movie:1, "provider.channel":1})索引;
  2. 根据渠道信息,查询对应的所有场次信息;
    1. 建立createIndex({"provider.channel":1})索引;

总结

这个案例并不复杂,需求也很清晰,但确实非常典型的MongoDB建模设计,开发人员在进行建模设计时经常也会受传统数据库的思路影响,沿用之前的思维惯性,而忽略了"文档"的价值。

物联网时序数据库建模

本案例非常适合与IoT场景的数据采集,结合MongoDB的Sharding能力,文档数据结构等优点,可以非常好的解决物联网使用场景。

需求

案例背景是来自真实的业务,美国州际公路的流量统计。数据库需要提供的能力:

  • 存储事件数据
  • 提供分析查询能力
  • 理想的平衡点:
    • 内存使用
    • 写入性能
    • 读取分析性能
  • 可以部署在常见的硬件平台上

建模

每个事件用一个独立的文档存储
java 复制代码
{

    segId: "I80_mile23",

    speed: 63,

    ts: ISODate("2013-10-16T22:07:38.000-0500")

}
  • 非常"传统"的设计思路,每个事件都会写入一条同样的信息。多少的信息,就有多少条数据,数据量增长非常快。
  • 数据采集操作全部是Insert语句;
每分钟的信息用一个独立的文档存储(存储平均值)
java 复制代码
{

    segId: "I80_mile23",

    speed_num: 18,

    speed_sum: 1134,

    ts: ISODate("2013-10-16T22:07:00.000-0500")

}
  • 对每分钟的平均速度计算非常友好(speed_sum/speed_num);
  • 数据采集操作基本是Update语句;
  • 数据精度降为一分钟;
每分钟的信息用一个独立的文档存储(秒级记录)
java 复制代码
{

    segId: "I80_mile23",

    speed: {0:63, 1:58, ... , 58:66, 59:64},

    ts: ISODate("2013-10-16T22:07:00.000-0500")

}
  • 每秒的数据都存储在一个文档中;
  • 数据采集操作基本是Update语句;
每小时的信息用一个独立的文档存储(秒级记录)
java 复制代码
{

    segId: "I80_mile23",

    speed: {0:63, 1:58, ... , 3598:54, 3599:55},

    ts: ISODate("2013-10-16T22:00:00.000-0500")

} 

相比上面的方案更进一步,从分钟到小时:

  • 每小时的数据都存储在一个文档中;
  • 数据采集操作基本是Update语句;
  • 更新最后一个时间点(第3599秒),需要3599次迭代(虽然是在同一个文档中)
进一步优化下:
java 复制代码
{

    segId: "I80_mile23",

    speed: {

        0:  {0:47, ..., 59:45},

        ...,

        59: {0:65, ... , 59:56}

    }

    ts: ISODate("2013-10-16T22:00:00.000-0500")

}
  • 用了嵌套的手法把秒级别的数据存储在小时数据里;
  • 数据采集操作基本是Update语句;
  • 更新最后一个时间点(第3599秒),需要59+59次迭代;

嵌套结构正是MongoDB的魅力所在,稍动脑筋把一维拆成二维,大幅度减少了迭代次数;

每个事件用一个独立的文档存储VS每分钟的信息用一个独立的文档存储

从写入上看:后者每次修改的数据量要小很多,并且在WiredTiger引擎下,同一个文档的修改一定时间窗口下是可以在内存中合并的;

从读取上看:查询一个小时的数据,前者需要返回3600个文档,而后者只需要返回60个文档,效率上的差异显而易见;

从索引上看:同样,因为稳定数量的大幅度减少,索引尺寸也是同比例降低的,并且segId,ts这样的冗余数据也会减少冗余。容量的降低意味着内存命中率的上升,也就是性能的提高;

每小时的信息用一个独立的文档存储VS每分钟的信息用一个独立的文档存储

从写入上看:因为WiredTiger是每分钟进行一次刷盘,所以每小时一个文档的方案,在这一个小时内要被反复的load到PageCache中,再刷盘;所以,综合来看后者相对更合理;

从读取上看:前者的数据信息量较大,正常的业务请求未必需要这么多的数据,有很大一部分是浪费的;

从索引上看:前者的索引更小,内存利用率更高;

总结

那么到底选择哪个方案更合理呢?从理论分析上可以看出,不管是小时存储,还是分钟存储,都是利用了MongoDB的信息聚合的能力。

  • 每小时的信息用一个独立的文档存储:设计上较极端,优势劣势都很明显;
  • 每分钟的信息用一个独立的文档存储:设计上较平衡,不会与业务期望偏差较大;

落实到现实的业务上,哪种是最优的?最好的解决方案就是根据自己的业务情况进行性能测试,以上的分析只是"理论"基础,给出"实践"的方向,但千万不可以此论断。

相关推荐
宇钶宇夕14 分钟前
EPLAN 电气制图:建立自己的部件库,添加部件-加SQL Server安装教程(三)上
运维·服务器·数据库·程序人生·自动化
爱可生开源社区38 分钟前
SQLShift 重磅更新:支持 SQL Server 存储过程转换至 GaussDB!
数据库
贾修行1 小时前
SQL Server 空间函数从入门到精通:原理、实战与多数据库性能对比
数据库·sqlserver
傲祥Ax1 小时前
Redis总结
数据库·redis·redis重点总结
一屉大大大花卷2 小时前
初识Neo4j之入门介绍(一)
数据库·neo4j
周胡杰3 小时前
鸿蒙arkts使用关系型数据库,使用DB Browser for SQLite连接和查看数据库数据?使用TaskPool进行频繁数据库操作
前端·数据库·华为·harmonyos·鸿蒙·鸿蒙系统
wkj0013 小时前
navicate如何设置数据库引擎
数据库·mysql
赵渝强老师3 小时前
【赵渝强老师】Oracle RMAN的目录数据库
数据库·oracle
暖暖木头3 小时前
Oracle注释详解
数据库·oracle
御控工业物联网3 小时前
御控网关如何实现MQTT、MODBUS、OPCUA、SQL、HTTP之间协议转换
数据库·sql·http