Mongo存储模型优化

author:skatexg

time:2021/08/11

一. mongo系统架构选择

我们mongo系统架构选择依据是什么?选副本集架构?还是选择分片集群架构?

1.存储容量遇到单机限制时选择分片集群架构。
2.读写能力遇到单机限制(可能是CPU、内存或者网卡等资源遭遇瓶颈),导致读写能力无法扩展时,选择分片集群架构。

如果选择分布式集群架构,最重要的是shared key设计
1.shared key易于切分,最好是无限的。
2.shared key具有高度的随机性,保证数据分布均匀。
3.shared key应该是你的请求的主要字段,避免scatter-gather查询。

二.存储模型优化

确定业务场景
确定业务场景,数据存储整体风格,嵌套存储?还是外联存储?

嵌套存储:意味着将数据存储到多个集合collections中,并在它们之间设计关联关系。更新数据比较容易,但是在读取数据性能变慢
外联存储:将若干对象数据,以嵌套的方式存储到单个文档中。它在读取数据的时候表现更好,但在写入时会变慢。这种存储数据的方式将占用更多空间

如果数据更新不是频繁,更新性能不是很重要,但是在读取时需要良好的性能,那么外联存储可能是明智的选择
如果数据库中的文档数据需要不断的更新,并且您希望在写入时具有良好的性能,那么您可能需要考虑嵌套存储


Collection设计
Mongo中的collection对象之间的关联关系,基本上在一个collection里就能体现了。如果还是通过关系型数据库的关系表的表示,那就没有发挥mongo的优势

Mongod一对多的设计:主要是平衡读写性能(通过合理数据合并,减少网络io、磁盘IO,达到读写平衡)

1.一对一-完整内嵌
表样例:
键盘与主机的关系。一台主机有一个键盘,一个键盘有一台主机。

字段:
主机:id、CPU核数、内存大小、显卡大小。
键盘:id、主机的id、键盘类型(机械/非机械)、颜色、牌子

对象一对一关系比较简单,在mongo中强烈建议使用完全内嵌模式,如下数据样例

{
"主机id":"1",
"CPU核数":"2核",
"内存大小":"16GB",
"显卡大小":"2GB",
"键盘":{
    "键盘类型":"机械",
    "颜色":"Black",
    "牌子":"双飞燕"
}
}

2.一对多-完整内嵌

场景:订单与订单项的关系

字段:
订单字段:id、订单号、运费、总价格、订单状态。 
订单项字段:名称、单价、数量


1)查看订单:查看订单时,都是从订单进入,然后查看所有的订单项。
2)支付成功或失败后,修改订单时,都是修改订单,没有修改订单项。
 
完整内嵌型的一对多设计非常适合这种场景,数据样例如下
 
 
{
        "id": "asdg184981651568956", 
        "订单号": "201809270012598323334", 
        "运费": 0, 
        "总价格": 41, 
        "订单状态": "已经支付", 
        "订单项": [
                {
                        "名称": "益达口香糖", 
                        "单价": 8, 
                        "数量": 2
                }, 
                {
                        "名称": "大大口香糖", 
                        "单价": 5, 
                        "数量": 1
                }, 
                {
                        "名称": "绿箭口香糖", 
                        "单价": 10, 
                        "数量": 2
                }
        ]
}

小结:
使用完整内嵌一对多设计时,前提有这几点
     1)内嵌数组不宜过大。因为太大会导致整个实体内容大,从而导致网络延迟问题。建议不超过30个。具体根据内嵌实体大小而定。
     2)如果内嵌实体数组过多(一般多于5个),查询时,内嵌实体内容没有作为单独的实体查询需求。
     3)如果内嵌实体数组过多(一般多于5个),修改/删除/插入时,内嵌实体内容没有作为单独的实体修改/删除/插入需求。
      
     
3.一对多-内嵌id型
场景:公司与员工的关系。一个公司对应N个员工。
 
1)员工的属性很多。
2)一个公司员工人数不确定。
3)单独查询员工信息的场景。
4)修改员工信息的场景。


这种场景采用完全内嵌设计就不合理,因为内嵌数组太大,所以可考虑内嵌id型,数据样例如下:

公司表:
{
        "id": "45465516654", 
        "名称": "千度科技有限公司", 
        "注册地址": "北京市朝阳区", 
        "所有人": "张大大", 
        "注册日期": "2001-9-1", 
        "员工": [
                {
                        "id": "员工id1"
                }, 
                {
                        "id": "员工id2"
                }
        ]
}

员工表:
{
        "id": "员工id", 
        "姓名": "alun", 
        "入职时间": "2010-8-8", 
        "身份证号": "423632155502", 
        "职位": "架构师", 
        "工资": 1000, 
        "入职年限": 1, 
        "公资金百分比": 5, 
        "得奖数": 1, 
        "体重": "50KG", 
        "住址": "广东省广州", 
        "下属人数": 20, 
        "年假剩余天数": 0, 
        "评价级别": 10
}

