0%

MongoDB 与 MySQL 的一些读写优化

本篇文章简述了 MongoDB 与 MySQL 设计中的一些读写优化,更加偏向随笔记录。

由于 MongoDB 与 MySQL 的设计年代不同,因此设计思路也有所不同。从整体上看,MongoDB 的优化更加偏向于写入性能,而 MySQL 的优化更加偏向于读取性能。

读取链路的优化

​ 由于 MongoDB 大量数据和索引是常驻内存的,所以读取性能是会比 MySQL 高很多的,通常来说 MongoDB 的写入性能更容易达到瓶颈。MongoDB 的设计中牺牲了一定的读取性能来换取更高的写入性能。而 MySQL 大量数据是常驻在硬盘中的,读性能的可优化之处更多也更加具有性价比,因此做了较多的优化来换取更高的读取性能。

MySQL 读取链路优化

​ MySQL 在读取链路的优化主要有查询缓存、页缓存、自适应哈希索引、CheckPoint、覆盖索引、MRR 优化、ICP 优化

MRR 优化

​ 需要查询时,先根据辅助索引将 WHERE 语句中所有符合条件的主键值放入缓冲区,在缓冲区进行排序后查询。这样可以将随机 IO 转化为顺序 IO。

ICP 优化

​ 当索引取出时,会根据 WHERE 语句条件进行判断,这一优化是适用于联合索引,或者 WHERE 语句中包含主键值的情况。

写入链路的优化

MongoDB 写入链路优化

​ MongoDB 的写入链路主要做了以下几点优化:Check Point、Copy On Write、无锁 B+ 树、Insert List、事务组提交。

Check Point

​ Check Point 技术允许 MongoDB 按照每一秒的频率对整个 B+ 树进行刷盘操作,这使得大量的写入操作在写入日志后不需要立刻进行刷盘,只需要写入到内存当中。结合Copy On Write可以使得 B+ 树在正常刷脏的过程中也不会阻塞写入。

Copy On Write

​ Copy On Write 技术是在上一 Check Point 的基础上在内存中新开辟内存区域进行写入,这样做的好处主要有两点:第一是很好地区分了脏页,在刷脏的过程中减小了遍历的范围,提高了刷脏的效率,第二是能够减少刷脏过程中的阻塞时间,如果一页在刷脏过程中被写入,那么将会立刻开辟出一个新的缓冲区,写入操作将会写入到该缓冲区的跳跃表中,并不会造成写入的阻塞(但并不意味着不会增加写入的时间,事实上,在刷脏时写入将会等待缓冲区的分配,这会稍微延长写入时间)。

Insert List

​ MongoDB 中所有的写入操作并不是在原有的位置上更新。每一个页表中都有多个跳跃表,插入操作会将值插入到统一的缓冲区内,并且在跳跃表中增加指向该值的指针,并且跳跃表是可以做到无锁并发的,可以大大提高写入的性能。但是这同时会造成读取链路变长,在读取时需要首先查找原有的值数组,然后再对跳跃表进行遍历。

无锁 B+ 树

​ 这里主要是针对 B+ 树节点的分裂优化,由于 B+ 树节点的分裂会造成写入的阻塞,在 MongoDB 中只有单个节点短时间内插入过多数据时,才会允许 B+树节点的分裂,其余情况下,只有在 Check Point 或者冷淘汰过程中才会进行节点的分裂。所以 MongoDB 中内存中页面的大小可能与硬盘中的页面大小不相同,在刷入硬盘中时,会将内存中的页面分为几个较小的页面写入到硬盘中。

​ 单个节点插入过多数据时,会将最后一个插入跳跃表放入到新的节点中,这一过程是可以做到不阻塞的(先将剩下的插入操作引导至新的页面中,随后再将旧节点拷贝到新的页面上)。但是这种设计会对热点数据的读取性能造成一定的影响,最新插入的数据需要更多的查询次数,降低了读取性能。

事务组提交

