对于后端开发来说,数据库是我们日常开发中几乎都会使用到的。而且对于许多大型应用来说,往往数据库就是限制其性能的瓶颈所在。在以前的大多数时间里面,对数据库的认知,始终停留在一个比较浅的层面里。遂决定翻阅相关的书籍、博客和官方文档,让自己对数据库有一个全面的了解。
MySQL架构
以下是MySQL大体的组件结构
如果只关心和聚焦于客户端向MySQL发送一条sql语句大致主要会涉及到的Server层的组件,可以查看下图:
上面这张图的箭头标识和注释也说得比较清楚了。如果想要更加详细的说明,可以自行查阅相关文献资料。需要说明的是在MySQL8.0中,已经将查询缓存整个去掉了。
MySQL内置存储引擎介绍
上图介绍的MySQL架构大致可以分成Server层和存储引擎层的。MySQL提供的基于插件式的存储引擎,使得我们可以根据不同的需要选择不同的引擎,甚至是在同一个schema中的不同表,也可以使用不同的存储引擎。而实际的数据,也是存储在存储引擎中的。不同的存储引擎的架构和对数据的组织方式也有所不同。正是这些不同,决定了这些存储引擎提供了不同的特性和功能。
内置存储引擎
我们可以使用show engines来查看当前mysql内置了哪些存储引擎
三种常见的存储引擎区别及介绍
InnoDB
InnoDB从5.1版本以后,已经取代了MyISAM,成为MySQL默认的存储引擎了。可以从上表上看到,InnoDB是唯一支持事务、XA和Savepoints的内置存储引擎。同时,它还支持行锁、外键约束等。InnoDB表基于聚簇索引建立,并且采用MVCC来支持高并发,同时实现了ANSI SQL92定义的四种隔离级别,并在引擎内部实现了redo log和undo log。这些特性组合在一起,使得InnoDB成为了一个适合处理大量数据的高性能事务引擎。对于DBA来说,结合server层的bin log组成的日志系统机制,使得使用InnoDB作为数据存储引擎的数据库具备安全的崩溃恢复能力和快速稳定的复制性能,这些都是其它存储引擎所不具备的。所以在Oracle收购MySQL以后,没有了版权问题,InnoDB就毫无争议地成为了MySQL的默认存储引擎了。
MyISAM
在MySQL5.1及之前的版本,MyISAM都是默认的存储引擎。虽然不支持事务、不支持行级锁,崩溃后无法安全恢复。但是还MyISAM还是提供了许多其它的特性,包括全文索引、压缩、空间函数(GIS)等。这些特性在某些场景下的性能是高于其它存储引擎的,比如需要存储和批量查询归档日志数据,MyISAM引擎能提供较高的处理效率。
Memory
Memory存储引擎将表中的数据存储到内存中,为查询和引用其他表数据提供快速访问。因为是存在内存中,所以数据访问的速度一般也要快于其它存储引擎,同时Memory支持Hash索引,因此在单值查找的速度非常快。同时,MySQL在执行查询的过程中需要使用临时表来保存中间结果,内部使用的临时表就是Memory表(如果结果集大小超出Memory表的限制,则会转换成MyISAM表)。
使用哪一种引擎需要灵活选择,一个数据库中多个表可以使用不同引擎以满足各种性能和实际需求,使用合适的存储引擎,将会提高整个数据库的性能
功 能 | MYISAM | Memory | InnoDB |
---|---|---|---|
存储限制 | 256TB | RAM | 64TB |
支持哈希索引 | No | Yes | No |
支持全文索引 | Yes | No | No |
支持数索引 | Yes | Yes | Yes |
支持数据缓存 | No | N/A | Yes |
支持外键 | No | No | Yes |
InnoDB内存/磁盘结构及存储逻辑结构
InnoDB总体架构
上面这张图是InnoDB存储引擎在内存和磁盘上的对应结构。
这里分别取两个最常操作的update和select操作大致描述一下内部的流转机制(在默认的可重复读级别下, 同时略去了所有有关加锁释放锁的操作):
1.select * from xxx where id=1 语句:
(1)引擎接收到执行计划,会创建一个trx_id。
(2)查询是否在这个内存中,如果在,则返回行。如果在change buffer中或者不在内存中,则从磁盘中读入内存(在change buffer中还要涉及merge操作更新内存中的数据)。引擎层拿到数据,返回给到server层的执行器。
(3)如果Innodb发现某二级索引被频繁访问,会对该索引上创建一个哈希索引。下次同样的查询过来,会直接走这个自适应哈希索引。
2.update xxx set xxx where id=1 语句:
(1)引擎接收到执行计划,会为该事务创建一个trx_id。
(2)查询是否在这个内存中,如果在,则返回行。如果在change buffer中或者不在内存中,则从磁盘中读入内存(在change buffer中还要涉及merge操作更新内存中的数据)。引擎层拿到数据,返回给到server层的执行器。
(3)执行器更新这行的相关列数据,再通过API接口调用引擎层,更新修改后的行数据更新到内存中。同时写入redo log,此时处于prepare阶段。
(4)引擎层告知执行器已经已经执行完成,随时可以提交事务。
(5)server层的执行器将该操作写入bin log
(6)执行期通过API接口告知引擎提交事务,引擎把刚刚写入的redo log标记为commit状态,更新完成。(两阶段提交)
上面就是5.5版本下的文件组织,可以看到,每个schema都有一个文件夹,文件夹里面有 db.opt和*.frm格式的文件。db.opt存放的是字符集和字符集排序规则信息(字符文件,可以打开),frm文件存的是表结构等信息,官方同时也给出说明,frm里的信息和InnoDB数据字典有所重叠,是出于历史遗留原因(参见)。另外如果你没有设置innodb_file_per_tale=ON那么所有的数据文件都将存储在ibdata1文件中。所以这个文件会随着数据的增长而增长。同时我们也可以看到,InnoDB结构图中5.7相对与5.5最大的变化,就是对于ibdata1的拆分,它把原本共享表空间,undo表空间和临时表空间从ibdata1中分了出来。好处当然就是结构更加清晰,更加方便独立管理,不会出现之前临时表空间不再使用释放了,ibdata1文件还是那么大等情况。
InnoDB的数据逻辑结构
从上面InnoDB的架构图里面的右半部分可以知道,无论是索引还是数据,InnoDB都把它们存在.idb后缀(或者ibdata1)的文件中。而在MyISAM中,索引和数据是分别存储在MYI和MYD文件中的。
下图是InnoDB的数据组织形式
从图中可以看出,InnoDB数据其实就是保存在聚簇索引的叶子节点中的,并且按照主键列顺序存储在数据文件中的。
相比之下,MyISAM的数据则是按照插入的顺序存储在磁盘上的,其索引的叶子节点存储的是可以定位到实际数据的行号(或者是可以找到其物理位置的地址,这里隐藏了页的物理细节)。
这两种数据组织形式,使得下面两种引擎有如下区别:
1.由于使用聚簇索引,所以无法同时把数据行存放在两个地方,所以一个表只能有一个聚簇索引。而InnoDB的二级索引的叶子节点也只能存储聚簇索引上的主键值,从而导致二级索引(除覆盖索引以外)在查询数据的时候需要回表。
2.也由于MyISAM的索引不存在聚簇索引,叶子节点存储的是实际数据的行号,所以对MyISAM而言,主键索引和其它索引一样,不存在定位实际数据块上的性能差异。
这里有一个有意思的问题,如果InnoDB的二级索引的叶子节点和MyISAM一样,存储的是可以直接找到实际数据的行号,那岂不是可以避免了回表的问题。我个人感觉确实是这样的。但是那样会存在一个问题,那就是行锁和间隙锁的加锁问题。我们知道,InnoDB的行锁其实质就是加在索引上面加的锁,只要访问到该索引,就会在对应的索引上面加锁(包含回表的加锁)如果不回表的话,那么不同索引上面加锁并且相互独立,那么行锁和间隙锁就毫无意义了。反过来说,InnoDB为什么会做一个回表这样的逻辑,其实是在牺牲部分二级索引定位数据页的性能,来换取更细粒度的锁带来的显著性能提升。另外,如果在二级索引上存储的是实际数据的行号,那在数据页调整的时候,也要对这些二级索引进行更新,这同时也会导致写性能的下降。
InnoDB的行锁和间隙锁
前面有提到,InnoDB的锁是加在索引上的,行锁的引入减少了锁竞争的情况,从而提高了并发度。在不同隔离级别的不同语句下,加锁情况也是不一样的。从上图可以知道,有一些查询甚至不需要加锁,通过基于MVCC实现的一致性读就可以达到对应的隔离级别,这里又进一步提高了并发度。
总结
其实当我们大概了解了InnoDB架构组件中各个组件的作用,以及其数据存储的逻辑结构。也就大概明白了为什么InnoDB提供了这么多其它存储引擎不能提供的相关特性。例如:
1.redo log及对应的两阶段提交协议的引入,使得引擎可以提供在系统崩溃的时候提供安全崩溃恢复机制。
2.undo log机制的引入,每一事务都有一个单调递增的trx_id,使得Innodb可以基于MVCC对相关的事务做一致性读(或者称为快照读)。
3.InnoDB基于聚簇索引的数据组织形式,多数情况下通过行锁,间隙锁和快照读实现了四种隔离级别。这种方式相比与直接加表锁,性能更高,更加适合高并发场景。
通过根据其作用推出它能提供的特性,反过来也加深我们对各个组件的作用的理解。同时,通过思考这些组件的设计思想来实现对应的特性,这本身就是一个很有趣的过程。