MongoDB 数据流监听&性能监控&性能调优
一、Chang Streams数据流事件
Change Stream是指数据的变化事件流,用于追踪MongoDB变动记录,类似于关系数据库的触发器,但原理不完全相同:
Change Stream 是基于 oplog 实现的,提供推送实时增量的推送功能。它在 oplog 上开启一个 tailable cursor 来追踪所有复制集上的变更操作,最终调用应用中定义的回调函数,我们可以监听回调函数来获取实时变动日志并进行业务处理。
被追踪的变更事件主要包括:
- insert/update/delete:插入、更新、删除;
- drop:集合被删除;
- rename:集合被重命名;
- dropDatabase:数据库被删除;
- invalidate:drop/rename/dropDatabase 将导致 invalidate 被触发, 并关闭 change stream;
使用场景
- 跨集群的变更复制——在源集群中订阅 Change Stream,一旦得到任何变更立即写入目标集群。
- 微服务联动——当一个微服务变更数据库时,其他微服务得到通知并做出相应的变更。
- 如 监听到某个集合发生了数据变动,就进行相应处理,如 发送用户通知、日志分析等。
- 其他任何需要系统联动的场景。
Change Stream会采用 “readConcern:majority”这样的一致性级别,保证写入的变更不会被回滚。因此:
- **未开启 majority readConcern **的集群无法使用 Change Stream;
- 当集群无法满足 {w: “majority”} 时,不会触发 Change Stream。
- 写入数据时如果 无法满足 超过半数节点确认,就不会触发 Change Stream
案例 1.监控
我们可以通过ChangeStreams监听某个集合(如 用户表),一旦集合发生了数据变化,就会立即将变更的消息实时推送出去。
案例 2.分析平台
我们需要增量数据去分析用户行为,可以订阅ChangeStreams将消息过滤后转发给下游的计算平台, 比如 类似 Flink、Spark 等。
案例 3.数据同步
- 从源复制集上复制一份完整的数据,搭建一个新的复制集 用于冗余备份,但是不对外提供服务
- 订阅ChangeStreams回调函数后,将增量消息 写入到 冗余备份的复制集 完成同步。
该方法可以实现 跨地域数据同步,当源复制集宕机后,该备份复制集上的数据依然存在并且可以正常对外提供服务。
案例 4.消息推送
假如用户想实时了解公交车的信息,那么公交车的位置每次变动,都实时推送变更的信息给想了解的用 户,用户能够实时收到公交车变更的数据,非常便捷实用。
注意事项
- Change Stream 依赖于 oplog,因此中断时间不可超过 oplog 回收的最大时间窗,否则会造成部分数据丢失。
- 由于oplog是一个固定集合,超过了固定大小后会覆写之前的数据。如果Change Stream出现了中断的情况,在oplog覆写数据之前还没有恢复,就会出现部分数据丢失。
- 在执行 update 操作时,如果只更新了部分数据,那么 Change Stream 通知的也是增量部分;
- 删除数据时通知的仅是删除数据的 _id。
Chang Stream整合Spring Boot
项目代码:https://files.javaxing.com/Java%08Demo/spring-boot-mongo-demo-02.zip
引入依赖
1 | <!--spring data mongodb--> |
修改配置文件 application.yaml
连接配置文档 https://www.mongodb.com/docs/manual/reference/connection-string/
我们可以根据文档里面的属性,设置 uri参数,如 复制集,分片集等其他参数。
1 | spring: |
增加配置文件
设置监听器类型,指定过滤监听类型
1 |
|
设置监听器
可以在监听器里面做一系列的业务逻辑,如 推送消息到下游服务器等。
1 |
|
实验测试
插入文档后,Change Stream 监听器 监听到数据,并成功打印出来。
二、MongoDB性能监控工具
1. mongostat
mongostat是MongoDB自带的监控工具,要求执行用户需具备clusterMonitor角色权限。mongostat所监视的对象是数据库进程。mongostat常用于查看当前的QPS/内存使用/连接数,以及多个分片的压力分布。
MongoDB 4.4版本及以上安装包不会附带工具包,所以需要另外下载,请根据实际需求下载:
Centos7~8 X86 64位:https://files.javaxing.com/mongoDB/mongodb-database-tools-rhel80-x86_64-100.9.4.tgz
官网下载(可以选择更多的版本):https://www.mongodb.com/try/download/database-tools
1 | mongostat -h localhost --port 27010 -uxing -p123123 --authenticationDatabase=admin --discover -n 300 2 |
参数说明:
- -h:指定监听的主机,分片集群模式下指定到一个mongos实例,也可以指定单个mongod,或者复制集的多个节点。
- –port:接入的端口,如果不提供则默认为27017。
- -u:接入用户名,等同于-user。
- -p:接入密码,等同于-password。
- –authenticationDatabase:鉴权数据库。
- –discover:启用自动发现,可展示集群中所有分片节点的状态。
- -n 300 2:表示输出300次,每次间隔2s。也可以不指定“-n 300”,此时会一直保持输出。
指标说明
指标名 | 说明 |
---|---|
inserts | 每秒插入数 |
query | 每秒查询数 |
update | 每秒更新数 |
delete | 每秒删除数 |
getmore | 每秒getmore数 |
command | 每秒命令数,涵盖了内部的一些操作 |
%dirty | WiredTiger缓存中脏数据百分比 |
%used | WiredTiger 正在使用的缓存百分比 |
flushes | WiredTiger执行CheckPoint的次数 |
vsize | 虚拟内存使用量 |
res | 物理内存使用量 |
qrw | 客户端读写等待队列数量,高并发时,一般队列值会升高 |
arw | 客户端读写活跃个数 |
netIn | 网络接收数据量 |
netOut | 网络发送数据量 |
conn | 当前连接数 |
set | 所属复制集名称 |
repl | 复制节点状态(主节点/二级节点……) |
time | 时间戳 |
mongostat需要关注的指标主要有如下几个:
- 插入、删除、修改、查询的速率是否产生较大波动,是否超出预期。
- qrw、arw:队列是否较高,若长时间大于0则说明此时读写速度较慢。
- conn:连接数是否太多。
- dirty:百分比是否较高,若持续高于10%则说明磁盘I/O存在瓶颈。
- netIn、netOut:是否超过网络带宽阈值。
- repl:状态是否异常,如PRI、SEC、RTR为正常,若出现REC等异常值则需要修复。
2. mongotop
mongotop命令可用于查看数据库的热点表,通过观察mongotop的输出,可以判定是哪些集合占用了大部分读写时间。mongotop与mongostat的实现原理类似,同样需要clusterMonitor角色权限。
如果没有权限的话,会提示 没有使用权限。如果只是为了方便测试,可以给与root 最高权限。
1 | mongotop -h localhost --port 27010 -uxing -p123123 --authenticationDatabase=admin |
默认情况下,mongotop会持续地每秒输出当前的热点表
指标说明
指标名 | 说明 |
---|---|
ns | 集合名称空间 |
total | 花费在该集合上的时长 |
read | 花费在该集合上的读操作时长 |
write | 花费在该集合上的写操作时长 |
mongotop通常需要关注的因素主要包括:
- 热点表操作耗费时长是否过高。这里的时长是在一定的时间间隔内的统计值,它代表某个集合读写操作所耗费的时间总量。在业务高峰期时,核心表的读写操作一般比平时高一些,通过mongotop的输出可以对业务尖峰做出一些判断。
- 是否存在非预期的热点表。一些慢操作导致的性能问题可以从mongotop的结果中体现出来
mongotop的统计周期、输出总量都是可以设定的
1 | 最多输出100次,每次间隔时间为2s |
3. Profiler模块 慢查询日志
Profiler模块可以用来记录、分析MongoDB的详细操作日志。默认情况下该功能是关闭的,对某个业务库开启Profiler模块之后,符合条件的慢操作日志会被写入该库的system.profile集合中。Profiler的设计很像代码的日志功能,其提供了几种调试级别:
级别 | 说明 |
---|---|
0 | 日志关闭,无任何输出 |
1 | 部分开启,仅符合条件(时长大于slowms)的操作日志会被记录 |
2 | 日志全开,所有的操作日志都被记录 |
对当前的数据库开启Profiler模块:
1 | 将level设置为2,此时所有的操作会被记录下来。 |
- slowms是慢操作的阈值,单位是毫秒;
- 如设置 slowms = 500ms,那超过500ms的查询就会写入到日志中,我们可以根据日志来优化
- sampleRate表示日志随机采样的比例,1.0则表示满足条件的全部输出。
如果希望只记录时长超过500ms的操作,则可以将level设置为1
1 | rs0:PRIMARY> db.setProfilingLevel(1,500) |
还可以进一步设置随机采样的比例
1 | rs0:PRIMARY> db.setProfilingLevel(1,{slowms:500,sampleRate:0.5}) |
每次复制集重启后,会默认关闭日志,避免写Profiler日志影响性能。
查看操作日志
开启Profiler模块之后,可以通过system.profile集合查看最近发生的操作日志
1 | rs0:PRIMARY> db.system.profile.find().limit(5).sort({ts:-1}).pretty() |
这里需要关注的一些字段主要如下所示:
- op:操作类型,描述增加、删除、修改、查询。
- ns:名称空间,格式为{db}.{collection}。
- Command:原始的命令文档。
- Cursorid:游标ID。
- numYield:操作数,大于0表示等待锁或者是磁盘I/O操作。
- nreturned:返回条目数。
- keysExamined:扫描索引条目数,如果比nreturned大出很多,则说明查询效率不高。docsExamined:扫描文档条目数,如果比nreturned大出很多,则说明查询效率不高。
- locks:锁占用的情况。
- storage:存储引擎层的执行信息。
- responseLength:响应数据大小(字节数),一次性查询太多的数据会影响性能,可以使用limit、batchSize进行一些限制。
- millis:命令执行的时长,单位是毫秒。
- planSummary:查询计划的概要,如IXSCAN表示使用了索引扫描。
- execStats:执行过程统计信息。
- ts:命令执行的时间点。
根据这些字段,可以执行一些不同维度的查询。比如查看执行时长最大的10条操作记录
查看某个集合中的update操作日志
1 | db.system.profile.find().limit(10).sort({millis:-1}).pretty() |
查看某个集合中的update操作日志
1 | db.system.profile.find({op:"update",ns:"shop.user"}) |
注意事项
system.profile是一个1MB的固定大小的集合,随着记录日志的增多,一些旧的记录会被滚动删除。
在线上开启Profiler模块需要非常谨慎,这是因为其对MongoDB的性能影响比较大。建议按需部分开启,同时slowms的值不要设置太低。
sampleRate的默认值是1.0,该字段可以控制记录日志的命令数比例,但只有在MongoDB 4.0版本之后才支持。
Profiler模块的设置是内存级的,重启服务器后会自动恢复默认状态。
4. 运行时状态 db.currentOp()
Profiler模块所记录的日志都是已经发生的事情,db.currentOp()命令则与此相反,它可以用来查看数据库当前正在执行的一些操作。想象一下,当数据库系统的CPU发生骤增时,我们最想做的无非是快速找到问题的根源,这时db.currentOp就派上用场了。
db.currentOp()读取的是当前数据库的命令快照,该命令可以返回许多有用的信息,比如:
操作的运行时长,快速发现耗时漫长的低效扫描操作。
执行计划信息,用于判断是否命中了索引,或者存在锁冲突的情况。
操作ID、时间、客户端等信息,方便定位出产生慢操作的源头。
对示例操作的解读如下:
(1)从ns、op字段获知,当前进行的操作正在对test.items集合执行update命令。
(2)command字段显示了其原始信息。其中,command.q和command.u分别展示了update的查询条件和更新操作。
(3)”planSummary”:”COLLSCAN” 说明情况并不乐观,update没有利用索引而是正在全表扫描。(4)microsecs_running:NumberLong(186070)表示操作运行了186ms,注意这里的单位是微秒。
优化方向:
- value字段加上索引
- 如果更新的数据集非常大,要避免大范围update操作,切分成小批量的操作
opid表示当前操作在数据库进程中的唯一编号。如果已经发现该操作正在导致数据库系统响应缓慢,则可以考虑将其“杀”死
1 | db.killOp(4001) |
db.currentOp默认输出当前系统中全部活跃的操作,由于返回的结果较多,我们可以指定一些过滤条件:
- 查看等待锁的增加、删除、修改、查询操作
1 | db.currentOp({ |
- 查看执行时间超过1s的操作
1 | db.currentOp({ |
- 查看test数据库中的操作
1 | db.currentOp({ |
currentOp命令输出说明
currentOp.type:操作类型,可以是op、idleSession、idleCursor的一种,一般的操作信息以op表示。其为MongoDB 4.2版本新增功能。
currentOp.host:主机的名称。currentOp.desc:连接描述,包含connectionId。currentOp.connectionId:客户端连接的标识符。currentOp.client:客户端主机和端口。currentOp.appName:应用名称,一般是描述客户端类型。
currentOp.clientMetadata:关于客户端的附加信息,可以包含驱动的版本。currentOp.currentOpTime:操作的开始时间。MongoDB 3.6版本新增功能。
currentOp.lsid:会话标识符。MongoDB 3.6版本新增功能。
currentOp.opid:操作的标志编号。
currentOp.active:操作是否活跃。如果是空闲状态则为false。
currentOp.secs_running:操作持续时间(以秒为单位)。
- 通过该参数实时可以找到 慢查询命令
currentOp.microsecs_running:操作持续时间(以微秒为单位)。
currentOp.op:标识操作类型的字符串。可能的值是:”none” “update” “insert””query””command” “getmore” “remove” “killcursors”。其中,command操作包括大多数命令,如createIndexes和findAndModify。
currentOp.ns:操作目标的集合命名空间。
currentOp.command:操作的完整命令对象的文档。如果文档大小超过1KB,则会使用一种$truncate形式表示。
currentOp.planSummary:查询计划的概要信息。
currentOp.locks:当前操作持有锁的类型和模式。
currentOp.waitingForLock:是否正在等待锁。
currentOp.numYields:当前操作执行yield(让步)的次数。一些锁互斥或者磁盘I/O读取都会导致该值大于0。
currentOp.lockStats:当前操作持有锁的统计。
currentOp.lockStats.acquireCount:操作以指定模式获取锁的次数。
currentOp.lockStats.acquireWaitCount:操作获取锁等待的次数,等待是因为锁处于冲突模式。acquireWaitCount小于或等于acquireCount。
currentOp.lockStats.timeAcquiringMicros:操作为了获取锁所花费的累积时间(以微秒为单位)。timeAcquiringMicros除以acquireWaitCount可估算出平均锁等待时间。
currentOp.lockStats.deadlockCount:在等待锁获取时,操作遇到死锁的次数。
注意事项
- db.currentOp返回的是数据库命令的瞬时状态,因此,如果数据库压力不大,则通常只会返回极少的结果。
- 如果启用了复制集,那么currentOp还会返回一些复制的内部操作(针对local.oplog.rs),需要做一些筛选。
- db.currentOp的结果是一个BSON文档,如果大小超过16MB,则会被压缩。可以使用聚合操作$currentOp获得完整的结果。
5. 性能问题排查参考案例
排查性能的几个步骤:
- mongostat 监控MongoDB进程的一个实时情况,如果实时数据没有问题的话在去分析 热点数据
- mongotop 分析热点数据库和集合情况
- 慢查询日志,分析出哪条命令占用时间较长
- db.currentOp() 实时日志
- 根据实时日志里面的currentOp.secs_running参数,分析出哪条命令占用时间较长
三、MongoDB性能调优
1. MongoDB开发规范
(1)命名原则。数据库、集合命名需要简单易懂,数据库名使用小写字符,集合名称使用统一命名风格,可以统一大小写或使用驼峰式命名。数据库名和集合名称均不能超过64个字符。
(2)集合设计。对少量数据的包含关系,使用嵌套模式有利于读性能和保证原子性的写入。对于复杂的关联关系,以及后期可能发生演进变化的情况,建议使用引用模式。
(3)文档设计。避免使用大文档,MongoDB的文档最大不能超过16MB。如果使用了内嵌的数组对象或子文档,应该保证内嵌数据不会无限制地增长。在文档结构上,尽可能减少字段名的长度,MongoDB会保存文档中的字段名,因此字段名称会影响整个集合的大小以及内存的需求。一般建议将字段名称控制在32个字符以内。
(4)避免建立过多的索引,单个集合建议不超过10个索引。MongoDB对集合的写入操作很可能也会触发索引的写入,从而触发更多的I/O操作。无效的索引会导致内存空间的浪费,因此有必要对索引进行审视,及时清理不使用或不合理的索引。遵循索引优化原则,如覆盖索引、优先前缀匹配等,使用explain命令分析索引性能。
(5)分片设计。对可能出现快速增长或读写压力较大的业务表考虑分片。分片键的设计满足均衡分布的目标,业务上尽量避免广播查询。应尽早确定分片策略,最好在集合达到256GB之前就进行分片。如果集合中存在唯一性索引,则应该确保该索引覆盖分片键,避免冲突。为了降低风险,单个分片的数据集合大小建议不超过2TB。
(6)升级设计。应用上需支持对旧版本数据的兼容性,在添加唯一性约束索引之前,对数据表进行检查并及时清理冗余的数据。新增、修改数据库对象等操作需要经过评审,并保持对数据字典进行更新。
(7)考虑数据老化问题,要及时清理无效、过期的数据,优先考虑为系统日志、历史数据表添加合理的老化策略,如使用TTL索引,或者以日期形式存储数据定期删除等。
(8)数据一致性方面,非关键业务使用默认的WriteConcern:1(更高性能写入);对于关键业务类,使用WriteConcern:majority保证一致性(性能下降)。如果业务上严格不允许脏读,则使用ReadConcern:majority选项。
(9)使用update、findAndModify对数据进行修改时,如果设置了upsert:true,则必须使用唯一性索引避免产生重复数据。
(10)业务上尽量避免短连接,使用官方最新驱动的连接池实现,控制客户端连接池的大小,最大值建议不超过200。
(11)对大量数据写入使用Bulk Write批量化API,建议使用无序批次更新。
(12)优先使用单文档事务保证原子性,能不用多文档事务则不用事务,如果需要使用多文档事务,则必须保证事务尽可能小,一个事务的执行时间最长不能超过60s。
(13)在条件允许的情况下,利用读写分离降低主节点压力。对于一些统计分析类的查询操作,可优先从节点上执行。
(14)考虑业务数据的隔离,例如将配置数据、历史数据存放到不同的数据库中。微服务之间使用单独的数据库,尽量避免跨库访问。
(15)维护数据字典文档并保持更新,提前按不同的业务进行数据容量的规划。
2. MongoDB调优
三大导致MongoDB性能不佳的原因:
慢查询
- 通过慢查询日志 进行优化相关命令
连接阻塞等待
硬件资源不足
第一条和第二条 通常是因为模型/索引设计不佳导致的,所以在设计模型和设计索引很重要。
我们将以Mongo 客户端,驱动,服务端,硬件端 等多个角度来分析,在生产环境中需要注意哪些细节来进行调优。
1)MongoDB 客户端
readPreference 读偏好
通过设置读偏好,可以设置 客户端从主节点读取还是从 从节点读取数据。
readPreference决定使用哪一个类型节点来发起的读请求。 可选值如下:
- primary: 只选择主节点,默认模式;
- primaryPreferred:优先选择主节点,如果主节点不可用则选择从节点;
- secondary:只选择从节点;
- secondaryPreferred:优先选择从节点, 如果从节点不可用则选择主节点;
- nearest:根据客户端对节点的 Ping 值判断节点的远近,选择从最近的节点读取。
合理的 ReadPreference 可以极大地扩展复制集的读性能,降低访问延迟。
不希望连接到远距离节点的话,可以用以下几种办法:
- 使用nearest方式,会选择客户端ping最低的节点
- 通过标签TAG控制可选节点,客户端会选择 绑定tag的节点
- 将远距离节点设置成 隐藏节点,设置隐藏后 该节点对客户端不可见
writeConcern
writeConcern 决定一个写操作落到多少个节点上才算成功。
- {w: 0} 对客户端的写入不需要发送任何确认,适用于性能要求高,但不关注正确性的场景
- {w: 1} 默认为1,数据写入到Primary就向客户端发送确认
- {w: majority} 数据写入到副本集大多数成员后向客户端发送确认,适用于对数据安全性要求比较高的场景,该选项会降低写入性能
非关键业务使用默认的WriteConcern:1(更高性能写入);对于关键业务类,使用WriteConcern:majority保证一致性(性能下降)。
readConcern
readConcern 决定这个节点上的数据哪些是可读的。可选值有:
available:读取所有可用的数据;
local:读取所有可用且属于当前分片的数据;
majority:读取在大多数节点上提交完成的数据;
- 可以有效的避免脏读,性能较低但是却比较安全可靠
- 使用majority 只能查询到已经被多数节点确认过的数据
linearizable:可线性化读取文档,仅支持从主节点读;
snapshot:读取最近快照中的数据,仅可用于多文档事务;
2)Driver驱动
连接池
maxPoolSize:连接池最大连接数,默认100,可以改成200但是最好别超过200
minPoolSize: 连接池保留的最小连接数,默认0
maxldelTimeMS:在连接删除前允许的空闲时间
连接等待:连接数超过maxPoolSize。只调大连接数不一定有用,优化查询性能才是最主要的
预留连接:设置合理的minPoolSize,避免反复创建连接,消耗更多资源。
为避免writeConcern 写入失败导致连接阻塞,可以设置超时时间 wtimeout: writeConcern ,当连接阻塞超时后 自动断开连接。
DB兼容性
驱动与DB的兼容性,DB用的版本 和 客户端驱动 必须保持一致。如 DB用的4.4 客户端也必须4.4,否则可能出现一些不可预料的问题。
3)服务端存储
MongoDB使用的是WireTiger存储引擎,持久化机制有2个,Checkpoints 快照刷盘,Journal 增量同步日志。
Checkpoints 默认每隔60秒 进行快照持久化刷盘。
Journal 写入的命令会增量存储在 Journal日志中,每隔100ms 进行持久化。 如果在写入命令的时候就指定Journal:true的话,那写入数据后会马上持久化。
4)操作系统
选择合适的内核文件系统和操作系统:Linux推荐EXT4/XFS,不推荐EXT3 生产会出现问题。
启用NTP:保证每个节点的时间完全同步一致,避免出现时差问题
禁用atime、Huge page、SELinux、关闭磁盘预读readahead。
修改Linux ulimit配置,提高MongoDB服务端进程在Linux中最大内存使用量和打开文件数量等限制。
5)硬件配置
- 要高效使用MongoDB 必须确保足够的内存,因为MongoDB非常吃内存,默认一启动占用一半以上的内存。
- 必须使用SSD或更高级别的硬盘
- 有条件的话可以使用RAID10 独立磁盘冗余阵列
- 禁用NUMA 非一致性内存访问,该选项是为了避免出现WireTiger的缓存和内存 数据不一致的情况
3. MongoDB数据建模
不要一上来咔咔咔的就造,而是要精心考量多方面后再着手设计,设计数据模型和索引是非常重要的,良好的设计可以提高查询效率,减少多文档事务。
1)最简易模型
内嵌文档一对一关系
下面的示例包含2个文档集合,一个用户 一个地址。
1 | // 用户文档 |
如果我们需要查询用户信息顺带查询出地址的话,可以将地址内嵌到用户里边
1 | // 嵌入式文档 |
内嵌文档一对多关系
1 | // patron document |
如果地址出现了多个的情况,就可以将地址以数组的形式内嵌到用户里面
1 | { |
内嵌引用ID 实现一对多关系
此时我们需要做一个图书管理模型,需要通过作者信息去查他名下所有的书籍。
1 | { |
上面的模型明显是不合理的,不合理的地方一共有2点:
1、将 作者信息放到了book集合里,作者信息可能会存在变动,一旦变动需要修改所有的文档作者信息。
2、无法根据作者信息 查询他名下的所有书籍(加索引也可以 但是效率不高)
合理的做法
1 | // 作者集合 |
将作者单独创建一个集合,图书创建一个集合,然后在图书里面增加publisher_id字段用于存储作者ID。
等同于 关系型数据库(如MySQL)的 多表ID关联一样,通过ID将多个表之间给逻辑关联起来。
2)朋友圈评论点赞设计
需求:模拟设计出 类似微信朋友圈的 数据模型,可以清晰的看到 哪些朋友点赞了,评论了。
1 | // 用户集合 |
我们创建了个用户集合和朋友圈记录集合,记录集合存praise_uid_list(点赞列表)、comment_msg_list(评论列表)。将用户ID和对应的评论内容存于上述字段中。
MongoDB的文档模型固然强大,但是也不能一股脑的全部嵌套进去,要考虑到 实际应用场景 合理的设计文档模型。如果不管三七二十一全都往里面嵌套,反而会遇到更麻烦的问题。
3)电影票多渠道价格
需求是基于电影票售卖的不同渠道价格存储。某一个场次的电影,不同的销售渠道对应不同的价格。
数据字段:
- 场次信息;
- 播放影片信息;
- 渠道信息,与其对应的价格;
- 渠道数量最多几十个;
业务查询有两种:
- 根据电影场次,查询某一个渠道的价格;
- 根据渠道信息,查询对应的所有场次信息;
比较糟糕的做法
1 | { |
直接将渠道的价格 内嵌到文档里面。后续增加价格的话,就增加渠道商字段。
根据电影场次,查询某一个渠道的价格
- 建立createIndex({scheduleId:1, movie:1})索引,虽然没有对price创建索引,但是能如果查询到某个电影,在通过该电影找到price字段 捞出渠道商的价格,也是可行的,就是效率不高
根据渠道信息,查询对应的所有场次信息
- 想通过渠道来查询所有电影的话,就需要为渠道商增加索引,但是总不能一个渠道商就创建一个索引吧?如果有几十个渠道商,不得创建几十个索引?那简直是一场噩梦
完善的做法
1 | { |
将供应商和价格字段放成一个小集合,放在provider数组里面,然后塞到电影票的文档里面。数组是可以创建索引的,所以也不用担心查询速度。
- 根据电影场次,查询某一个渠道的价格
- 建立createIndex({scheduleId:1, movie:1, “provider.channel”:1})索引;
- 根据渠道信息,查询对应的所有场次信息
- 建立createIndex({“provider.channel”:1})索引;
4)物联网时序数据库建模
本案例非常适合与IoT场景的数据采集,结合MongoDB的Sharding能力,文档数据结构等优点,可以非常好的解决物联网使用场景。
需求
案例背景是来自真实的业务,美国州际公路的流量统计。数据库需要提供的能力:
存储事件数据
提供分析查询能力
理想的平衡点:
- 内存使用
- 写入性能
- 读取分析性能
可以部署在常见的硬件平台上
每个事件用一个独立的文档存储
1 | { |
非常“传统”的设计思路,每个事件都会写入一条同样的信息。多少的信息,就有多少条数据,数据库会变得非常庞大
数据采集操作全部是Insert语句
每分钟的信息用一个独立的文档存储(存储平均值)
1 | { |
- 可行,但是数据不够精确。数据精度降为一分钟;
- 数据采集操作基本是Update语句;
每分钟的信息用一个独立的文档存储(秒级记录)
1 | { |
- 将一分钟划分成60份,然后将这60份以数组的形式都存在speed中。 由于时间是恒定的,如果要取出里面哪一秒的数据,直接按size -1去取就行了,因为默认是从0开始计算的
- 数据采集操作基本是Update语句;
每小时的信息用一个独立的文档存储(秒级记录)
1 | { |
- 将一小时划分成60份,因为每小时有60分钟恒定的,所以可以通过size找到具体第几分钟的数据。在通过几分钟 找到里面的第几秒
- 数据采集操作基本是Update语句
- WiredTiger每60秒持久化一次,如果采用每小时一个文档的方案,他需要 反反复复持久化后再load到PageCache 内存缓存中,修改后再持久化,性能较低。