You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

453 lines
12 KiB
Markdown

## 数据模型设计
### 1. 模型设计基础
- 数据模型设计的元素
- 实体
- 描述业务的主要数据集合
- 谁, 什么, 何时, 何地, 为何, 如何
- 属性
- 描述实体里面的单个信息
- 关系
- 描述实体与实体之间的数据规则
- 结构规则: 1-N, N-1, N-N
- 引用规则: 电话号码不能单独存在
- 传统模型设计
- ![传统模型设计.png](pic/传统模型设计.png)
- 架构师要产出 LDM 给开发者
- 逻辑模型
- 实体, 属性, 函数名称, 实体间关系
- 开发者: 第三范式下的物理模型
- 数据在库里尽量不可能存在冗余
### 2. JSON 文档模型的设计特点
- MongoDB 文档模型设计的三个误区
- 不需要模型设计
- MongoDB 应用用一个超级大文档来组织所有数据
- MongoDB 不支持关联或者事务
- 关于 JSON 文档模型设计
- 文档模型设计处于是物理模型设计阶段 (PDM)
- JSON 文档模型通过内嵌数组或引用字段来表示关系
- 文档模型设计不遵从第三范式, 允许冗余
- 流程: 概念建模 -> 逻辑建模 -> 文档模型 / 关系模型
- 为什么人们都说 MongoDB 是无模式
- 严格来说, MongoDB 同样需要概念/逻辑建模
- 文档建模设计的物理层结构可以和逻辑层类似
- MongoDB 无模式由来:
- **可以省略物理建模的具体过程**
- 概念模型 -> 逻辑模型 -(复用)-> 物理模型
- JSON 模型和逻辑模型对比
- ![JSON 模型和逻辑模型对比.png](pic/JSON%20模型和逻辑模型对比.png)
- **文档模型的设计原则: 性能和易用**
- 关系模型 vs 文档模型
- ![关系模型 vs 文档模型.png](pic/关系模型%20vs%20文档模型.png)
### 3. 文档模型三步走
- MongoDB 文档模型设计三步曲 - 总览
- ![MongoDB文档模型设计三步曲.png](pic/MongoDB文档模型设计三步曲.png)
- **第一步: 建立基础文档模型**
- 根据概念模型或者业务需求推导出逻辑模型 - 找到对象
- 列出实体之间的关系(及基数) - 明确关系
- 套用逻辑设计原则来决定内嵌方式 - 进行建模
- 完成基础模型构建
- 业务需求及逻辑模型 -(逻辑导向)-> 基础建模 -> 集合/字段/基础形状
- 举例
- 找到对象:
- Contacts
- Groups
- Address
- Portraits
- 明确关系
- 一个联系人有一个头像(1-1)
- 一个联系人可以有多个地址(1-N)
- 一个联系人可以属于多个组, 一个组可以有多个联系人(N-N)
- 表示:
- Groups(name) <-[N-N]-> Contacts (name,company,title) <-[1-N]-> Addresses(type,street,city,state,zip_code)
- Contacts(...) <-[1-1]-> Portraits(mine_type,date)
- 1-1 关系建模: portraits
- 基本原则: **一对一关系以内嵌为主, 作为子文档形式或者直接在顶级不涉及到数据冗余**
- 例外情况: **如果内嵌后导致文档大小超过16MB**
```json
name: "TJ Tang",
company: "TAPDATA",
title: "CTO",
portraits: {
mimetype: xxx,
date: xxxx
}
```
- 1-N 关系建模: Addresses
- 基本原则: **一对多关系同样以内嵌为主, 用数组来表示一对多不涉及到数据冗余**
- 例外情况: **内嵌后导致文档大小超过 16 MB, 数组长度太大(数万或更多) 数组长度不确定**
- Groups(name) <-[N-N]->
```json
name: "TJ Tang",
company: "TAPDATA",
title: "CTO",
portraits: {
mimetype: xxx,
date: xxxx
},
addresses: {
{type: home, ...},
{type: work, ...},
}
```
- N-N 关系建模: 内嵌数组模式
- 基本原则: **不需要映射, 一般用内嵌数组来表示一对多, 通过冗余来实现 N-N**
- 例外情况: **内嵌后导致文档大小超过 16 MB, 数组长度太大(数万或更多) 数组长度不确定**
```json
name: "TJ Tang",
company: "TAPDATA",
title: "CTO",
portraits: {
mimetype: xxx,
date: xxxx
},
addresses: {
{type: home, ...},
{type: work, ...},
},
groups: {
{name: :"Friends"}
{name: :"Surfers"}
}
```
- **第二步: 根据读写工况细化**
- 技术需求, 读写比例, 方式及数量 -(技术导向)-> 工况细化 -> [引用及关联]
- 考虑的问题
- 最频繁的数据查询模式
- 最常用的查询参数
- 最频繁的数据写入模式
- 读写操作的比例
- 数据量的大小
- 基于内嵌的文档模型
- 根据业务需求
- 使用引用来避免性能瓶颈
- 使用冗余来优化访问性能
- 举例
- 联系人管理应用分组需求
- 用于客户营销
- 有千万级联系人
- 需要频繁变动分组的信息, 如增加分组及修改名称及描述以及营销状态
- 一个分组可以有百万级联系人
```json
name: "TJ Tang",
company: "TAPDATA",
title: "CTO",
portraits: {
mimetype: xxx,
date: xxxx
},
addresses: {
{type: home, ...},
{type: work, ...},
},
groups: {
{name: :"Friends"}
{name: :"Surfers"}
}
```
```json
name: "Mona Jang",
company: "HUAXIA",
title: "DIRECTOR",
portraits: {
mimetype: xxx,
date: xxxx
},
addresses: {
{type: home, ...},
{type: work, ...},
},
groups: {
{name: :"Friends"}
{name: :"Surfers"}
}
```
- 一个分组(groups)的改动意味着百万级的DB操作
- 解决方案: Group 使用单独的集合
- 类似于关系型设计
- 用 id 或者唯一键关联
- 使用 $lookup 来提供一次查询多表的能力(类似关联)
```json
name: "Mona Jang",
company: "HUAXIA",
title: "DIRECTOR",
portraits: {
mimetype: xxx,
date: xxxx
},
addresses: {
{type: home, ...},
{type: work, ...},
},
group_ids: [1,2,3]
```
```json
Groups
group_id
name
```
- 使用 group_id 进行关联
- 此时查询语句
```spring-mongodb-json
db.contacts.aggregate([
{
$lookup:{
from: "groups",
localField: "group_ids",
foreignField: "group_id",
as: "groups"
}
}])
```
- aggregate 的 $lookup 来实现关联查询
- 例子2: 联系人的头像: 引用模式
- 头像使用高保真, 大小在 5MB - 10MB
- 头像一旦上传, 一个月不可变更
- 基础信息查询(不含头像) 和 头像查询的比例为 9 1
- 建议: 使用引用方式, 把头像数据放到另外一个集合, 可以显著提升 90% 的查询效率
```json
name: "Mona Jang",
company: "HUAXIA",
title: "DIRECTOR",
addresses: {
{type: home, ...},
{type: work, ...},
},
group_ids: [1,2,3]
```
```json
Contact_Portrait: {
_id: 123,
mimetype: xxx,
data: xxxx
}
```
- 什么时候该使用引用方式?
- 内嵌文档太大, 数 MB 或者超过 16MB
- 内嵌文档或数组元素会频繁修改
- 内嵌数组元素会持续增长并且没有封顶
- MongoDB 引用设计的限制
- MongoDB 对使用引用的集合之间并无主外键检查
- MongoDB 使用聚合框架的 $lookup 来模仿关联查询
- $lookup 只支持 left outer join
- $lookup 的关联目标(from) 不能是分片表
- **第三步: 套用设计模式**
- 文档模式: 无范式, 无思维定式, 充分发挥想象力
- 设计模式: 实战过屡试不爽的设计技巧, 快速应用
- 举例: 一个 loT 场景的分桶设计模式, 可以帮助把存储空间降低10倍并且查询效率提升数十倍
- 经验和学习 -(模式导向)-> 套用设计模式 -> 优化的模型
- 问题: 物联网场景下的海量数据处理 - 飞机监控数据
```json
{
"_id" : "20160101050000:CA2790",
"icao": "CA2790",
"callsign": "CA2790",
"ts": ISODate("2016-01-01T05:00:00.000+0000"),
"events": {
"a": 31418,
"b" : 173,
"s" :91,
"v" : 80
}
}
```
- 实际问题: 520亿条, 10TB - 海量数据
- 10万架飞机
- 1年的数据
- 每分钟1条
- ![飞机-海量数据.png](pic/飞机-海量数据.png)
- 52.6B = 100000 * 365 * 24 * 60
- 6364GB = 100000 * 365 * 24 * 60 * 130
- 4503GB = 100000 * 365 * 24 * 60 * 92
- 解决方案: 分桶设计
```json
{
" id" :"20160101050000:WG9943",
"icao":"WG9943",
"ts":ISODate("2016-01-01T05:00:00.000+0000"),
"events":[
{
"a":24293, "b":319, "p":[41,70], "s":56,
"t": ISODate("2016-01-01T05:00:00.000+0000")
},
{
"a":33663, "b":134, "p":[-38, -30], "s":385,
"t": ISODate("2016-01-01T05:00:01.000+0000")
}
]
}
```
- 60 events == 1小时的数据
- 一个文档: 一架飞机一个小时的数据
- ![飞机-海量数据-分桶.png](pic/飞机-海量数据-分桶.png)
- 可视化表现24小时的飞行数据
- 1440次读
- 模式小结:
- 场景
- 时序数据
- 物联网
- 智慧城市
- 智慧交通
- 痛点
- 数据点采频繁, 数据量太多
- 设计模式的方案及优点
- 利用文档内嵌数组, 将一个时间段的数据聚合到一个文档里
- 大量减少文档数量
- 大量减少索引占用空间
### 4. 设计模式集锦
- 问题: 大文档, 很多字段, 很多索引
```json
{
title: "Dunkirk",
release USA:"2017/07/23",
release UK:"2017/08/01",
release France:"2017/08/01",
release Festival San Jose:"2017/07/22"
}
```
- 需要很多索引
```json
{release_UsA:1}
{release_UK:1}
{release_France:1}
{release_Festival_San_Jose:1 }
```
- 电影的上映时间
- 解决方法: 列转行
```json
{
title: "Dunkirk",
releases:[
{country:"USA",date:"2017/7/23"},
{country:"Uk",date:"2017/08/01”}
]
}
```
- 转换后: 字段数变少了
- 创建索引: db.movies.createIndex({"releases.country":1, "releases.date":1})
- 模式小结:
- 场景: 产品属性多, 多语言(多国家属性)
- 痛点: 文档中有很多类似的字段, 会用于组合查询搜索, 需要创建很多索引
- 设计模式方案及优点: 转化为数组, 一个索引解决所有查询问题
- 问题: 模型灵活了, 如何管理文档的不同版本
```json
"id":ObiectId("5de26f197edd62c5d388babb"),
"name":"TJ",
"Tapdata""company":
```
- v1.0
```json
"id":obiectId("5de26f197edd62c5d388babb")
"name":"TJ"
"company":"Tapdata"
"wechat":"titang826"
"schema_version": "2.0"
```
- v2.0
- 解决方案schema_version 使用这个字段进行区分标识
- 模式小结:版本字段
- 场景: 任何有版本衍生的数据库
- 痛点: 文档模型格式多, 无法知道其合理性, 升级时候需要更新太多文档
- 设计模式及优点: 增加一个版本号字段, 快速过滤掉不需要升级的文档, 升级时候对不同的文档做不同的升级
- 问题: 统计网页点击流量
- 每访问一个页面就会产生一次数据库计数更新操作
- 统计数字准确性并不十分重要
- 解决方案: 用近似计算
- 每隔 10(X)次写一次
- Increment by 10(X)
- {$inc:{views:1}}
- if random(0,9) == 0 increment by 10
- 模式小结: 近似计算
- 场景:
- 网页计数
- 各种结果不需要准确的排名
- 痛点:
- 写入太频繁, 消耗系统资源
- 设计模式方案及优点
- 间隔写入, 每隔10次或者100次
- 大量减少写入需求
- 问题: 业绩排名, 游戏排名, 商品统计等精确统计
- 热销榜: 某个商品今天卖了多少, 这个星期卖了多少, 这个月卖了多少?
- 电影排行: 观影者, 场次统计
- 传统解决方案: 通过聚合计算
- 痛点: 消耗资源多, 聚合计算时间长
- 解决方案: 用预聚合字段
```json
{
product:"Bike"
sku:“abc123456”
quantitiy:20394,
daily_sales: 40,
weekly sales:302,
monthly_sales:1419
}
```
```spring-mongodb-json
db.inventory.update({_id:123},{
$inc:{
quantity:-1,
daily_sales: 1,
weekly_sales:1,
monthly_sales: 1
}
})
```
- 模式小结: 预聚合
- 场景: 准确排名, 排行榜
- 痛点: 统计计算耗时, 计算时间长
- 设计模式方案及优点: 模型中直接增加统计字段, 每次更新数据时候同时更新统计值