【存储】MySQL 和 MongoDB

todo: 2021-12-17 最近在使用mongo时发现了bson解析在数据量大时消耗CPU很高的问题,(暂时)感觉这个没什么特别好的解决办法。这可能也是mongo的一个需要改进的地方吧。后面找时间仔细研究下这部分内容。

文章目录

  • 技术选型:MySQL or MongoDB
  • 索引
  • 日志
  • 事务
  • 查询优化
  • 数据一致性

这篇文章主要想聊聊mysql和mongoDB。这两个数据库的定位都是持久化的主存储。mysql的地位不用多说,mongoDB的应用也越来越广泛。在学习mongoDB的过程中,发现其在设计上和mysql有很多相似的地方,所以想着写一篇关于mysql和mongoDB文章。

本文的内容会包括技术选型、索引、事务、日志、数据一致性等,因为内容较多,可能很难一次写完。我会尽量督促自己多思考、多总结、多写、多分享。然后内容可能不会太详细,后面会慢慢完善,包括补充一些图片。

技术选型:MySQL or MongoDB

说起mysql和mongo,大家最关注的肯定是技术选型的问题。当要开发一个应用的时候应该选mysql还是mongo。或者说小孩子才做选择,成年人全都要,那么哪些数据下适合存mysql,哪些适合存mongo。针对这个问题,我总结了下面几点。

  • 文档型 vs 关系型
    众所周知mysql是关系型数据库,mongo是文档型数据库。所以首先要考虑的是业务的数据模型更适合关系型还是文档型。这里我的建议是,如果是复杂的数据模型,选择关系型数据库。这里的复杂的数据模型是指存在多个对象且对象之间存在多种对应关系。关系型数据库天生就是为复杂数据模型而生的。如果是简单的数据模型,可以考虑文档型数据库,其嵌套的文档模型和无schema也很好用。
  • 元数据 vs 增量数据
    mongo作为nosql,其扩展性明显好于mysql。所以在大数据量或者增量数据的存储下,可以考虑使用mongo来进行存储。可以简单地把数据分为元数据和增量数据。举个例子,比如创建一份问卷,那么问卷相关的数据比如说 创建用户id、问卷名称、题目等信息都是相对固定的,这些数据可以划分为元数据;而问卷的作答数据会随着用户作答不断增长,可以划分为增量数据。一般情况下,元数据倾向于存储在mysql,增量数据倾向于存储在mongo。
  • 精通程度 and 学习成本
    在进行技术选型时,开发人员对某项技术的掌握程度以及学习成本是重要影响因素。在这方面,mysql是占有压倒性优势的。对绝大多数的后端开发来说,mysql都是其最精通的技能之一。而mongo的普及程度就远远不如。另外,以我个人的学习经历而言,mongo的资源同样远远不如mysql。

以上是关于技术选型的几点。其中没有提到性能对比,关于性能对比,其实推荐根据具体的业务需求和具体的版本进行压测来得到更有说服力的数据。

接下来会介绍mysql和mongo的更具体的内容。mysql和mongo的架构设计中都采用了可插拔的存储引擎,而不同的存储引擎之间差异巨大。所以在此需要声明:下面的内容中,mysql是基于innoDB的,mongo是基于wiredtiger。

索引

聚簇索引 vs 非聚簇索引
innoDB的索引是B+树。wiredtiger支持B+树和LSM树,默认是B+树。在多数情况情况下,mongo都会选择使用B+树。两者之间的比较大的区别是innoDB是采用聚簇索引的形式组织数据,wiredtiger的B+树是非聚簇的。使用聚簇索引能够更好的利用局部性原理,但是插入性能会比非聚簇索引差一些,因为插入的时候可能会涉及到节点的拆分等。

因为有同学纠结b树和b+树的问题,这里贴一下wiredTiger官方文档的说明,可以看到是明显的b+树结构。
在这里插入图片描述

多键索引
虽然底层的数据结构都是B+树,但是由于mongo的嵌套的文档数据模型,其存在一种特殊的多键索引。关于多键索引,我在mongoDB的多键索引和查询优化中进行了详细的说明,这里不做展开。

日志

对mysql有一定程度了解的朋友都知道,日志在mysql的设计中占有非常重要的地位。后面要讲到的事务、读写分离导致的数据一致性问题等都要涉及到不同的日志。所以我们把日志的部分放到前面来说明。

mysql的日志系统主要包括redo log、uodo log、binlog。其中redo log和undo log是innoDB提供的功能,也就是在存储引擎层实现的,而binlog是mysql的server层实现的。binlog的作用主要是主从复制、数据归档;redo log是预写日志,将磁盘的随机写改变为顺序写,实现了事务的持久化;undo log的作用主要是支持事务的回滚以及MVCC。

