引言
在MySQL的早期版本,MyISAM由于其性能表现(读写快),丰富的特性(支持全文索引),也作为MySQL的默认引擎。而Memory引擎也凭借着其优秀的读写性能,在一定的场景也占有一席之地。
但随着版本的迭代,MySQL开始主推InnoDB作为表的引擎,到了5.6及以后的版本,InnoDB引擎也已成为MySQL的默认引擎。InnoDB引擎可不是 “程序里的关系户”,它究竟比其它引擎强在哪里?
对比介绍如下:
InnoDB引擎比MyISAM强在哪里?
InnoDB比MyISAM强在哪里?在前文我们分别详细介绍了MySQL的索引、并发、日志、内存管理等内容。因此,接下来将以这四点进行切入,对比介绍。
索引支持的对比
当使用InnoDB引擎建表的时候,会创建两个文件,如下:
- frm: 存放创建表的结构
- ibd:存放表的行数据和索引数据
innoDB建表的行数据和索引数据是放在一个文件内的
当使用MyISAM引擎建表时,则会创建三个三件,如下:
- frm:存放表的结构
- MYD:存放表的行数据
- MYI:存放表的索引数据
对比innoDB,MyISAM建表的行数据和索引数据是分开放的
比较两者建立的索引类型
在索引篇中,我们介绍了索引可分为:聚簇索引和非聚簇索引。对于InnoDB引擎来说,有且仅有一个聚簇索引,如果一张表中存在主键,则主键为聚簇索引;若不存在,则选择表中的唯一字段为聚簇索引;若都不存在,则使用隐藏列row_id作为聚簇索引。聚簇索引只能有一个,而非聚簇索引可以存在多个。
除此之外,聚簇和非聚簇索引的另一大特性为:
- 聚簇索引要求物理和逻辑上的空间连续
- 非聚簇索引仅要求逻辑上的空间连续
Tips:申请数组的内存空间满足物理连续;链表的内存空间则满足逻辑连续,通过指针相连
总结:
由于MyISAM建表时,表的行数据和索引数据是分开存放的,物理上没有连续性可言。因此,用MyISAM建的表均不存在聚簇索引。
而InnoDB建表时,表的行数据和索引数据是放在一个文件的,可支持聚簇索引的创建。
比较两者的B+树的叶子节点
MyISAM也支持B+树作为索引的数据结构。由MyISAM创建的表所建立的索引,均是平级关系(全是非聚簇索引)。 索引维护的B+树的叶子节点均存放指向真实数据存放的内存地址
由InnoDB创建的表中存在两种索引:聚簇索引和非聚簇索引。聚簇索引维护的B+树的叶子节点存放的是具体的行数据;非聚簇索引的B+树的叶子节点则存放对应的主键值。
在进行读操作时,如果需要用到索引,两种引擎的读区别如下:
如果是InnoDB创建的表,若用到的是普通索引,则很有可能需要通过回表,也就是访问两次B+树,才能找到完整的数据。
如果是MyISAM创建的表,表中所有的索引都是平级的,任意索引的B+树叶子节点均指向真实数据。因此不需要通过回表,就能找到完整数据。
从理论上看,MyISAM引擎的查询速度是要快于InnoDB的,但事情没这么简单,我们往后看!
并发方面上的对比
提到数据库的并发,就不得不提到事务,事务的特点以及事务引起的并发问题和相应的解决方案。因此从并发的角度出发,两者的区别主要在:事务、MVCC、Lock
事务机制的对比
当引擎为InnoDB时,MySQL是支持事务的。在并发篇中:MySQL的事务实现机制,我们提到,MySQL的事务运行分为三个阶段:
- begin,一个事务的开始标志
- 执行阶段,在事务开启后,执行相应的sql
- commit,一个事务的结束标志
如果在执行阶段发生了错误,由于事务具有原子性,可通过执行rollback命令,回滚之前的操作。关键就在这里,事务是如何保证原子性的呢?
在InnoDB启动时,存在一块内存空间,专门存储事务执行时产生的旧版本数据,即undo-log buffer。当该事务需要回滚时,就通过undo-log buffer里的旧数据来覆盖新数据。就可以将数据变为事务执行前的状态。
通过InnoDB引擎创建的表,可以通过undo-log buffer(内存)及undo-log(磁盘)来实现事务。
但是,当引擎为MyISAM时,MySQL是不支持事务的。undo-log回滚机制是InnoDB所独有的,并非MySQL Server层的东西。MyISAM没有类似于回滚机制的设计,如果没有InnoDB引擎,MySQL在启动的时候是不会有undo-log buffer的,因此磁盘也不会有undo-log。所以,MyISAM不支持事务。
MVCC版本比较
由上文的事务机制对比,我们了解到:MyISAM引擎由于不存在undo-log回滚机制,是不支持事务的。而实现MVCC最重要的一环就是版本链的控制,也是通过undo-log日志来完成的。因此,MyISAM引擎同样不支持MVCC。而InnoDB引擎支持事务,天然支持MVCC。
锁粒度的比较
大家看比较InnoDB和MyISAM的八股文应该都知道:InnoDB支持表锁和行锁;但MyISAM只支持表锁,不支持行锁。那为什么MyISAM不支持行锁呢?
MyISAM为什么不支持行锁?
MyISAM不支持行锁,本质上和索引有关系。在索引的比较中,我们已经了解到:MyISAM引擎是不支持聚簇索引的,创建的索引均是非聚簇索引,且索引树的叶子节点存放的是指向真实行数据的地址。
在介绍MySQL的锁时,我们了解到行锁的本质是在某一条行数据的索引上加锁。如果MyISAM也实现行锁,要对一行数据的索引树上加锁,是可以锁定该索引树上的行数据,但是没有办法锁定其它索引树上对应的行数据。(注意这里锁的是:真实行数据的地址)
假设存在如下情况:通过MyISAM创建了一张user表,有列:id,name;分别建立索引;插入一条数据:
insert into user (id, name) values (1, "test");
假设该条数据所在的内存地址为:0x8888。此刻事务A执行一条查询sql,即:
select * from user where id = 1 for update;
我们知道,该条sql会去访问列名为id的非聚簇索引树,找到id=1的数据,对其真实的行数据地址加锁,将内存地址为:0x8888的数据上锁!
在同一时刻,如果此刻由于其它数据的增删改,发生了页分裂,导致id=1的数据的内存地址发生了变化,假设变为:0x8889。事务B也执行一条查询sql,即:
select * from user where name = "test" for update;
该条sql会去访问列名为name的非聚簇索引树,找到name="test"的数据,也对其真实的行数据地址加锁,地址为:0x8889。将其锁住!
我们发现,分明是相同的数据,但由于地址发生了变化,无法正确对真实的地址上锁!两个不同的事务可以同时操作一条数据,还是会造成脏读,不可重复读、幻读等问题!
如果我们通过给MyISAM加入一个检测地址更新的机制,在id=1的数据的地址从0x8888变为0x8889时,及时去更新下锁的地址(0x8888->0x8889)。可以解决该问题吗?
肯定是不行的,对于更新地址的线程来说,我们还得保证一件事,更新锁地址的操作,要快于其它线程获取新地址锁的操作! 好像事情变得越来越麻烦了~~~
InnoDB为什么能支持行锁?
由InnoDB引擎建立的表,在创建索引时,分为聚簇索引和非聚簇索引(索引数据和真实数据都保存在同一个文件中)。聚簇索引树的叶子节点存放真实的行数据;普通索引的叶子节点存放对应的主键值。还是上面的例子,换成了由InnoDB引擎建立的表user,主键为id
在执行sql(1)的时候,会访问聚簇索引树,找到id=1的真实行数据,对真实数据上锁。
对于sql(2)来说,则是访问普通索引树,找到name="test"的数据主键为1,再访问聚簇索引树。但此刻id=1的真实数据已经被上锁啦,因此该线程会被阻塞住。
因此,由于MyISAM不支持聚簇索引,因此无法实现行锁。在出现多线程并发时,只能通过表锁来确保数据的一致性。而InnoDB支持聚簇索引,所有的索引树最终都是要通过聚簇索引树才能找到完整的行数据。因此,在出现多线程并发时,只要锁住聚簇索引树下的真实行数据,即可实现行锁。
日志方面上的对比
MySQL里有几个重要的日志,分别为:redo-log,undo-log,binlog。三者的作用分别为:
- redo-log: 保证数据不丢失,断电及恢复,事务的持久性保证
- undo-log:保证事务的原子性,且MVCC的实现机制
- binlog:数据备份使用,主从复制、归档
下面将分别从MySQL的故障恢复、事务原子性、数据备份等角度去对比
故障恢复的比较
在前文中,我们详解了MySQL是如何做到数据不丢失的?即:redo-log是如何保证事务的持久性的? 当有数据写入到MySQL时,其实不会马上去写磁盘,而是会先写在内存里,并记录在redo-log日志中。在数据还未真正持久化,若发生宕机,MySQL也能通过redo-log日志保证数据不丢失。而redo-log日志是作用在InnoDB引擎上的,非MySQL Server层。
对于MyISAM引擎来说,是没有redo-log的,所以并不支持数据的故障恢复。如果通过MyISAM创建的表数据写到内存里,此刻MySQL突然宕机,内存里的数据是会直接丢失的。
从这点上来说,InnoDB引擎远远比MyISAM来得可靠很多,数据会有丢失的风险这也是MySQL万万不能接受的。就好比你去银行存钱,在银行告知你存储成功后,后面突然发现你的钱怎么没了?
事务原子性的比较
在上文中,我们在事务机制上的对比中,就有提及到undo-log是InnoDB引擎所特有的,MyISAM并不支持undo-log。因此,MyISAM也没有事务原子性的说法。
而InnoDB引擎支持undo-log日志,保证了事务的原子性。即:一个事务内执行的sql要么全部成功,要么全部失败。事务的原子性详解见:undo-log是如何保证事务的原子性的?
数据备份上的对比
MySQL依赖于binlog日志来实现数据备份。而binlog日志是作用于MySQL Server层的,MySQL出厂自带。也就是说:无论是使用InnoDB还是MyISAM,都可以通过binlog来实现数据备份。
内存利用上的对比
InnoDB-Buffer Pool的开发利用
在前文中,我们详解了MySQL是如何管理内存的,详见:MySQL是如何管理内存的? 在MySQL进程启动的时候,就会有一块连续的内存空间:Innodb-buffer pool供InnoDB引擎使用。InnoDB引擎将该内存运用到了极致:无论是读和写的操作,都有对应的缓存来提高效率(change buffer,redo-log buffer,data page等等)。在功能方面上,几乎是实现了基于内存运行。
查询缓存的使用和弊端
对于MyISAM引擎来说,对于内存的利用上远没有InnoDB引擎来得全面。实际使用中,大量的操作还是会去操作磁盘。对于读操作,MyISAM依赖于Server层中的查询缓存来提高效率。而查询缓存也并非像其设计的初衷那般,那么好用,具体的利与弊如下:
既然有了InnoDB引擎,MyISAM可以被完全替换掉吗?
在上文,我们分别从索引、锁、日志、内存等角度去对比了两者的区别,得出来的结论是:MyISAM几乎是全方面被InnoDB吊打。也正是如此,在MySQL5.6及后面的版本,InnoDB彻底代替了MyISAM成为了MySQL的默认引擎。
但MyISAM引擎真的是一无是处吗?其实还是有点用的,MyISAM也具有一些连InnoDB也没有的特性,稍微了解下
统计总数的优化
当我们想要获取一张表的总条数,即执行如下sql:
select count(*) from table_name;
对于count()计数型的操作,在MyISAM引擎中会直接记录表的行数。因此,如果表的引擎是MyISAM的话,直接获取之前统计的值并返回即可。
如果表是InnoDB引擎的话,是不会存储统计总数的值。当需要执行count()相关的计数操作时,则需要通过InnoDB引擎接口去获取数据,再由Server层去统计符合条件的行数。详细可见:count(*)在索引中的实际应用
但是MyISAM关于统计总数的优化,也仅局限于统计全表的数据量,如果计数的时候后面跟了where筛选条件:
select count(*) from table where xxx = "xxx";
那MyISAM这个特性就失效了,毕竟where条件千变万化,MyISAM也不可能一一都会存储对应的数据量总数。那这个时候,InnoDB和MyISAM的工作流程就均是相同的了,先获取数据量,再一一筛选符合条件的数据条数。
CRUD的速度更快
在上文我们比较了InnoDB和MyISAM的索引支持类型,知道由MyISAM建立的索引树均为非聚簇索引,且叶子节点存放的是真实数据的存储地址。因此,在查询数据时,如果能命中索引,只需要走一次索引树就能找到数据。不像InnoDB引擎,需要通过回表,再走一次聚簇索引才能找到完整数据。
在写数据的时候,由于MyISAM所建立的索引都是非聚簇索引,索引之间是相互独立的。不像InnoDB引擎,需要维护不同索引之间的关系。
因此,从理论的角度上出发,MyISAM的读写效率是会高于InnoDB的。
但在实际的生产环境中,InnoDB的读写性能未必会输MyISAM。主要是由以下两点原因:
- InnoDB支持行锁,锁冲突的概率更小。在高并发的场景下,行锁的性能肯定是更出色的,远胜于表锁。
- InnoDB将内存利用开发到极致,我们的读写操作,大多数情况都是直接读的内存,写的内存。做了大量的缓存优化,这也是MyISAM所不具备的。
官网的二者对比测试图(读写并发 and 只读)如下:
由上图可知,随着CPU核心的增加,InnoDB的性能呈现不断上升趋势,而MyISAM则几乎没有变化。
MyISAM的使用场景
根据前面对MyISAM的分析,MyISAM引擎适用于不需要事务(不支持事务)、没什么并发(只支持到表锁) 的场景,但现在这种情况应该是比较少了哈哈
有了InnoDB引擎,还需要Memory引擎吗?
在InnoDB已成为MySQL的默认引擎后,Memory凭借着优异的读写性能,仍然有着自己的一席之地。接下来我们将详细介绍Memory引擎的性能特点
Memory表的底层结构长啥样?
为了更深入了解Memory引擎,我们引入一个例子,存在两张表t1和t2(主键为id,存在一列a),t1和t2表的引擎为Memory和InnoDB。依次插入如下数据:
insert into t1 values(1,1),(2,2),(3,3),(4,4),(5,5),(6,6),(7,7),(8,8),(9,9),(0,0);
insert into t2 values(1,1),(2,2),(3,3),(4,4),(5,5),(6,6),(7,7),(8,8),(9,9),(0,0);
接下来在两张表执行:select * from t1; select * from t2;,结果如下:
对比可知,由InnoDB所创建的表t2返回的数据是按升序排列返回的。id=0这条数据在第一行。我们知道,InnoDB的主键索引树为B+树,且叶子节点存放的是真实数据,数据天然有序,底层的叶子节点数据从左到右依次变大。如下:
当执行select *时,就会按照叶子节点从左到右扫描。因此得到的结果里,id=0就出现在第一行。
由Memory所创建的表t1返回的数据里,id=0数据出现在最后一行。这个就要从Memory存储的数据结构说起了,如下:
Memory所建的表的数据部分是以数组的方式单独存放的,而主键ID索引里,存的是每个数据的位置。将id通过hash计算,找到id对应在索引里的位置,再通过索引存储的地址,找到数据。主键id是hash计算后的值,所以索引上的key并非有序。
在对表t1执行insert语句时,id=0是最后一条插入的数据。因此id=0放于数组的最末端。当执行select *时,会顺序扫描该数组,因此id=0的数据被放到了数组最后面。
对比二者的数据结构,可得出以下结论:
- InnoDB的表数据总是有序存储的;而Memory表的数据是按写入顺序来存放的。
- 当数据页有空洞时,InnoDB表在插入数据时,仍旧会遵循有序存储,在固定的位置插入;而Memory表是找到空位,就可以直接插入。
- 在用到普通索引时,InnoDB表可能会经历回表;而Memory表仅查找一次即可。
由于Memory表的特性,数据在被删除后,空出的位置可直接被要插入的数据复用,如下:
如上,先删除表t1中的id=5的行数据,随即插入一条id=10的行数据,再查询一次全量结果。可看出id=10这一条数据出现在原先id=5的数据位置上。
当Memory表碰到范围查询怎么办?
我们可以将Memory表的主键索引数据结构看成一个K-V结构的Hash表。而Hash表的单点查询效率是最高的,只需要通过一次Hash计算即可找到数据(假设没有hash冲突)。但如果在Memory表上碰到了范围查询呢?该怎么办?(Hash计算是无序的,范围查询对Hash结构来说是个严重影响效率的操作)
对于Hash表的范围查询,需要去全表扫描,找到符合条件的数据,再返回结果集,十分影响效率。
实际上,Memory引擎也是支持B+树结构的,也可以在列上直接建立一个B-Tree树索引,此刻建了一个非聚簇索引,叶子节点存放的是真实数据的地址。
我们给表t1中的id字段建一个B-Tree树索引,如下:
alter table t1 add index a_btree_index using btree (id);
紧接着,再执行:
select * from t1 where id < 5;
可看到返回的结果如下:
返回的结果是有序的,id=0这行数据出现在第一行。再执行explain查看执行结果:
可看到,MySQL的优化器选择了我们创建的B-Tree树索引。这也很好解决了原先范围查询需要全表扫描的烦恼。
如果我们强制让范围查询的sql走主键id(Hash表)索引,就会看到如下结果:
没有走索引,选择全表扫描去找到符合id<5的数据。
为什么不建议使用Memory引擎?
Memory引擎的数据都放在内存,而内存的读写速度肯定是优于磁盘的。并且通过建立B-Tree索引来解决范围查询的问题,但还是不推荐在生产环境上使用Memory表存储业务数据。原因如下:
- Memory引擎不支持行锁
- MySQL重启后,Memory表的数据均会被清空
Memory锁粒度问题
Memory表不支持行锁,只支持表锁。因此,只要一张表的数据有更新,就会堵住其它线程在这张表上的读写操作。在生产环境上,锁冲突的概率是非常高的,性能也不会太好。
Memory数据持久化问题
Memory表的数据都是直接放在内存里的,是优势,同样也是劣势。优势在于读写的性能会很高;而劣势在于只要MySQL重启,表的数据都会被清空。
在主从(M-S)架构里,如果主库的表通过InnoDB建立,而备库的表为了提高查询性能,表通过Memory建立。如果备库突然重启,使得Memory表的数据被清空。此刻就会导致主备数据不一致的现象。
Memory引擎适用于哪些情景?
针对于Memory引擎存在的弊端,最好是适用于如下场景:
- 内存表不会有很多线程访问,没有并发性的问题。
- 内存表里的数据是过渡性的,数据丢失不影响业务。
- 备库的内存表尽量不要影响到主库的线程。
在前文我们有提到过join的使用最好是要让小表去驱动大表,而大表需要合理建立和小表对应的索引。将BLJ升级成NLJ算法,提高查询效率。详解可见: Join的详细使用分析
其中有提到,如果这条join是一条很低频的sql语句,在大表上新增一个索引是浪费资源的行为(维护索引也需要成本)。在当时我们的做法是:建立一个临时表,将大表的数据插入到临时表,并在临时表上建立索引来替换大表。
但其实这里使用内存临时表来替换临时表的效果更好,原因如下:
- 临时表也是通过InnoDB引擎建立的,相较于Memory表,读写速度会慢一些。
- Memory表建立的hash索引,查找速度会优于B+ Tree索引。
因此,可以将创建临时表替换成内存表。也满足我们对Memory引擎的使用场景:该定制的Memory表只有当前线程访问;临时数据重启后丢失不会造成业务影响;不影响主库的业务数据