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的信息聚合的能力。

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

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

相关推荐
O(1)的boot22 分钟前
微服务的问题
java·数据库·微服务
骐骥11 小时前
PostgreSQL/PostGIS中提升空间查询(分析)性能(效率)的一些方法
数据库·postgresql·postgis·空间分析·空间查询
jikuaidi6yuan1 小时前
并行口的基本概念
数据库·mongodb
web136885658711 小时前
Spring Boot 中使用 @Transactional 注解配置事务管理
数据库·spring boot·sql
骷大人3 小时前
mysql开启配置binlog
数据库·mysql
Yolo_nn3 小时前
MySQL_第14章_存储过程与函数
数据库·mysql·存储过程·函数
GDDGHS_3 小时前
Flink自定义数据源
大数据·数据库·flink
Milk夜雨3 小时前
数据库进阶教程:结合编程实现动态数据操作
数据库·python·adb
weisian1513 小时前
Redis篇-5--原理篇4--Lua脚本
数据库·redis·lua
是十一月末3 小时前
Linux的基本功能和命令
linux·服务器·开发语言·数据库