当前位置: 首页 > 科技观察

【通俗易懂】MongoDB和WiredTiger

时间:2023-03-15 13:35:22 科技观察

MongoDB是目前主流的NoSQL数据库之一。与关系型数据库和其他NoSQL不同,MongoDB采用面向文档的数据存储方式,以类JSON的方式存储数据。磁盘上,由于项目上的一些历史问题,笔者在最近的工作中也经常要和MongoDB打交道,所以才有了这篇文章。虽然之前对MongoDB有一些了解,但是在项目中大规模使用还是第一次,在使用过程中也暴露了很多问题。不过,这里主要讨论MongoDB中一些重要概念的原理。介绍中也会和传统的关系型数据库如MySQL做对比,让读者自己判断优缺点。概述MongoDB虽然也是一个数据库,但是它与传统的RDBMS有很大的不同。许多开发人员认为或被灌输了MongoDB这种没有Scheme的数据库相比RDBMS有巨大的性能提升。这种判断其实是一种误解;因为数据库的性能不仅与数据库本身的设计有关,还与开发者对表结构和索引的设计、存储引擎的选择、业务等有着巨大的关系。如果你认为仅仅更换了数据库,想要获得一个数量级的性能提升,还是太年轻了。体系结构现有的流行数据库实际上具有非常相似的体系结构。MongoDB其实类似于MySQL中的架构。底层使用“可插拔”的存储引擎来满足用户的不同需求。用户可以根据表中的数据特性选择不同的存储引擎,可以在同一个MongoDB实例中使用;在最新版本的MongoDB中,使用WiredTiger作为默认的存储引擎,WiredTiger提供了不同粒度的并发控制和压缩机制,可以为不同种类的应用提供出色的性能和存储效率。不同存储引擎的上层是MongoDB的数据模型和查询语言。与关系数据库不同,MongoDB因其在数据存储和RDBMS上的较大差异而创建了一套不同的查询语言;虽然MongoDB查询语言非常强大,支持很多功能,而且还是可编程的。但是里面包含的内容非常复杂,API设计也不是很优雅,所以还是需要一定的学习成本。对于长期使用MySQL的开发者来说,肯定会有些不习惯。db.collection.updateMany(,,{upsert:,writeConcern:,collat??ion:})查询语言复杂,因为MongoDB支持的数据类型很多,每一个数据record,也就是一个文档的结构非常复杂,设计上无法避免,所以使用MongoDB的开发者需要花一些时间去学习各种API。RDBMS和MongoDBMongoDB使用面向文档的数据模型,导致许多概念与RDBMS有所不同。虽然两者大体上都有对应的概念,但是概念之间的细微差别其实会影响我们对MongoDB的理解。理解:传统的RDBMS实际上是使用Table格式,将数据逻辑存储在一个二维表中,不包含任何复杂的数据结构,但是由于MongoDB支持使用嵌入式文档、数组、哈希等多种复杂数据结构,所以它最终将所有数据存储为BSON数据格式。RDBMS和MongoDB中的概念有对应??关系。数据库、表、行和索引的概念在两个数据库中非常相似,除了最重要的JOIN和EmbeddedDocumentorReference有巨大差异。这种差异实际上影响了使用MongoDB时CollectionSchema的设计。如果我们按照RDBMS中同样的思路在MongoDB中设计Collection,那么不可避免地要使用大量的“JOIN”语句,而MongoDB不支持“JOIN”,这种查询在应用中的性能非常,很差。这时候使用嵌入文档其实可以解决这个问题。虽然嵌入文档可能会造成很多数据库的数据冗余,导致我们在更新的时候非常痛苦,但是查询确实是非常快的。{_id:,name:"draveness",books:[{_id:,name:"MongoDB:TheDefinitiveGuide"},{_id:,name:"HighPerformanceMySQL"}]}在MongoDB中时在使用它的时候,我们必须忘掉RDBMS中关于表设计的很多规则,同时想清楚MongoDB的优势,仔细思考如何设计表来利用MongoDB提供的众多特性来提高查询效率。数据模型MongoDB和RDBMS最大的区别在于数据模型的设计有很大的不同。数据模型的不同决定了它具有截然不同的特点。存储在MongoDB中的数据有一个非常灵活的Schema。我们和RDBMS一样,在插入数据之前不需要决定和定义表中的数据结构。MongoDB的组合并没有对Collection的数据结构做任何限制,但是在实际使用中,同一个Collection中的大部分文档都具有相似的结构。在为MongoDB应用程序设计数据模型时,如何表示数据模型之间的关系其实是需要开发者慎重考虑的。MongoDB提供了两种不同的方法来表示文档之间的关系:引用和嵌入。标准化数据模型参考(Reference)在MongoDB中称为标准化数据模型。它与MySQL的外键非常相似。每个文档都可以通过一个xx_id字段“链接”到其他文档:但是这种在MongoDB中的这种引用在MySQL中不是通过JOIN直接搜索的,我们需要使用额外的查询来找到引用对应的模型,虽然这提供了灵活性更高,但由于增加了客户端与MongoDB之间的交互次数(Round-Trip),也会导致查询变慢,甚至会出现非常严重的性能问题。MongoDB中的引用并没有对引用对应的数据模型是否实际存在做任何约束,所以如果在应用层没有对文档之间的关系进行约束,那么就可能存在引用不存在的文档的问题:尽管该引用存在严重的性能问题,并且在数据库级别没有限制是否删除模型,但它提供的一些特性是嵌入式文档无法提供的。当我们需要表达多对多或更多的关系时,当你有一个庞大的数据集时,你可以考虑使用标准化的数据模型——引用。嵌入式数据模型MongoDB除了与MySQL非常相似之外,由于其独特的数据存储方式,还提供了嵌入式数据模型。嵌入式数据模型也被认为是一种非标准的数据模型:因为MongoDB使用BSON数据格式存储数据,而嵌入式数据模型中的子文档实际上是父文档中的另一个值,只不过是存储了一个对象it:{_id:,username:"draveness",age:20,contact:[{_id:,email:"i@draveness.me"}]}嵌入式数据模型允许我们存储信息在同一条数据记录中具有相同的关系,以便应用程序可以更快地查询和更新相关数据;当我们的数据模型有“包含”这样的关系或者模型经常需要和其他模型一起出现(查询),比如文章、评论,那么我们可以考虑使用嵌入式关系数据模型来设计。总而言之,embedding的使用可以让我们在更少的请求中得到更多的相关数据,可以为读操作提供更高的性能,同时也提供了在同一个写请求中更新相关数据的支持。MongoDB底层的WiredTiger存储引擎可以保证对同一个文档的操作是原子的,任何写操作都不能原子性地影响多个文档或多个集合。主键与索引本节主要介绍MongoDB中不同类型的索引,当然也包括每个文档中非常重要的字段_id,可以理解为MongoDB的“主键”。此外,我们还会介绍单字段索引、复合索引、多键索引等多种类型的索引。MongoDB中索引的概念其实和MySQL中的差不多。底层数据结构和基本索引类型几乎相同。两者的区别在于MongoDB支持的数据结构类型不同,因此也理所当然的提供了更多的索引类型。MySQL默认索引中的每一行数据都有一个主键,数据库中的数据是按照主键作为key物理存储在文件中的;主键除了用于数据存储外,由于其特性声明,还可以加快数据库查询。MongoDB中的所有文档也都有一个唯一的_id字段。默认情况下,所有文档都使用一个12字节的ObjectId作为默认索引:前四位代表生成当前_id时的Unix时间戳,之后是三位机器标识符和两位处理器标识符,***为三位计数器,初始值为随机数;这样,不用增加id,就可以解决分布式MongoDB生成唯一标识符的问题,也可以保证id的增长在一定程度上是增量的。单字段索引(SingleField)除了MongoDB默认提供的_id字段,我们还可以创建其他的单键索引,而且不仅支持顺序索引,还支持索引倒置:db.users.createIndex({age:-1})MySQL8.0之前的索引只能正序排列,8.0之后引入了倒序索引。单字段索引可以说是MySQL中辅助(Secondary)索引的一个子集。只需为除_id之外的任何单个字段构建正向或反向索引树。复合索引(Compound)除了单字段索引这种非常简单的索引类型,MongoDB还支持由多个不同字段组成的复合索引(CompoundIndex)。由于MongoDB支持同一字段的正反序,所以比较多的情况会出现在MySQL中的辅助索引中:db.users.createIndex({username:1,age:-1})db.users.createIndex({username:1,age:1})上面的两个索引是完全不同的,磁盘上的B+树实际上是以完全不同的顺序存储的。虽然username字段是升序排列,但是对于age,两个索引的处理是完全相反的:这样一来,在使用查询语句在集合中查找数据时,如果约定好正反序,则不同实际上会使用索引。所以在创建索引的时候一定要考虑使用场景,避免创建无用的索引。.多键索引(Multikey)由于MongoDB支持类数组的数据结构,所以它也提供了一个叫做多键索引的功能,可以对数组中的每一个元素进行索引。索引的创建和单字段索引区别不大区别:db.collection.createIndex({address:1})如果一个字段是数组,当使用上面的代码,可以加快查找数组中元素的速度。文本索引(Text)文本索引是MongoDB提供的另外一个实用的功能,不过这里我们只提这一类索引,并不打算深入讲这个东西的性能。如果是真的要做全文索引的话,建议使用Elasticsearch这种比较专业的东西,而不是使用MongoDB提供的这个功能。如何存储数据是一个比较重要的问题。正如我们前面提到的,MongoDB和MySQL都提供了插件式存储引擎支持。存储引擎作为MongoDB的主要组件之一,全面负责MongoDB的数据管理。WiredTiger是MongoDB3.2之后默认的存储引擎。如果你对每一个存储引擎都不了解,那就不要去改变MongoDB默认的存储引擎;它有很多优点,比如非常高效的缓存机制:WiredTiger还支持在内存和磁盘上对索引进行压缩,还使用前缀压缩来减少RAM的使用。在接下来的文章中,我们将详细介绍和分析WiredTiger存储引擎是如何存储各种数据的。.Journaling为了在数据库宕机时保证MongoDB中数据的持久化,MongoDB使用WriteAheadLogging将日志文件预写到磁盘上;除了journal日志,MongoDB还使用检查点(Checkpoint)来保证数据的一致性。当数据库宕机时,我们需要Checkpoint和journalfiles配合完成数据恢复工作:在数据文件中找到之前checkpoint的标识;在journal文件中找到identifier对应的记录;重做对应记录后的所有操作;MongoDB每隔60s或当日志数据写入达到2GB时会设置一个检查点。当然我们也可以通过在写入时传入j:true参数来强制同步journal文件。本文不介绍Journal文件的格式及相关内容。作者可能会在后面介绍和分析WiredTiger的文章中简单分析一下它的存储格式和其他一些特性。小结本文仅简单介绍了MongoDB的一些基本特性和数据模型。虽然“***”扩展名是MongoDB的一个非常重要的特性,但是限于篇幅,我们没有介绍任何与MongoDB集群相关的内容。这些资料,不过在后面的文章中会专门介绍多实例MongoDB是如何协同工作的。在这里,我想说的是,如果读者在使用MongoDB的过程中收到MongoDB性能比MySQL好很多的断言,但仍然沿用之前RDBMS对数据库的设计方法,那么我相信性能会不一样到底。提高太多,可能不增反减;只有真正理解MongoDB的数据模型,并根据业务需求进行设计,才能利用好嵌入式文档等特性,提升MongoDB的性能。