mongo的话,大家可能稍微陌生一点,那我们先说下mongo支持的功能。mongo是支持主从架构的,并且其副本之间的数据同步也是通过日志实现。从4.0版本开始,mongo开始支持多文档的事务并提供快照级别的事务隔离。可以看到mongo的这些功能和mysql基本是一样的,所以两者的日志设计也非常相似。mongo通过oplog来实现主从复制,wal来实现事务的持久化,update list来实现MVCC。下面我们来做一些详细的对比。

redo log vs WAL
先做名词解释,wal指的是预写日志。mysql中的预写日志起名叫redo log,mongo直接用wal表示。所以下文中wal无特殊说明都是指mong的预写日志。

mysql和mongo的数据都是存储在磁盘上,但是磁盘的io性能上很差的,所以两者都将数据缓存在内存中来优化读写性能。 发生写操作时,先在缓存中修改数据,同时在缓存中构建预写日志记录所做的操作。当事务提交时,将预写日志刷到磁盘中。(事实上预写日志并不是一定要在提交时持久化到磁盘中,mysql和mongo都提供了不同的刷盘策略。只是建议配置为在事务提交时将预写日志刷盘,这样能保证不丢数据。) mysql或者mongo的后台线程会定期(定期或者预写日志大小达到阈值)做checkpoint将缓存中的数据持久化到磁盘上。这样就将对B+树的随机写变为对预写日志的追加写,大大提升了写入性能。

基于上面的描述,如果不关注具体的实现,只讨论设计思路的话,mysql的redo log和mongo的wal是没什么差别的。实际上,mysql的redo log和mongo的wal有一个很大的区别,就是mysql的redo log采用了两阶段提交的方式。导致这个的原因其实主要是mysql的binlog和mongo的oplog的差别。所以会在下一小节详细介绍。

binlog vs oplog
前面提到了mysql的binlog和mongo的oplog存在相当的差异,同样这里不指具体的格式,而是设计思路。先说差异,mysql的binlog是server层实现逻辑日志,其独立于存储引擎;mongo的oplog虽然名为log,但其实现为写入一个特定的集合(表)中。
下面来具体描述一下mysql和mongo写入的过程,以便更好地理解其中的差异。
mysql的写入过程:

  • 如果对应的数据在内存中,则修改内存中的数据(dirty page),否则写入chang buffer;
  • 在将数据写入内存时构建redo log和binlog,此时两种存储在内存中;
  • 事务提交时,先将redo log刷盘,刷盘后redo log处于prepare状态;
  • 然后将binlog刷盘,binlog刷盘后再将redo log置为commit状态;

mongo的写入过程:

  • 修改内存中的数据;
  • 构造oplog,将其作为写入数据加入事务;
  • 构造wal;
  • 事务提交时将wal刷盘;

从上面的详细描述过程可以看出,redo log的提交采用了两阶段提交的方式。这是因为binlog和redo log是涉及到两次刷盘行为,这使其行为表现得像写入两个系统。在跨系统的数据一致性问题中两阶段提交是常用的方法。而相对来说,mongo的wal只涉及到wal的刷盘行为,则不需要两阶段提交。

关于优劣,我们肯定更喜欢mongo的这种实现,简单有效。但是了解问题时不能只看当下,还要看其历史渊源。innodb是mysql发展到一定阶段才出现的,也就是binlog和redo log之间隔了相当的时间。基于此,innodb的redo log两阶段提交也是可以理解的。

事务

mysql的事务相信大部分都非常熟悉,是innodb存储引擎支持的。mysql的事务通过redo log实现事务的持久化,通过undo log实现事务的回滚,提供了读未提交、读已提交、可重复读、串行化四种隔离级别。innodb默认的隔离级别是可重复读,也是最普遍最常用的隔离级别。可重复读是由MVCC实现的。

mongo从4.0版本开始提供了多文档的事务,从4.2版本开始提供了跨shard的分布式事务。因为我本人用的是4.0版,所以对跨shard事务了解不多,只是大概知道是采用两阶段提交的方式实现的,本文也不多提。对于单shard的事务,其实和innodb的事务非常类似的,不过隔离级别只提供了快照级别的事务隔离,也就是innodb的可重复读。

本文关注的一个点是,多事务并发时出现写冲突的情况下,mysql和mongo的分别是如何处理的。先说结论:mysql通过写锁将(有写冲突的)事务变为串行化;mongo在判断事务冲突时采用了乐观锁的方式,先修改的事务win,后修改的事务abort。

