我们平时创建的那些表都是有对应的表空间的,每个表空间就是对应了磁盘上的数据文件,在表空间里有很多组数据区,一组数据区是256个数据区,每个数据区包含了64个数据页,是1mb
表空间的第一组数据区的第一个数据区的头三个数据页,都是存放特殊信息的;表空间的其他组数据区的第一个数据区的头两个数据页,也都是存放特殊信息的
表空间(tablespace)有一个32位的spaceid,用户表空间物理上是由page连续构成的,每个page的序号是一个32位的uint,page 0位于文件物理偏移量0处,page 1位于16384偏移量处。由此推出InnoDB单表最大
2^32 * 16k = 64T
。表的所有行数据都存在页类型为INDEX的索引页(page)上,为了管理表空间,还需要很多其他的辅助页,例如文件管理页FSP_HDR/XDES、插入缓冲IBUF_BITMAP页、INODE页等
Tablespace可以分为5大类,分别是:
System Tablespace
,File-Per-Table Tablespaces
,General Tablespaces
,Undo Tablespaces
, Temporary Tablespace
。表空间、数据页 和表,行的区别
表、列和行,都是逻辑概念,,表空间、数据页,是物理上的概念,在物理层面,表里的数据都放在一个表空间中,表空间是由一堆磁盘上的数据文件组成的,这些数据文件里都存放了表里的数据,这些数据是由一个一个的数据页组织起来的,这些都是物理层面的概念。
System Tablespace
system表空间是InnoDB数据字典、
double write buffer
、change buffer
和undo log
的存储区域。如果表是在系统表空间中创建的,而不是在文件表或普通表空间中创建的,那么它还可能包含表和索引数据。系统表空间可以有一个或多个数据文件。默认情况下,在data目录中创建一个名为
ibdata1
的系统表空间数据文件由于系统表空间可以存储多张表,因此,其为一个共享表空间。系统表空间由一个或多个数据文件组成,默认情况下,其包含一个叫
ibdata1
的系统数据文件,位于mysql 数据目录(datadir)
下。系统表空间数据文件的位置、大小和数目由innodb_data_home_dir
和innodb_data_file_path
启动选项控制。innodb_file_per_table
如果你想让每一个数据库表都有一个单独的表空间文件的话,可以通过参数
innodb_file_per_table
设置。这个参数只有在MySQL5.6或者是更高的版本中才可以使用。
可以通过配置文件
[mysqld] innodb_file_per_table=ON
也可以通过命令
mysql> SET GLOBAL innodb_file_per_table=ON;
一个表对应一个独立的文件,文件以
db_name/table_name.ibd
命名。行存储在这类文件。另外还有5.7之后引入General Tablespace
,可以将多个表放到同一个文件里面。General Tablespaces
通用表空间
(General Tablespaces)
通用表空间为通过create tablespace
语法创建的共享表空间。通用表空间可以创建于mysql数据目录外的其他表空间,其可以容纳多张表,且其支持所有的行格式。 通过create table tab_name ... tablespace [=] tablespace_name
或alter table tab_name tablespace [=] tablespace_name
语法将其添加与通用表空间内。undo tablespace
undo表空间
(undo tablespace)
undo表空间由一个或多个包含undo日志的文件组成。innodb_undo_tablespace
配置选项控制undo表空间的数目。undo表空间创建于innodb_undo_directory
配置选项确定的位置,该选项典型被用于将undo日志放于不同的存储设备上。如果该选项没有确定任何路径,undo表空间则被默认创建于mysql数据目录(datadir)下。temporary Tablespace
用户创建的临时表和磁盘内部临时表创建于共享临时表空间中。
innodb_temp_data_file
选项确定临时表空间数据文件的相对路径、名字、大小和属性等。如果该选项未确定任何值,默认情况下,系统将在 innodb_data_home_dir
确定的目录下创建一个叫ibtmp1
的自动扩展的数据文件,该文件将稍大于12m。 mysql服务器正常关闭或异常终止初始化时,临时表空间将被移除,并且,mysql服务器每次启动时会被重新创建。当临时表空间被创建时,其被赋予一个动态产生的空间ID(space ID)
。如果不能创建临时表空间, mysql服务器启动将被拒绝。mysql服务器异常终止的情况下,临时表空间将不被移除。表空间空洞问题
表数据既可以存在共享表空间里,也可以是单独的文件。这个行为是由参数
innodb_file_per_table
控制的:- 这个参数设置为OFF表示的是,表的数据放在系统共享表空间,也就是跟数据字典放在一起;
- 这个参数设置为ON表示的是,每个InnoDB表数据存储在一个以
.ibd
为后缀的文件中。
从MySQL 5.6.6版本开始,它的默认值就是ON了。
我建议你不论使用MySQL的哪个版本,都将这个值设置为ON。因为,一个表单独存储为一个文件更容易管理,而且在你不需要这个表的时候,通过drop table命令,系统就会直接删除这个文件。而如果是放在共享表空间中,即使表删掉了,空间也是不会回收的。
所以,将
innodb_file_per_table
设置为ON,是推荐做法。delete删除数据导致空洞
我们在删除整个表的时候,可以使用
drop table
命令回收表空间。但是,我们遇到的更多的删除数据的场景是删除某些行,这时就遇到了问题:表中的数据被删除了,但是表空间却没有被回收。案例1
如下所示的索引结构。
假设,要删掉R4这个记录,InnoDB引擎只会把R4这个记录标记为删除。如果之后要再插入一个ID在300和600之间的记录时,可能会复用这个位置。但是,磁盘文件的大小并不会缩小。
InnoDB的数据是按页存储的,如果删掉了一个数据页上的所有记录,整个数据页就可以被复用了。
但是,数据页的复用跟记录的复用是不同的。
记录的复用,只限于符合范围条件的数据。比如上面的这个例子,R4这条记录被删除后,如果插入一个ID是400的行,可以直接复用这个空间。但如果插入的是一个ID是800的行,就不能复用这个位置了。
而当整个页从B+树里面摘掉以后,可以复用到任何位置。如果将数据页page A上的所有记录删除以后,page A会被标记为可复用。这时候如果要插入一条ID=50的记录需要使用新页的时候,page A是可以被复用的。
如果相邻的两个数据页利用率都很小,系统就会把这两个页上的数据合到其中一个页上,另外一个数据页就被标记为可复用。
进一步地,如果我们用delete命令把整个表的数据删除呢?结果就是,所有的数据页都会被标记为可复用。但是磁盘上,文件不会变小。
delete命令其实只是把记录的位置,或者数据页标记为了“可复用”,但磁盘文件的大小是不会变的。
也就是说,通过delete命令是不能回收表空间的。这些可以复用,而没有被使用的空间,看起来就像是“空洞”。
插入数据页分裂导致的空洞
实际上,不止是删除数据会造成空洞,插入数据也会。
如果数据是按照索引递增顺序插入的,那么索引是紧凑的。但如果数据是随机插入的,就可能造成索引的数据页分裂。
假设
page A
已经满了,这时我要再插入一行数据,会怎样呢?可以看到,由于page A满了,再插入一个ID是550的数据时,就不得不再申请一个新的页面page B来保存数据了。页分裂完成后,page A的末尾就留下了空洞(注意:实际上,可能不止1个记录的位置是空洞)。
另外,更新索引上的值,可以理解为删除一个旧的值,再插入一个新值。不难理解,这也是会造成空洞的。
也就是说,经过大量增删改的表,都是可能是存在空洞的。所以,如果能够把这些空洞去掉,就能达到收缩表空间的目的。
而重建表,就可以达到这样的目的。
重建表
试想一下,如果你现在有一个表A,需要做空间收缩,为了把表中存在的空洞去掉,你可以怎么做呢?
你可以新建一个与表A结构相同的表B,然后按照主键ID递增的顺序,把数据一行一行地从表A里读出来再插入到表B中。
由于表B是新建的表,所以表A主键索引上的空洞,在表B中就都不存在了。显然地,表B的主键索引更紧凑,数据页的利用率也更高。如果我们把表B作为临时表,数据从表A导入表B的操作完成后,用表B替换A,从效果上看,就起到了收缩表A空间的作用。
这里,你可以使用
alter table A engine=InnoDB
命令来重建表。在MySQL 5.5版本之前,这个命令的执行流程跟我们前面描述的差不多,区别只是这个临时表B不需要你自己创建,MySQL会自动完成转存数据、交换表名、删除旧表的操作。显然,花时间最多的步骤是往临时表插入数据的过程,如果在这个过程中,有新的数据要写入到表A的话,就会造成数据丢失。因此,在整个DDL过程中,表A中不能有更新。也就是说,这个DDL不是Online的。
Online DDL
而在MySQL 5.6版本开始引入的
Online DDL
,对这个操作流程做了优化。我给你简单描述一下引入了
Online DDL
之后,重建表的流程:- 建立一个临时文件,扫描表A主键的所有数据页;
- 用数据页中表A的记录生成B+树,存储到临时文件中;
- 生成临时文件的过程中,将所有对A的操作记录在一个日志文件
(row log)
中,对应的是图中state2的状态;
- 临时文件生成后,将日志文件中的操作应用到临时文件,得到一个逻辑数据上与表A相同的数据文件,对应的就是图中state3的状态;
- 用临时文件替换表A的数据文件。
可以看到,由于日志文件记录和重放操作这个功能的存在,这个方案在重建表的过程中,允许对表A做增删改操作。这也就是
Online DDL
名字的来源。确实,上图的流程中,
alter
语句在启动的时候需要获取MDL写锁,但是这个写锁在真正拷贝数据之前就退化成读锁了。为什么要退化呢?为了实现Online,MDL读锁不会阻塞增删改操作。
那为什么不干脆直接解锁呢?为了保护自己,禁止其他线程对这个表同时做DDL。
而对于一个大表来说,
Online DDL
最耗时的过程就是拷贝数据到临时表的过程,这个步骤的执行期间可以接受增删改操作。所以,相对于整个DDL过程来说,锁的时间非常短。对业务来说,就可以认为是Online的。需要补充说明的是,上述的这些重建方法都会扫描原表数据和构建临时文件。对于很大的表来说,这个操作是很消耗IO和CPU资源的。因此,如果是线上服务,你要很小心地控制操作时间。如果想要比较安全的操作的话,我推荐你使用GitHub开源的
gh-ost
来做。Online 和 inplace
说到Online,我还要再和你澄清一下它和另一个跟DDL有关的、容易混淆的概念
inplace
的区别。你可能注意到了,改锁表DDL的图中,我们把表A中的数据导出来的存放位置叫作
tmp_table
。这是一个临时表,是在server层创建的。在
Online DDL
中,根据表A重建出来的数据是放在“tmp_file
”里的,这个临时文件是InnoDB在内部创建出来的。整个DDL过程都在InnoDB内部完成。对于server层来说,没有把数据挪动到临时表,是一个“原地”操作,这就是“inplace”名称的来源。所以,我现在问你,如果你有一个1TB的表,现在磁盘间是1.2TB,能不能做一个inplace的DDL呢?
答案是不能。因为,
tmp_file
也是要占用临时空间的。我们重建表的这个语句
alter table t engine=InnoDB
,其实隐含的意思是:alter table t engine=innodb,ALGORITHM=inplace;
跟
inplace
对应的就是拷贝表的方式了,用法是:alter table t engine=innodb,ALGORITHM=copy;
当你使用
ALGORITHM=copy
的时候,表示的是强制拷贝表,对应的流程就是改锁表DDL的图的操作过程。但我这样说你可能会觉得,inplace跟Online是不是就是一个意思?
其实不是的,只是在重建表这个逻辑中刚好是这样而已。
比如,如果我要给InnoDB表的一个字段加全文索引,写法是:
alter table t add FULLTEXT(field_name);
这个过程是inplace的,但会阻塞增删改操作,是非Online的。
如果说这两个逻辑之间的关系是什么的话,可以概括为:
- DDL过程如果是Online的,就一定是inplace的;
- 反过来未必,也就是说inplace的DDL,有可能不是Online的。截止到MySQL 8.0,添加全文索引
(FULLTEXT index)
和空间索引(SPATIAL index)
就属于这种情况。
optimize table
、analyze table
和alter table
这三种方式重建表的区别- 从MySQL 5.6版本开始,
alter table t engine = InnoDB
(也就是recreate)默认的就是上面图4的流程了;
analyze table t
其实不是重建表,只是对表的索引信息做重新统计,没有修改数据,这个过程中加了MDL读锁
optimize table t
等于recreate+analyze
案例
假设现在有人碰到了一个“想要收缩表空间,结果适得其反”的情况,看上去是这样的:
- 一个表t文件大小为1TB;
- 对这个表执行
alter table t engine=InnoDB;
- 发现执行完成后,空间不仅没变小,还稍微大了一点儿,比如变成了1.01TB。
你觉得可能是什么原因呢
就是这个表,本身就已经没有空洞的了,比如说刚刚做过一次重建表操作。在DDL期间,如果刚好有外部的DML在执行,这期间可能会引入一些新的空洞。
在重建表的时候,InnoDB不会把整张表占满,每个页留了1/16给后续的更新用。也就是说,其实重建表之后不是“最”紧凑的。
假如是这么一个过程:
- 将表t重建一次;
- 插入一部分数据,但是插入的这些数据,用掉了一部分的预留空间;
- 这种情况下,再重建一次表t,就可能会出现问题中的现象。