小结:
使用内嵌id型一对多设计时,前提有这几点:
     1)一对多的多那方数量要多,最好是几千个不等。
     2)需要单独把内嵌的实体取出。
    这种方案的缺点:
     1)查询员工属于哪些公司时,需要跨表查询。
     2)内嵌方的数量不能过多。  
     
     
4.一对多-内嵌id+查询字段型
场景:继续上面例子

查询公司下面的员工名字。这个功能是占用查询率70%以上,可以考虑使用内嵌id+查询字段型。数据样例如下:
{
"id":"45465516654",
"名称":"千度科技有限公司",
"注册地址":"北京市朝阳区",
"所有人":"张大大",
"注册日期":"2001-9-1",
"员工":[
    { "id":"员工id1", "姓名":"alun" },
    { "id":"员工id2", "姓名":"vivien" }
]
}

在查询时,不需要关联员工表,直接查询公司表即可。查询效率大大提升

小结:
使用内嵌id+查询字段型一对多设计时,前提有这几点
     1)一对多的多那方数量要多,最好是几千个不等。
     2)内嵌方的属性(字段)不宜过多。
     3)查询需求远大于修改需求。
     这种方案的缺点:
     1)修改时需要原子性操作。
     2)文档内容加大了,即产生了多余字段。如上面的【姓名】。

     
5.一对多-父级引用型
场景:电子商务的某商城与商铺的关系,商城里的商铺有几万到几十万,甚至几百万不等。

这么大量的数据,使用内嵌型有些力不从心。因为文档大小也有限制,如果太大的话,不禁mongodb报错,还可能导致网络延迟。因为一次查询的数据量过大。
所以这里使用父级引用,即子对象引用父对象id。数据样例如下:


商城表:
{
"id":"hijgio19089popik",
"名称":"某宝",
"注册地址":"杭州市",
"所有人":"某某某",
"注册日期":"2005-1-1"
}

 
商铺表 
{
"id":"84948654",
"名称":"alun的商铺",
"创建时间":"2018-2-1",
"过期时间":"2019-2-1",
"是否合法":true,
"商品品质级别":"高",
"是否个人商铺":true,
"持有人身份证号":"441922365587444468",
"某宝/某东id":"hijgio19089popik"
}

查询:查询属于某商城的商铺时,在商铺表通过某商城id可以查出所有的商铺。
修改:修改商铺、修改某商城表的属性都是单个表修改。

小结:
使用父级引用型一对多设计时,前提有这几点
     1)一对多的多那方数量很多,几万以上。
     2)对性能要求不高。


 三.索引设计

创建索引原则:

1.排序、分组的字段上创建索引

2.在查询条件的字段上创建索引

3.索引的数量不宜过多,建议控制在3个以内

4.创建的索引最大化覆盖SQL语句

1.单字段索引
为普通字段添加索引,并且为索引命名

db.集合名.createIndex( {"字段名": 1 },{"name":'idx_字段名'})

说明: (1)索引命名规范:idx_<构成索引的字段名>。如果字段名字过长,可采用字段缩写。
            (2)字段值后面的 1 代表升序;如是 -1 代表 降序。
       
eg:
db.doc_test.createIndex( {"productname": 1 },{"name":'idx_productname',background:true})


为内嵌字段添加索引

db.集合名.createIndex({"字段名.内嵌字段名":1},{"name":'idx_字段名_内嵌字段名',background:true})

eg:
db.doc_test.createIndex({"prodname.child_prodname":1},{"name":'idx_prodname_child_prodname',background:true})
 
 

2.组合索引
db.集合名.createIndex({"字段名1":-1,"字段名2":1},{"name":'idx_字段名1_字段名2',background:true})

eg:
db.doc_test.createIndex({"prodname":-1,"projectname":1},{"name":'idx_prodname_projectname',background:true})

说明:
当查询语句覆盖精确匹配,范围查询与排序的时候,{精确匹配字段,排序字段,范围查询字段} 创建这样的索引排序会更为高效

 

3.唯一索引
db.集合名.createIndex( {"字段名": 1 },{"name":'idx_字段名',unique:true,background:true},)

eg:
db.doc_test.createIndex( {"productname": 1 },{"name":'idx_productname',unique:true,background:true})


4.设置TTL索引(document自动过期删除)

db.集合名.createIndex( { "字段名": 1 },{ "name":'idx_字段名',expireAfterSeconds: 定义的时间,background:true} )

  说明 :expireAfterSeconds为过期时间(单位秒)  

eg:
db.doc_test.createIndex( { "productname": 1 },{ "name":'idx_productname',expireAfterSeconds: 3600,background:true} )
 

参考:https://www.cnblogs.com/meloncodezhang/p/13883234.html
https://dbaplus.cn/news-162-3666-1.html
https://zhuanlan.zhihu.com/p/150031851

------end-------

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值