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-------