InnoDB基础
copy-MySQL技术内幕:InnoDB存储引擎(第2版).pdf (opens new window)
# InnoDB是否是第一个支持事务的数据库引擎?
BDB才是第一个支持事务的MySQL存储引擎,现在已经完全停止开发。
# InnoDB的体系架构
# 后台线程
InnoDB是单进程多线程的模型,后台有很多的后台线程,负责处理不同任务。
Master thread主要任务有将缓冲池中的数据异步刷新到磁盘,保证数据的一致性,包括脏页的刷新,合并插入缓冲。
# 内存
# 缓冲池
InnoDB是基于磁盘存储的,并将其数据按照页的方式进行管理。
缓冲池是一块内存区域,通过内存的速度来弥补磁盘速度较慢对数据库性能的影响。
对数据库中的页做修改时,先修改在缓冲池中的页,然后再以一定的频率刷新到磁盘上,使用一张checkpoint技术刷新。
使用LRU算法进行管理。
# 重做日志缓冲
InnoDB存储引擎首先将重做日志信息先放入到缓冲区,然后按照一定的频率刷新到重做日志文件中。
一般一秒钟会将日志缓冲刷新到重做日志。
# checkpoint技术
前面已经讲到了,缓冲池的设计目的为了协调CPU速度与磁盘速度的鸿沟。因此页的操作首先都是在缓冲池中完成的。如果一条DML语句,如Update或Delete改变了页中的记录,那么此时页是脏的,即缓冲池中的页的版本要比磁盘的新。数据库需要将新版本的页从缓冲池刷新到磁盘。
倘若每次一个页发生变化,就将新页的版本刷新到磁盘,那么这个开销是非常大的。若热点数据集中在某几个页中,那么数据库的性能将变得非常差。同时,如果在从缓冲池将页的新版本刷新到磁盘时发生了宕机,那么数据就不能恢复了。为了避免发生数据丢失的问题,当前事务数据库系统普遍都采用了Write Ahead Log策略,即当事务提交时,先写重做日志,再修改页。当由于发生宕机而导致数据丢失时,通过重做日志来完成数据的恢复。这也是事务ACID中D(Durability持久性)的要求。
# InnoDB关键特性
# 自适应哈希索引
InnoDB存储引擎会监控对表上各索引页的查询。如果观察到建立哈希索引可以带来速度提升,则建立哈希索引,称之为自适应哈希索引(Adaptive Hash Index,AHI)。
AHI是通过缓冲池的B+树页构造而来,因此建立的速度很快,而且不需要对整张表构建哈希索引。
InnoDB存储引擎会自动根据访问的频率和模式来自动地为某些热点页建立哈希索引。 自适应哈希索引也放在缓冲池中。
# 异步IO(Asyn IO)
为了提高磁盘操作性能,当前的数据库系统都采用异步IO(Asynchronous IO,AIO)的方式来处理磁盘操作。InnoDB存储引擎亦是如此。
与AIO对应的是Sync IO,即每进行一次IO操作,需要等待此次操作结束才能继续接下来的操作。但是如果用户发出的是一条索引扫描的查询,那么这条SQL查询语句可能需要扫描多个索引页,也就是需要进行多次的IO操作。在每扫描一个页并等待其完成后再进行下一次的扫描,这是没有必要的。用户可以在发出一个IO请求后立即再发出另一个IO请求,当全部IO请求发送完毕后,等待所有IO操作的完成,这就是AIO。
AIO的另一个优势是可以进行IO Merge操作,也就是将多个IO合并为1个IO,这样可以提高IOPS的性能
# 日志文件
# 错误日志
查看错误日志的位置:mysql> show variables like 'log_error';
当出现MySQL数据库不能正常启动时,第一个必须查找的文件应该就是错误日志文件,该文件记录了错误信息,能很好地指导用户发现问题。
# 慢查询日志(show log)
慢查询日志(slow log)可帮助DBA定位可能存在问题的SQL语句,从而进行SQL语句层面的优化。
可以在MySQL启动时设一个阈值,将运行时间超过该值的所有SQL语句都记录到慢查询日志文件中。DBA每天或每过一段时间对其进行检查,确认是否有SQL语句需要进行优化。该阈值可以通过参数long_query_time来设置,默认值为10,代表10秒。mysql> show variables like 'long_query_time';
# 二进制日志(binlog)
二进制日志(binary log)记录了对MySQL数据库执行更改的所有操作,但是不包括SELECT和SHOW这类操作,因为这类操作对数据本身并没有修改。然而,若操作本身并没有导致数据库发生变化,那么该操作可能也会写入二进制日志。
使用命令查看二进制日志中记录的内容:mysql> show binlog events in 'binlog.000085';
二进制日志的作用:
恢复:某些数据的恢复需要二进制日志,例如,在一个数据库全备文件恢复后,用户可以通过二进制日志进行point-intime的恢复。
复制:用于主从复制,实现实时同步
审计:用户可以通过二进制日志中的信息来进行审计,判断是否有对数据库进行注入的攻击。
# InnoDB存储文件
除了表结果定义文件、日志文件、参数文件这些MySQL数据库本身的文件外,每个表存储引擎还有其自己独有的文件。
# 重做日志文件
为什么强调是重做日志文件呢?因为重做日志文件对于InnoDB存储引擎至关重要,它们记录了对于InnoDB存储引擎的事务日志。
当实例或介质失败(media failure)时,重做日志文件就能派上用场。例如,数据库由于所在主机掉电导致实例失败,InnoDB存储引擎会使用重做日志恢复到掉电前的时刻,以此来保证数据的完整性。
每个InnoDB存储引擎至少有1个重做日志文件组(group),每个文件组下至少有2个重做日志文件,如默认的ib_logfile0和ib_logfile1。
**重做日志文件的大小设置对于InnoDB存储引擎的性能有着非常大的影响。**一方面重做日志文件不能设置得太大,如果设置得很大,在恢复时可能需要很长的时间;另一方面又不能设置得太小了,否则可能导致一个事务的日志需要多次切换重做日志文件。此外,重做日志文件太小会导致频繁地发生async checkpoint,导致性能的抖动。
# redo(重做)日志与binlog(二进制文件)有什么区别?
性质 | redo Log | bin Log |
---|---|---|
文件大小 | redo log 的大小是固定的(配置中也可以设置,一般默认的就足够了) | bin log 可通过配置参数max_bin log_size设置每个bin log文件的大小(但是一般不建议修改)。 |
实现方式 | redo log是InnoDB引擎层实现的(也就是说是 Innodb 存储引起过独有的) | bin log是 MySQL 层实现的,所有引擎都可以使用 bin log日志 |
记录方式 | redo log 采用循环写的方式记录,当写到结尾时,会回到开头循环写日志。 | bin log 通过追加的方式记录,当文件大小大于给定值后,后续的日志会记录到新的文件上 |
使用场景 | redo log适用于崩溃恢复(crash-safe)(这一点其实非常类似与 Redis 的持久化特征) | bin log 适用于主从复制和数据恢复 |
首先,二进制日志会记录所有与MySQL数据库有关的日志记录,包括InnoDB、MyISAM、Heap等其他存储引擎的日志。而InnoDB存储引擎的重做日志只记录有关该存储引擎本身的事务日志。
其次,记录的内容不同,无论用户将二进制日志文件记录的格式设为STATEMENT还是ROW,又或者是MIXED,其记录的都是关于一个事务的具体操作内容,即该日志是逻辑日志。而InnoDB存储引擎的重做日志文件记录的是关于每个页(Page)的更改的物理情况。
此外,写入的时间也不同,二进制日志文件仅在事务提交前进行提交,即只写磁盘一次,不论这时该事务多大。而在事务进行的过程中,却不断有重做日志条目(redo entry)被写入到重做日志文件中。
重做日志条目结构
写入重做日志文件的操作不是直接写,而是先写入一个重做日志缓冲(redo log buffer)中,然后按照一定的条件顺序地写入日志文件。
**为了保证事务的ACID中的持久性,必须将innodb_flush_log_at_trx_commit设置为1,也就是每当有事务提交时,就必须确保事务都已经写入重做日志文件。**那么当数据库因为意外发生宕机时,可以通过重做日志文件恢复,并保证可以恢复已经提交的事务。而将重做日志文件设置为0或2,都有可能发生恢复时部分事务的丢失。不同之处在于,设置为2时,当MySQL数据库发生宕机而操作系统及服务器并没有发生宕机时,由于此时未写入磁盘的事务日志保存在文件系统缓存中,当恢复时同样能保证数据不丢失。
# 表空间文件
InnoDB采用将存储的数据按表空间(tablespace)进行存放的设计。在默认配置下会有一个初始大小为10MB,名为ibdata1的文件。该文件就是默认的表空间文件(tablespace file)。
设置innodb_data_file_path参数后,所有基于InnoDB存储引擎的表的数据都会记录到该共享表空间中。若设置了参数innodb_file_per_table,则用户可以将每个基于InnoDB存储引擎的表产生一个独立表空间。独立表空间的命名规则为:表名.ibd。
# InnoDB逻辑存储结构
# 数据页
在 File Header 中有两个指针,分别指向上一个数据页和下一个数据页,连接起来的页相当于一个双向的链表,如下图所示:
# User Records
数据页中的记录按照「主键」顺序组成单向链表,单向链表的特点就是插入、删除非常方便,但是检索效率不高,最差的情况下需要遍历链表上的所有节点才能完成检索。
因此,数据页中有一个页目录,起到记录的索引作用,就像我们书那样,针对书中内容的每个章节设立了一个目录,想看某个章节的时候,可以查看目录,快速找到对应的章节的页数,而数据页中的页目录就是为了能快速找到记录。
从图可以看到,页目录就是由多个槽组成的,槽相当于分组记录的索引。然后,因为记录是按照「主键值」从小到大排序的,所以我们通过槽查找记录时,可以使用二分法快速定位要查询的记录在哪个槽(哪个记录分组),定位到槽后,再遍历槽内的所有记录,找到对应的记录,无需从最小记录开始遍历整个页中的记录链表。
# InnoDB行溢出数据
**InnoDB存储引擎可以将一条记录中的某些数据存储在真正的数据页面之外。**一般认为BLOB、LOB这类的大对象列类型的存储会把数据存放在数据页面之外。但是,这个理解有点偏差,BLOB可以不将数据放在溢出页面,而且即便是VARCHAR列数据类型,依然有可能被存放为行溢出数据。
很多DBA喜欢MySQL数据库提供的VARCHAR类型,因为相对于Oracle VARCHAR2最大存放4000字节,SQL Server最大存放8000字节,MySQL数据库的VARCHAR类型可以存放65535字节。但是,这是真的吗?真的可以存放65535字节吗?
InnoDB存储引擎并不支持65535长度的VARCHAR。这是因为还有别的开销,通过实际测试发现能存放VARCHAR类型的最大长度为65532。
上述创建的VARCHAR长度为65 532的表,其字符类型是latin1的,如果换成GBK又或UTF-8的,这次即使创建列的VARCHAR长度为65532,也会提示报错。用户也应该理解VARCHAR(N)中的N指的是字符的长度。而文档中说明VARCHAR类型最大支持65535,单位是字节。
此外需要注意的是,MySQL官方手册中定义的65535长度是指所有VARCHAR列的长度总和,如果列的长度总和超出这个长度,依然无法创建。
InnoDB存储引擎的页为16KB,即16384字节,怎么能存放65532字节呢?
在一般情况下,InnoDB存储引擎的数据都是存放在页类型为B-tree node中。但是当发生行溢出时,数据存放在页类型为Uncompress BLOB页中。
表空间中有一个数据页节点B-tree Node,另外有4个未压缩的二进制大对象页Uncompressed BLOB Page,在这些页中才真正存放了65532字节的数据。
既然实际存放的数据都在BLOB页中,那数据页中又存放了些什么内容呢?
那多长的VARCHAR是保存在单个数据页中的,从多长开始又会保存在BLOB页呢?
InnoDB存储引擎表是索引组织的,即B+Tree的结构,这样每个页中至少应该有两条行记录(否则失去了B+Tree的意义,变成链表了)。因此,如果页中只能存放下一条记录,那么InnoDB存储引擎会自动将行数据存放到溢出页中。
如果可以在一个页中至少放入两行数据,那VARCHAR类型的行数据就不会存放到BLOB页中去。经过多次试验测试,发现这个阈值的长度为8098。如用户建立一个列为varchar(8098)的表,然后插入2条记录。
对于TEXT或BLOB的数据类型,用户总是以为它们是存放在Uncompressed BLOB Page中的,其实这也是不准确的。是放在数据页中还是BLOB页中,和前面讨论的VARCHAR一样,至少保证一个页能存放两条记录。