本文将通过对比 MySQL 与 MongoDB 的一些性能和特点,来简单刨析结构化数据库与文档性数据库,从而给大家以后在生产环境中数据存储介质的选择提供一些参考。
前言
SQL 的全称是 Structured Query Language(结构化查询语言),主要用来操作结构化数据库的增删改查与结构的定义。其中结构化的数据库主要有 MySQL,Oracle,SQL Server 等。其中 MySQL 在国内使用非常广泛,其以开源、免费闻名于世界;并且我们公司生产环境也多以 MySQL 5.7 为主。
而 NoSQL 的意思是说 Not Only SQL,就是说不仅仅是 SQL,我还有别的功能;这可能与大多数人的认知有些偏差。其中的代表有 K/V 存储数据库像 Redis,levelDB 等;文档形数据库像 MongoDB,DynamoDB(Amazon),CouchDB 等;列族数据库像 HBase、Cassandra 等。最符合其 Not Only SQL 身份的是文档形数据库,其主要用来存储非结构化的数据,同时也兼顾了存储结构化数据的功能,相当于对现有结构化数据库的一种扩展。其中代表的数据库有 MongoDB,DynamoDB(Amazon),CouchDB 等。在生产环境当中,免费的以开源的 MongoDB 占大多数,而付费的 DynamoDB 也占据一片江山。本文讨论的 NoSQL 也将以文档形数据库中的 MongoDB 为主。
本人在集团生产环境中使用了阿里云的 MongoDB 已有近两年的时间,其中也积累了一些经验;同时也经历了 MongoDB 从 3.X 到 4.X 版本升级带来的新特性。本文将通过对比 MySQL 与 MongoDB 的一些性能和特点,来简单刨析结构化数据库与文档性数据库,从而给大家以后在生产环境中数据存储介质的选择提供一些参考。
一些术语类比
为了便于对 MongoDB 有个大致的概念,下表中将 MongoDB 与 MySQL 功能相同、相近的概念进行了列举:
MongoDB | MySQL |
数据库 Database | 数据库 Database |
集合 Collection | 表 Table |
文档 Document | 记录行 Row |
字段 Field | 列 Column |
数据存储形式
以存储学生信息为例,假设要存储 学号,姓名,性别,生日,入学时间,年级 等信息为例。(同时也加上我司特色的 gmtCreate,gmtModified 字段,以及自增主键 id)
在 MySQL 当中会以如下的形式存储,当然需要提前把表建好(在这里省略掉),数据的存储形式类似于一个 Excel 表格:
id | gmt_create | gmt_modified | student_number | last_name | fitst_name | full_name | gender | dateofbirth | register_time | grade |
666 | 2018-02-04 11:22:33 | 2021-02-05 01:02:03 | 123505666 | 白 | 敬亭 | 白敬亭 | M | 1993-10-15 | 2018-09-01 08:08:08 | 3 |
888 | 2021-02-05 11:22:33 | 2022-02-06 01:02:03 | 123505888 | 赵 | 今麦 | 赵今麦 | F | 2002-9-29 | 2021-09-01 08:08:08 | 1 |
而在 MongoDB 当中,不需要提前创建表。其数据会以如下的形式存储,类似于一个 JSON:
[ { "_id" : "5a760bf9e1d51a18bebcf5e1", "gmtCreate" : "2018-02-04 03:22:33.666", "gmtModified" : "2021-02-04 17:02:03.888", "studentNumber" : "123505666", "lastName" : "白", "firstName" : "敬亭", "fullName" : "白敬亭", "gender" : "M", "dateOfBirth" : "1993-10-15", "registerTime" : "2018-09-01 00:08:08.686", "grade" : 1, "_class" : "com.alibaba.xxx.xxx.StudentDO" }, { "_id" : "601cd6196b919e6b328d8888", "gmtCreate" : "2021-02-05 13:22:33.778", "gmtModified" : "2022-02-05 17:02:03.345", "studentNumber" : "123505888", "lastName" : "赵", "firstName" : "今麦", "fullName" : "赵今麦", "gender" : "F", "dateOfBirth" : "2002-9-29", "registerTime" : "2021-09-01 08:08:08.225", "grade" : 1, "_class" : "com.alibaba.xxx.xxx.StudentDO" } ]
实际上,它是以一个叫 BSON,一种二进制的 JSON 形式存储的。
基本操作语法
以基本的增删改查为例,两种查询语法确实有很大的区别。
增加记录
INSERT INTO student (gmt_create, gmt_modified, student_number, last_name, fitst_name, full_name, gender, date_of_birth, register_time, grade) VALUES (now(), now(), "123505666", "白", "敬亭", "白敬亭", "M", "1993-10-15", "2018-09-01 08:08:08", 3);
db.student.insert( { "gmtCreate" : "2018-02-04 03:22:33.666", "gmtModified" : "2021-02-04 17:02:03.888", "studentNumber" : "123505666", "lastName" : "白", "firstName" : "敬亭", "fullName" : "白敬亭", "gender" : "M", "dateOfBirth" : "1993-10-15", "registerTime" : "2018-09-01 00:08:08.686", "grade" : 1 } );
删除记录
DELETE FROM student WHERE id = 888;
db.stduent.deleteMany({ "_id" : "601cd6196b919e6b328d8888" });
修改记录
UPDATE student SET grade = 4 WHERE id = 888;
db.student.update( { "_id" : "5a760bf9e1d51a18bebcf5e1" }, { $set : { "grade" : 4 } } )
查询记录
SELECT * FROM student WHERE full_name = "赵今麦";
db.student.find({ "fullName" : "赵今麦" })
可以看出,MongoDB 的语法其实也并不复杂,整个操作就像是在写一个描述对象的 JSON,外加一些操作的关键字。
使用感受对比
- MongoDB 非常适合敏捷开发。首先它不需要创建表,再加上我自己封装了一套 DAO 框架,大概创建一套 DO 对象的增删改查只要继承对象、实现接口,整个增删改查代码只要在 300 行(包含 DO 本身)之内就可以搞定,最多 30min 内搞定。而 MySQL 需要到集团 DMS 上去先创建一个结构设计工单,然后把数据库结构同步到线上。再用 IDEA 中的插件把 SQL,Mapper 等生成,或者自己写也行。一套 DO 对象的增删改查代码轻轻松松上千行,保守估计耗时要 1h。有的时候碰上双 11、618 等 “良辰吉日” ,审批等个 1 - 2 天也是有可能的。
- 第二点 MongoDB 在开发时非常方便。虽然很多时候在开发之前都会把领域模型,数据库表设计好,但是不免有考虑不周的地方:例如在开发过程中要加个字段、删个字段、改个字段名、或者改个字段类型之类的。改字段名、删字段等在集团 MySQL 中做了限制,基本上就别想了。而增加字段,改字段类型还要走一遍前面 DDL 流程,然后再去改 Mapper 等文件,非常麻烦。而如果用 MongoDB 的话,直接改一下 DO 对象,查询对象就好,基本上是分分种就能搞定。同时由于 MongoDB 的 JSON 格式,多一个字段,少一个字段,也不会在查询中报错,顶多是对于老数据来说,新加的字段没有值而已。
- 在高端玩法上面,MongoDO 对多态支持的非常好。有的时候一个基类有多个衍生类需要存储,它这种文档形式的存储天然可以根据不同的衍生类扩展不同的字段。同时它的文档里面存有一个 "_class" 字段来标识查询出来的数据要反序列化成哪一个对象的,并且 MongoDB 的 Spring 底层框架已经封装得很好。所以查出来的衍生类都可以无感知的用基类来接收而不用多写一行代码,同时再配合使用策略模式使用,简直完美。反观 MySQL 其实也可以实现,要在表中增加一个字段来记录衍生类的类型,同时自己要写一些 TypeHandler 来处理转换成对应的衍生对象,我以前有使用过,算是比较麻烦的。
- 另一个高端玩法,MongoDB 支持使用对象里面包含的对象中的字段来建索引,同时也支持将对象当中的数组元素来建索引。比如说在实践中有这种场景,在开发的时候业务侧信心满满的跟你说这个字段肯定不会用来查询,然后你也很自信把它存储在了扩展字段或者 JSON 里面。而过了两个月,他又跑过来跟你说,我觉得这个字段筛选有用,能不能支持一下查询。如果这个时候使用 MySQL 简直要疯了,而使用 MongoDB 的化,只需要在 "xx.yy" 上面加个索引即可;另外在数组元素中加索引,MySQL 是想都不敢想的。另一方面,在 MySQL 中存储时,对象可能平铺的更开一些,一般遇到 "xx.yy",'"xx.zz" 都会展开成两个字段放在最外层。而 MongoDB 可以使用与原始 DO 甚至 DTO 对应的存储形式,查询结果看起来更直观一些。
- 很多人提到 MongoDB 都会说它不支持事务,那是在 4.0 之前的事了。MongoDB 在 4.0 开始支持事务,在 4.2 开始支持分片的事务,Spring 的 @Transactional 注解也是可以使用的。其实我个人理解,事务对性能是有一定损耗的,B 端业务使用肯定没问题,在 C 端大流量场景下,除非有强一致性要求,能不用就别用。同时一些消息中间件,定时任务也可以用来保证数据的最终一致性。
- 在 MySQL 中的横向扩展例如分库分表,在 MongoDB 当中当然也是支持的,并且支持得更好。因为 MongoDB 的开发初衷就是为了更好得支持分布式系统,所以这种在设计初都被考虑进去过。它采用了一种叫分片的形式,用多个 mongos 节点服务器作为代理节点,后面跟的是多个 shard 节点,外加配置服务器 config server。分片的规则都在 shard 节点配置好,对客户端其实是无感知的。
- 在关系型数据库支持的级联查询以及 MySQL 风靡一时的存储过程,我也做过一些调研,在 MongoDB 当中都是支持的,不过确实实现起来会比 MySQL 复杂一些。但是话说回来,在生产环境当中还是不太推荐使用级联查询,关联多张表(或多个文档)的查询还是分多次来比较好。另外存储过程现在也基本上没人使用;当初某个互联网公司有人在 MySQL 存储过程中去连接另一个数据库进行查询的时代也过去了(大家千万别这样用,也别说我说的 LOL)。
- 但是有一点,目前集团对 MongoDB 所做的一些监控,报警之类的并没有 MySQL 齐全,在使用的时候可能需要投入一些时间来运维。举个例子如果一个慢查询,在 MySQL 当中你不改可能过两几天就有人来催你了;但是在 MongoDB 当中,你需要自己去阿里云云控制台,查看监控来分析、发现问题。
性能测试
在这里先简单做一个 MongoDB 在高并发场景下的性能测试,以上面事例中的对象为例。因精力有限,此次只做写入和索引查询的性能测试,更丰富的测试在后面的文章中呈现。
数据库服务器配置
采用单机运行,16核 CPU,128G 内存,1T SSD。机器是借来进行测试的,基本可以达到单台数据库服务器性能的天花板。
并发写入
插入数据量 (条) | 耗时 (ms) | 平均QPS |
100 | 4 | 25000 |
1000 | 26 | 38462 |
10000 | 232 | 43103 |
100000 | 2228 | 44883 |
1000000 | 22632 | 44185 |
PS:进行多组测试,每组测试插入不同的数据量,每组测试中对相同的数据量进行多轮测试,取每轮结果的平均值作为该组测试的结果。每次插入采用 JAVA 代码随机生成单条上述对象并进行插入,采用并多线程提升并发量。每组数据测试前将表中数据清空,但每轮测试时保留当前数据不清空。例如:测量 10W 并发插入时,先将数据清空,然后连续调用插入 10 次,期间不清空数据,取每次耗时的平均值。
并发索引查询
查询数据量 (条) | 耗时 (ms) | 平均QPS |
100 | 3 | 33333 |
1000 | 24 | 41667 |
10000 | 108 | 92592 |
100000 | 835 | 119760 |
1000000 | 8315 | 120264 |
PS:在并发查询测试之前按照上面测试流程插入 100W 条随机数据,且在 studentNumber 字段上面创建索引。每组测试中在一定范围内随机 studentNumber 值来进行并发查询相应的次数;每组测试中进行多轮测试,取耗时结果的平均值。
总结
本文主要介绍了 MongoDB 数据库的基本使用,以及其与关系型数据库 MySQL 的对比;包括一些个人在使用当中的见解和感受。同时进行了 MongoDB 在高并发场景下的简单性能测试。从单台数据库服务器看来,MongoDB 有很高的写入、查询性能;如果采用多台服务器组成副本集(类似于 MySQL 的读写分离),或者分片(类似于 MySQL 的分库分表),将会有更好的性能。
个人建议,如果新上线的项目,特别是要快速上线,敏捷开发,建议可以在有限的精力下尝试使用 MongoDB;而如果是以前使用 MySQL 开发的老项目,建议还是继续使用 MySQL,因为这时不会带来质的飞跃,并且还是一定的数据迁移的成本。
扩展资料
- 阿里云 MongoDB 产品(地址:https://www.aliyun.com/product/mongodb)
- 前 Oracle 首席工程师怒喷:MySQL 是“超烂的数据库”,建议考虑 PostgreSQL(地址:https://blog.csdn.net/j3T9Z7H/article/details/121986807)
- 一种NewSQL:TiDB 简介 | PingCAP Docs(地址:https://docs.pingcap.com/zh/tidb/v4.0)