todo: 2021-12-17 最近在使用mongo时发现了bson解析在数据量大时消耗CPU很高的问题,(暂时)感觉这个没什么特别好的解决办法。这可能也是mongo的一个需要改进的地方吧。后面找时间仔细研究下这部分内容。
本文的内容会包括技术选型、索引、事务、日志、数据一致性等,因为内容较多,可能很难一次写完。我会尽量督促自己多思考、多总结、多写、多分享。然后内容可能不会太详细,后面会慢慢完善,包括补充一些图片。
说起mysql和mongo,大家最关注的肯定是技术选型的问题。当要开发一个应用的时候应该选mysql还是mongo。或者说小孩子才做选择,成年人全都要,那么哪些数据下适合存mysql,哪些适合存mongo。针对这个问题,我总结了下面几点。
以上是关于技术选型的几点。其中没有提到性能对比,关于性能对比,其实推荐根据具体的业务需求和具体的版本进行压测来得到更有说服力的数据。
接下来会介绍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的写入过程:
mongo的写入过程:
从上面的详细描述过程可以看出,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的事务冲突处理。在这之前先明确几个点:
明确了上面的两点,我们可以很清晰地推理出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中我们应该避免使用多文档事务,而是通过嵌套的文档模型使用单文档的事务。那么有其他的方法么?
上面的内容主要写了自带的实现写读一致性的功能。但是在实际业务中我们还可以有一些其他的方法来实现。