惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Y
Y Combinator Blog
有赞技术团队
有赞技术团队
J
Java Code Geeks
H
Hackread – Cybersecurity News, Data Breaches, AI and More
美团技术团队
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Hugging Face - Blog
Hugging Face - Blog
人人都是产品经理
人人都是产品经理
酷 壳 – CoolShell
酷 壳 – CoolShell
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
C
Check Point Blog
博客园 - 【当耐特】
The GitHub Blog
The GitHub Blog
Recent Announcements
Recent Announcements
The Cloudflare Blog
Microsoft Azure Blog
Microsoft Azure Blog
腾讯CDC
Vercel News
Vercel News
IT之家
IT之家
MyScale Blog
MyScale Blog
博客园_首页
Martin Fowler
Martin Fowler
WordPress大学
WordPress大学
罗磊的独立博客

MadLife

算法部署从docker到K8s 聊一聊:Prometheus监控系统 聊一聊:AI时代的前夜 Web服务打包exe 聊一聊:我眼中的数据中台 内网ubuntu环境下离线部署K8s 聊一聊:FaaS 在大规模网络爬虫的实践 聊一聊:Golang 使用sync.Pool 降低GC压力 聊一聊:Python和Golang的垃圾回收 聊聊数据库的那些索引 源码阅读——robfig/cron GitHub Actions+Serverless搭建博客 聊一聊:敏捷开发 聊一聊:常见的分布式锁 2021书单 sentry部署 MongoDB——TTL索引 S3协议入门 使用 Go 客户端控制 Kubernetes
聊聊 MongoDB
Yance Huang · 2021-08-21 · via MadLife

聊聊 MongoDB

MongoDB 是目前主流的 NoSQL 数据库之一。由于历史原因,接手的一个项目底层大量使用了 MongoDB 。在学生时代对 MongoDB 也有一些了解,但是真正用到生产项目里面还是第一次。不出意外的,在使用过程中也伴随着许多不适应,也不止一次的怀念便捷的 ORM、熟悉的 SQL (甚至 InfluxDB 这种时序数据库也是兼容SQL查询的) 。这里主要讲一下,在过去一年里使用 MongoDB 中的一些新认识以及遇到的问题。

数据建模

数据建模中的关键挑战是平衡应用程序的需求,数据库引擎的性能特征以及数据检索模式。在设计数据模型时,请始终考虑数据的应用程序使用情况(即查询,更新和数据处理)以及数据本身的固有结构。

很多人误以为MongoDB这类数据库就不应该存在相互之间的关联,尽量将相关数据存在同一张表里,使用嵌套的形式。

其实下面这种遵循范式的数据表设计在MongoDB中也是常见的

一般来说,在下述情况下可以使用规范化模型:

  • 当内嵌数据会导致很多数据的重复,并且读性能的优势又不足于盖过数据重复的弊端时候。
  • 需要表达比较复杂的多对多关系的时候。
  • 大型多层次结构数据集。

引用比内嵌要更加灵活一些。 但客户端应用必须使用二次查询来解析文档内包含的引用。换句话说,对同样的操作来说,规范化模式会导致更多的网络请求发送到数据库服务器端。

索引结构的误区

从MongoDB 3.2开始,WiredTiger存储引擎是默认的存储引擎。而WiredTiger采用的数据结构就是B+Tree。参考链接:MongoDB存储引擎说明,以及 WiredTiger官方介绍

  • WiredTiger maintains a table’s data in memory using a data structure called a B-Tree ( B+ Tree to be specific), referring to the nodes of a B-Tree as pages. Internal pages carry only keys. The leaf pages store both keys and values.

依稀记得去年还有人问过我Mysql为什么要用B+树,谈到B+树节点间相互连接有利于快速的做范围查询。当时也是看到过类似的文章说,MongoDB由于不需要做范围查询,所以使用的占用空间比较少的B树。现在想起来,还是不能人云亦云。

ObjectId的生成

MongoDB作为分布式数据库,它默认的主键的 _id 的生成方式实际上是值得好好聊聊的。

在一些早前的文章里,很容易看到下面类似的描述:

这些是 _id 的一些主要特征的摘要:

  • _id 是集合中文档的主键,用于区分文档(记录)。
  • _id自动编入索引。指定 { _id: } 的查找将 _id 索引作为其指南。
  • 默认情况下,_id 字段的类型为 ObjectID,是 MongoDB 的 BSON 类型之一。如果需要,用户还可以将 _id 覆盖为 ObjectID 以外的其他内容。

ObjectID 长度为 12 字节,由几个 2-4 字节的链组成。每个链代表并指定文档身份的具体内容。以下的值构成了完整的 12 字节组合:

  • 一个 4 字节的值,表示自 Unix 纪元以来的秒数
  • 一个 3 字节的机器标识符
  • 一个 2 字节的进程 ID
  • 一个 3 字节的计数器,以随机值开始

img

通常,你不必担心要如何生成 ObjectID。如果文档尚未分配 _id 值,MongoDB 将自动生成一个 _id 值。

实际上在3.4版本之后,官方文档中对ObjectId描述是:

ObjectIds are small, likely unique, fast to generate, and ordered. ObjectId values are 12 bytes in length, consisting of:

  • a 4-byte timestamp value, representing the ObjectId’s creation, measured in seconds since the Unix epoch
  • a 5-byte random value
  • a 3-byte incrementing counter, initialized to a random value

