(九)MySQL引擎介绍:InnoDB为何能替代MyISAM和Memory?

引言

在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;分别建立索引;插入一条数据:

sql 复制代码
    insert into user (id, name) values (1, "test");

假设该条数据所在的内存地址为:0x8888。此刻事务A执行一条查询sql,即:

sql 复制代码
    select * from user where id = 1 for update;

我们知道,该条sql会去访问列名为id的非聚簇索引树 ,找到id=1的数据,对其真实的行数据地址加锁,将内存地址为:0x8888的数据上锁!

在同一时刻,如果此刻由于其它数据的增删改,发生了页分裂,导致id=1的数据的内存地址发生了变化,假设变为:0x8889。事务B也执行一条查询sql,即:

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:

csharp 复制代码
    select count(*) from table_name;

对于count()计数型的操作,在MyISAM引擎中会直接记录表的行数。因此,如果表的引擎是MyISAM的话,直接获取之前统计的值并返回即可。

如果表是InnoDB引擎的话,是不会存储统计总数的值 。当需要执行count()相关的计数操作时,则需要通过InnoDB引擎接口去获取数据,再由Server层去统计符合条件的行数。详细可见:count(*)在索引中的实际应用

但是MyISAM关于统计总数的优化,也仅局限于统计全表的数据量 ,如果计数的时候后面跟了where筛选条件

sql 复制代码
    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。依次插入如下数据:

scss 复制代码
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树索引,如下:

sql 复制代码
alter table t1 add index a_btree_index using btree (id);

紧接着,再执行:

csharp 复制代码
    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表只有当前线程访问;临时数据重启后丢失不会造成业务影响;不影响主库的业务数据

相关推荐
夜泉_ly1 小时前
MySQL -安装与初识
数据库·mysql
qq_529835352 小时前
对计算机中缓存的理解和使用Redis作为缓存
数据库·redis·缓存
月光水岸New4 小时前
Ubuntu 中建的mysql数据库使用Navicat for MySQL连接不上
数据库·mysql·ubuntu
狄加山6754 小时前
数据库基础1
数据库
我爱松子鱼4 小时前
mysql之规则优化器RBO
数据库·mysql
chengooooooo5 小时前
苍穹外卖day8 地址上传 用户下单 订单支付
java·服务器·数据库
Rverdoser6 小时前
【SQL】多表查询案例
数据库·sql
Galeoto6 小时前
how to export a table in sqlite, and import into another
数据库·sqlite
人间打气筒(Ada)6 小时前
MySQL主从架构
服务器·数据库·mysql
leegong231116 小时前
学习PostgreSQL专家认证
数据库·学习·postgresql