电子说
这篇文章会通过一条SQL更新语句的执行流程让大家清楚地明白:
正文开始!
之前我们讲过了一条SQL查询语句是如何执行的,那么插入(INSERT)、更新(UPDATE)和删除(DELETE)操作的流程又是什么样子呢?
其实对于MySQL而言,只有两种通常意义的操作,一种是Query(查询),另一种是Update(更新),后者包含了我们平常使用的INSERT、UPDATE和DELETE操作。
那么MySQL的更新流程和查询流程有什么区别呢?
其实基本的流程是一致的,也要经过 处理连接 、 解析优化 、存储引擎几个步骤。主要区别在更新操作涉及到了MySQL更多的细节。
注:我们接下来的所有描述,针对的都是InnoDB存储引擎,如果涉及到其他存储引擎,将会特殊说明
对于MySQL任何存储引擎来说,数据都是存储在磁盘中的,存储引擎要操作数据,必须先把磁盘中的数据加载到内存中才可以。
那么问题来了,一次性从磁盘中加载多少数据到内存中合适呢?当获取记录时,InnoDB存储引擎需要一条条地把记录从磁盘中读取出来吗?
当然不行!我们知道磁盘的读写速度和内存读写速度差了几个数量级,如果我们需要读取的数据恰好运行在磁盘的不同位置,那就意味着会产生多次I/O操作。
因此,无论是操作系统也好,MySQL存储引擎也罢,都有一个预读取的概念。概念的依据便是统治计算机界的局部性原理。
空间局部性:如果当前数据是正在被使用的,那么与该数据空间地址临近的其他数据在未来有更大的可能性被使用到,因此可以优先加载到寄存器或主存中提高效率
就是当磁盘上的一块数据被读取的时候,我们干脆多读一点,而不是用多少读多少。
InnoDB存储引擎将数据划分为若干个页,以页作为磁盘和内存之间交互的最小单位。InnoDB中页的大小默认为16KB。也就是默认情况下,一次最少从磁盘中读取16KB的数据到内存中,一次最少把内存中16KB的内容刷新到磁盘上。
对于InnoDB存储引擎而言,所有的数据(存储用户数据的索引、各种元数据、系统数据)都是以页的形式进行存储的。
为了更好地管理页,MySQL又设计了「表空间」的概念。表空间又有很多类型,具体类型我们不需要知道,我们只需要知道,一个表空间可以划分成很多个InnoDB页,InnoDB表数据都存储在某个表空间的页中。
为了方便我们定位,MySQL贴心地为表空间设计了一个唯一标识——表空间ID(space ID)。同理,InnoDB页也有自己的唯一编号——页号(page number)。
因此,我们可以这么认为。给定表空间ID和页号以及页的偏移量,我们就可以定位到InnoDB页的某条记录,也就是数据库表的某条记录。
为了更好地让大家理解这个抽象的概念,我创建了名为test
的数据库,在其下分别创建了3张表t_user_innodb
,t_user_myisam
,t_user_memory
,对应的存储引擎分别为InnoDB
、MyISAM
、MEMORY
。
进入MySQL的数据目录,找到test
目录,看一下test
数据库下所有表对应的本地文件目录
drwxr-x--- 2 mysql mysql 4096 Jan 26 09:28 .
drwxrwxrwt 6 mysql mysql 4096 Jan 26 09:24 ..
-rw-r----- 1 mysql mysql 67 Jan 26 09:24 db.opt
-rw-r----- 1 mysql mysql 8556 Jan 26 09:28 t_user_innodb.frm
-rw-r----- 1 mysql mysql 98304 Jan 26 09:28 t_user_innodb.ibd
-rw-r----- 1 mysql mysql 8556 Jan 26 09:27 t_user_memory.frm
-rw-r----- 1 mysql mysql 0 Jan 26 09:28 t_user_myisam.MYD
-rw-r----- 1 mysql mysql 1024 Jan 26 09:28 t_user_myisam.MYI
-rw-r----- 1 mysql mysql 8556 Jan 26 09:28 t_user_myisam.frm
「表空间」是InnoDB存储引擎独有的概念。
我们看到t_user_innodb
表在数据库对应的test
目录下会生成以下两个文件
其中,t_user_innodb.ibd就是t_user_innodb
表对应的表空间在文件系统上的表示;t_user_innodb.frm用来描述表的结构,如表有哪些列,列的类型是什么等。
和InnoDB不同,MyISAM没有表空间的概念,表的数据和索引全都直接存放在对应的数据库子目录下,可以看到t_user_myisam
对应了三个文件
其中,t_user_myisam.MYD表示表的数据文件,也就是我们实际看到的数据表的内容;t_user_myisam.MYI表示表的索引文件,为该表创建的索引都会存放在这个文件中;t_user_myisam.frm用来描述表的结构。
MEMORY存储引擎对应的数据表只有一个描述表结构的文件t_user_memory.frm。
为了更好的利用局部性原理带给我们的优势,InnoDB在处理客户端请求时,如果需要访问某个页的数据,会把该数据所在的页的全部数据加载到内存中。哪怕是只需要访问一个页中的一条数据,也需要加载整个页。
从磁盘中加载数据到内存中的操作太昂贵了!有什么办法可以提高数据操作的效率呢?缓存!
为了缓存磁盘的页,InnoDB在MySQL服务器启动时会向操作系统申请一片连续的内存区域,这片内存区域就是 Buffer Pool 。
很容易理解,为了更好地缓存页数据,Buffer Pool对应的一片连续内存空间也被划分为若干个页,而且默认情况下,Buffer Pool页的大小和InnoDB页大小一样,都是16KB。为了区分两种不同的页,我们将Buffer Pool中的页面称为缓冲页。
读取数据的时候,InnoDB先判断数据是否在Buffer Pool中,如果是,则直接读取数据进行操作,不用再次从磁盘加载;如果不是,则从磁盘加载到Buffer Pool中,然后读取数据进行操作。
修改数据的时候,也是将数据先写到Buffer Pool缓冲页中,而不是每次更新操作都直接写入磁盘。当缓冲页中的数据和磁盘文件不一致的时候,缓冲页被称为脏页。
那么脏页是什么时候被同步到磁盘呢?
InnoDB中有专门的后台线程每隔一段时间会把脏页的多个修改刷新到磁盘上,这个动作叫做「刷脏」。
不定时刷脏又带来一个问题。如果脏页的数据还没有刷新到磁盘上,此时数据库突然宕机或重启,这些数据就会丢失。
首先想到的最简单粗暴的解决方案就是在事务提交之前,把该事务修改的所有页面都刷新到磁盘。但是上文说过,页是内存和磁盘交互的最小单位,如果只修改了1个字节,却要刷新16KB的数据到磁盘上,不得不说太浪费了,此路不通!
所以,必须要有一个持久化的措施。
为了解决这个问题,InnoDB把对所有页的更新操作(再强调一遍,包含INSERT、UPDATE、DELETE)专门写入一个日志文件。
当有未同步到磁盘中的数据时,数据库在启动的时候,会根据这个日志文件进行数据恢复。我们常说的关系型数据库的ACID
特性中的D
(持久性),就是通过这个日志来实现的。
这个日志文件就是大名鼎鼎的 redo日志 。
「re」在英文中的词根含义是“重新”,redo就是「重新做」的意思,顾名思义就是MySQL根据这个日志文件重新进行操作
这就出现了一个有意思的问题,刷新磁盘和写redo日志都是进行磁盘操作,为什么不直接把数据刷新到磁盘中呢?
我们需要稍微了解一下磁道寻址的过程。磁盘的构造如下图所示。
每个硬盘都有若干个盘片,上图的硬盘有4个盘片。
每个盘片的盘面上有一圈圈的同心圆,叫做「磁道」。
从圆心向外画直线,可以将磁道划分为若干个弧段,每个磁道上一个弧段被称之为一个「扇区」(右上图白色部分)。数据是保存在扇区当中的,扇区是硬盘读写的最小单元,如果要读写数据,必须找到对应的扇区,这个过程叫做「寻址」。
如果我们需要的数据是随机分散在磁盘上不同盘片的不同扇区中,那么找到相应的数据需要等到磁臂旋转到指定的盘片然后继续寻找对应的扇区,才能找到我们所需要的一块数据,持续进行此过程直到找完所有数据,这个就是随机I/O,读取数据速度非常慢。
假设我们已经找到了第一块数据,并且其他所需的数据就在这一块数据之后,那么就不需要重新寻址,可以依次拿到我们所需的数据,这个就叫顺序 I/O。
现在回答之前的问题。因为刷脏是随机I/O,而记录日志是顺序I/O(连续写的),顺序I/O效率更高,本质上是数据集中存储和分散存储的区别。因此先把修改写入日志文件,在保证了内存数据的安全性的情况下,可以延迟刷盘时机,进而提升系统吞吐。
redo日志位于MySQL数据目录下,默认有ib_logfile0
和ib_logfile1
两个文件,如下图所示。
可以发现,两个redo日志文件的大小都是50331648,默认48MB。为什么这个大小是固定的呢?因为如果我们要使用顺序I/O,就必须在申请磁盘空间的时候一次性决定申请的空间大小,这样才能保证申请的磁盘空间在地址上的连续性。
这也就决定了redo日志的旧数据会被覆盖,一旦文件被写满,就会触发Buffer Pool脏页到磁盘的同步,以腾出额外空间记录后面的修改。
可以通过以下指令查看redo日志的系统属性。
mysql> show variables like 'innodb_log%';
+-----------------------------+----------+
| Variable_name | Value |
+-----------------------------+----------+
| innodb_log_buffer_size | 16777216 |
| innodb_log_checksums | ON |
| innodb_log_compressed_pages | ON |
| innodb_log_file_size | 50331648 |
| innodb_log_files_in_group | 2 |
| innodb_log_group_home_dir | ./ |
| innodb_log_write_ahead_size | 8192 |
+-----------------------------+----------+
参数名称 | 含义 |
---|---|
innodb_log_file_size | 指定每个redo日志文件的大小,默认48MB |
innodb_log_files_in_group | 指定redo日志文件的数量,默认2 |
innodb_log_group_home_dir | 指定redo文件的路径,如果不指定,则默认为datadir目录 |
介绍到这里,读者朋友可以发现,我们刚才探索的是如何让已经提交的事务保持持久化,但是如果某些事务偏偏在执行到一半的时候出现问题怎么办?
事务的原子性要求事务中的所有操作要么都成功,要么都失败,不允许存在中间状态。就好比我在写这篇文章的时候,会时不时地敲一下ctrl+Z
返回到上一步或者过去好几步之前的状态,MySQL也需要“留一手”,把事务回滚时需要的东西都记录下来。
比如,插入数据的时候,至少应该把新增的这条记录的主键的值记录下来,这样回滚的时候只要把这个主键值对应的记录删除就可以了。
MySQL又一个鼎鼎大名的日志—— undo日志 ,正式登场!
全部0条评论
快来发表一下你的评论吧 !