所以中间ObjectId的机器标识符与进程 ID的五位被一个随机值替代了。

实际上在云原生容器化的场景下,机器标识符与进程id很有可能存在大规模的相同。因此一个随机数反而更加安全,可以保证id的唯一性。

毕竟,在同一时刻,两个独立的进程通过同样的次数,生成一样的随机数。这种情况几乎不可能发生。

分片键的选择

在Mongodb里面存在另一种集群,就是分片技术,可以满足MongoDB数据量大量增长的需求。

优点:

  • 对应用层无感,开发人员不在需要关注该怎么分表、分表以后跨表查询、事务要如何处理
  • 配置简单(没有特殊情况按照_id进行哈希分片即可,特殊的如时序数据可以按照时间字段进行范围分片)

当MongoDB存储海量的数据时,一台机器可能不足以存储数据,也可能不足以提供可接受的读写吞吐量。这时,我们就可以通过在多台机器上分割数据,使得数据库系统能存储和处理更多的数据。

https://imgtu.com/i/fjSm5Q

分片方式

MongoDB提供了基于哈希(hashed)和基于范围(Range)2种分片方式:

哈希分片

哈希分片使用hash索引来在分片集群中对数据进行划分。哈希索引计算某一个字段的哈希值作为索引值,这个值被用作片键。哈希分片以减少定向操作和增加广播操作为代价。分片集群内的数据更加均衡。从MongoDB4.0开始,mongo shell提供了convertShardKeyToHashed()方法,用于查看键的hash值。选择作为hash分片键的字段应该有良好的基数或者该字段包含大量不同的值,hash分片非常适合选取具有像objectId或时间戳那样单调更改的字段作为片键。使用sh.shardCollection()方法,来对集合进行hash分片。

1
sh.shardCollection("database.collection",{<field> : "hashed" } )

范围分片

基于范围的分片会将数据划分为由片键值确定的连续范围。在范围分片模型中,具有“接近”片键的文档可能位于相同的chunk或者shard中,连续范围读取文档将变得高效,但是如果片键选择不佳,则读取和写入的想你将会降低。如果未选择其它选项(如hash分片或者zone),则基于范围的分片是默认的分片方式。范围分片片键的选择:基数大频率低非单调变化使用sh.shardCollection()方法,来对集合进行范围分片,可以选择单字段或者多字段sh.shardCollection(“database.collection”,{}) *

片键选择因素

分片键决定了集合内的文档如何在集群的多个分片上分布数据,分片键要么是一个索引字段,要么是一个存在于集合所有文档中的符合索引字段。MongoDB尝试在集群中的各个分片之间平均分配数据块(chunk),特别注意,shard之间平均分配的数据块(chunk),而不是数据量,分片键的选择直接关系到分片结果的好坏。NOTE:在MongoDB4.2之前,文档的分片字段是不可以修改的。从4.2版本开始,除非分片键是不可变的_id字段,否则你可以更新文档的分片字段。所有需要分片的集合都必须具有支持分片的索引,即分片键上必须有索引,可以使分片键的索引,也可以是符合索引,对于符合索引,分片键必须是索引的前缀。如果集合为空,则sh.shardCollection()在分片键上自动创建索引,无需认为干预如果集合存在数据,则必须先创建索引,然后再使用sh.shardCollection()来为集合分片。分片键的选择需要综合考虑分片键的基数、频率和变化率。

  • 基数。分片键的基数决定了分片集群可以创建的最大chunk的数目。在任何给定的时间,唯一的分片值只能存在一个chunk上。例如:使用性别进行分片,则只能分为“男”和“女”2个chunk,不能随着数据增多而分裂为更多的chunk,因为一个分片值只能存储在同一个chunk中。

  • 频率。频率代表给定值在该列中出现的比率,与关系型数据库中select distinct …异曲同工。如果大多数文档包含了这些值的子集,那么存储这些文档的chunk将成为集群中的瓶颈,随着数据的增长,他们将会成为不可分割的数据块,降低了集群水平扩展的有效性。例如:集合people用来统计各个名族的人信息,使用名族作为分片字段,那么根据我国56个名族的人数分布,占据人口总数92%的汉族将占据一个chunk,这样会导致该chunk非常巨大,失去了分片的意义。

  • 变化率。单调递增或单调递减的分片键可能将数据写到集群中的单个分片上。如果分片键值始终在增加,则所有新插入都将路由到以maxKey为上限的块。 如果分片键值始终在减小,则所有新插入都将路由到以minKey为下限的块。 包含该块的分片将成为写操作的瓶颈。

危险的模糊查询

MongoDB的模糊查询可以使用 $regex 运算符通过正则表达式来进行匹配查询。
$regex :为查询中的模式匹配字符串提供正则表达式功能 。
语法:

1
2
3
{ < field >: { $ regex : / pattern / , $ options : ‘’ } }
{ < field >: { $ regex : ‘pattern’ , $ options : ‘’ } }
{ < field > : { $ regex : / pattern / < options > } }

options 中设置i模式可以进行忽略大小写的匹配。但是这样大概率不会使用索引,将会进行全表扫描。

本人有幸使用这个语句,查询某个亿级单表时,将整个MongoDB集群CPU拉爆。。。事后explain( )语句分析了下相关的查询语句,发现早前设置的索引确实没有命中。