先详细说下mysql的事务冲突处理。在这之前先明确几个点:

  1. 在执行更新语句时会加行锁,但是该锁只有在事务结束时才会释放;
  2. 执行器在执行更新语句时会先执行相应的查询语句,再执行更新语句,该查询语句是当前读而不是快照读,执行当前读需要获取行锁;

明确了上面的两点,我们可以很清晰地推理出mysql如何处理事务冲突:先写的事务获取到锁,后写的事务阻塞在锁上,直到先写的事务结束释放锁。

mongo采用了类似乐观锁的冲突检测,这类的冲突检测策略有两种:first update win和first commit win。mongo选择了first update win的策略,尽早地检测冲突。如果a事务修改某条数据时发现该数据已经被某个a事务开启时尚未提交的事务修改,则a事务就要abort。

另外,mysql中事务提交时只是主节点(主从架构)进行了持久化;mongo中事务提交时为了保证数据不丢,会将事务中大多数节点上复制后再向客户端返回响应。可以看出,mongo的事务的持久化程度更高,考虑了分布式环境中单点失效的问题,但是相应的事务的响应时间就会变长。

其实不仅是事务这个点,从整体上,mysql对分布式的支持都不是很好,而mongo的很多设计天然是站在分布式角度考虑的。

查询优化

查询优化其实是一个比较大的话题,涉及到索引以及优化器如何选择索引。这里也暂时不展开讲了,附之前写的两篇,分别讲了mongo和mysql的优化器工作原理。
查询优化——mysql如何选择最优的执行计划
mongoDB的多健索引及查询优化

数据一致性

数据一致性是个很大的话题。在这里提到的数据一致性为主从架构下异步复制导致的写读一致性问题。说明下本文讨论所基于的技术背景:mysql为经典的主从架构,并进行了读写分离;mongo为单shard的副本集,不考虑多shard的情况。然后再说明下遇到的具体问题:之前业务遇到一个写完马上要去读的场景,然后在读写分离的架构下出现读不到所写内容的情况。因为是在一个接口中写完马上读,代码执行速度远胜于主从复制的速度,所以在类似的场景下问题几乎是必现的。接下来大概分析下遇到该问题有哪些措施。

mysql
在mysql的读写分离的情况下,我们公司现在的做法是DBA对每个集群提供了读连接和写连接,由业务自己选择。其中写连接默认操作主库,可读可写;读连接默认操作从库,可读。所以这种情况下我们选择写连接即可。
另外再想一下,在本地操作时通常会开启事务来保证操作的原子性,那么对有读有写的事务只能选择写连接。
所以对mysql而言,开启事务就可以避免该问题。而在本地操作中我们也确实应该开启事务。

mongo
同样的道理,在mongo中也可以通过开启多文档事务的方式来避免该问题。但是前面说了mongo的多文档事务损耗相对较大。而且在mongo中我们应该避免使用多文档事务,而是通过嵌套的文档模型使用单文档的事务。那么有其他的方法么?

  • mongo提供了read concern和write concern的客户端参数来控制副本集的一致性行为。选择write concren: majority参数时mongo会在将写入复制到大多数节点后再返回,选择read concern: majority时mongo只会读取已经写入到大多数副本上的数据。
    这似乎很符合分布式系统中通过quorum来保证写读一致性的思路。但实际上深入了解mongo的实现后发现这能保证我们读到的内容不会回滚,而不能保证一定能读到最新的写。
    (mongo是通过commited字段来实现上述功能的,后面再来补相关的细节吧。展开讲又是很长的内容。)
  • mongo通过session来传递上下文,实际上事务也是通过session来实现的。通过传递session,mongo可以保证我们读的数据不会旧于写入的数据。实际实现是写入后mongo会分配逻辑时间戳并放置在session中,读取时如果从库的逻辑时间戳早于session携带的逻辑时间戳,那么就不会读取该数据,会不断轮询直到读到足够新的数据。

上面的内容主要写了自带的实现写读一致性的功能。但是在实际业务中我们还可以有一些其他的方法来实现。

  • 在本地记录所做的修改,查询得到结果和本地记录的修改做比对,保证拿到的是最新的数据。但这种问题的方法在于要考虑对数据并发操作的可能性。如果存在对目标数据的并发写入,在需要慎重考虑。
  • 在数据查询层做修改,采用的方式其实类似mongo提供的方式的第二点,修改后记录修改的时间戳。只有当从库对应表的时间戳不早于记录的时间戳时才读取。

你可能感兴趣的:(数据库,web开发,mongodb,mysql,数据库)