​ 事务组提交是数据库系统中比较常见的优化,但 MongoDB 与MySQL的组提交有所不同。MongoDB 中组提交的缓冲区很小,并且每次组提交后都会进行刷盘,而 MySQL 中时以文件的形式来进行刷盘的。MongoDB 中,一次组提交中线程会将 log 拷贝到缓冲区中,拷贝完成按照 log 的序列号进行提交,不会造成不同组的乱序提交。

MySQL 的写入链路优化

MySQL 写入的优化主要集中在如何将随机 IO 转化为顺序 IO 上,这是因为在设计时 MySQL 的 B+树索引主要是存储在硬盘上的。

插入缓冲区

辅助索引的写入是统一写入到缓冲区后,再进行刷盘操作。这一优化是因为辅助索引通常并不是聚集的,直接写入会造成随机 IO,通常引入这样一个缓冲区,可以结合异步 IO 机制将随机 IO 转换为顺序 IO,提高刷盘时的性能。

Check Point 技术

MySQL 中同样是采取了该技术,当数据需要写入时会将对应的页读取到内存中,然后在内存中进行修改,随后该页并不会被立刻刷入到硬盘中,而是会由 master 线程进行刷盘。MySQL 中的该设计其实主要是考虑到热数据的问题,刚刚写入的数据被再次修改和读取的概率是比较大的,这一举措不仅优化了写入性能,还能够优化读取性能。

事务组提交

bin log 的组提交机制类似于 MongoDB。redo log 则是采用多个文件缓冲和双指针的机制,一个指针指向当前写入的文件缓冲区,另外一个指针指向需要刷盘的缓冲区,实现无锁写入。MySQL 的bin log组提交被 wiredtiger 中的 journal 组提交所借鉴,这确实是一个比较巧妙的设计,但是 MongoDB 中将应用层的日志也当做数据库中的记录,这一设计了避免了bin log 需要和 redo log 分开进行提交的缺陷。

异步 IO

异步 IO 是 MySQL 其他许多优化的基础,异步 IO 允许操作系统将多个 IO 操作合并并且重新排列顺序,使之更加符合硬盘的写入顺序。

刷新邻接页

这一操作在固态硬盘时代好像是一个负优化,一般会关闭。

对比

​ MongoDB 的写入链路是比读取链路要短的,这一点与 MySQL 正好相反。对于单独的事务来看,由于应用层日志是不需要单独提交的,所以 MongoDB 的 WAL 写入理论上是要比 MySQL 快的,(事务在写入 WAL 后就可以返回)。

​ 在大量事务写入的情况下,很容易会造成热点页刷脏的问题。由于 MongoDB 允许内存与硬盘中 B+树大小不相同,并且针对内存中的 B+ 树做了写入优化,大量写入的情况下会优先写入到内存缓冲区中,并且优先在内存中进行分裂。在处理这种瞬时写入压力时,MongoDB 的设计会更加好,这主要是 MongoDB 的设计考虑到了大量的内存缓冲,并且牺牲了一部分读取性能换来的。

​ 最主要的是,MySQL 作为关系型数据库,其事务隔离等级要求较高,这同样会造成较大的性能损耗,例如外键约束、间隙锁等。

​ 但是,MongoDB 在指定主键插入时,由于插入的位置可能并不是页面内最后一个跳跃表,这样就可能会导致创建出多个新页面才能够将插入位置放入到新页面中,或者来不及进行内存节点分裂,导致页面被锁住禁止写入,这样会造成较大的性能毛刺。因为要插入一个非常大的跳跃表,并且还要完成多个页面的持久化。此外,指定主键时,如果数据库体量较大,导致大部分数据不能够常驻内存,MongoDB 可能还需要从硬盘中读取数据来确保主键是唯一的,这样就导致数据量较大时指定主键的插入性能非常低。

​ 所以,MongoDB 并不适合存储那些由用户指定,并且需要保证唯一性的数据,例如用户名称等,而订单编号这种情况可以直接使用 uuid 进行插入。