InnoDB总体架构
https://dev.mysql.com/doc/refman/8.0/en/innodb-architecture.html
内存结构(In-Memory Structures)
Buffer Pool主要分为4个部分:Buffer Pool、Change Buffer、Adaptive Hash Index、(redo) Log Buffer。
Buffer Pool
Buffer Pool缓存的是页面信息,包括数据页、索引页。
Buffer Pool默认大小是128M,可以调整。
查看系统变量:
show variables like ‘%innodb_buffer_pool%‘;
查看服务器状态,里面有很多跟Buffer Pool相关的信息:
show status like ‘%innodb_buffer_pool%‘;
Buffer Pool满了怎么办?InnoDB用了LRU算法来管理缓冲池(链表实现,不是传统的LRU算法,分成了young区和old区),经过淘汰的数据就是热点数据。
LRU
传统LRU,可以用Map+链表实现。value存的是链表中的地址。
InnoDB中使用了一个双向链表--LRU list。但是这个LRU list放的不是data page,而是指向缓存页的指针。
如果写buffer pool的是会发现没有空闲页了,就要从buffer pool中淘汰数据页,这就要根据LRU链表的数据来操作。
另外,InnoDB的数据页并不是都在访问的时候才缓存到buffer pool的。
预读机制
InnoDB有一个预读机制(read ahead)。
https://dev.mysql.com/doc/refman/8.0/en/innodb-performance-read_ahead.html
这种预读的机制分为两种类型。
一种叫线性预读(异步的 Linear read-ahead)。为了便于管理,InnoDB中把64个相邻的page叫做一个extent(区)。如果顺序地访问了一个extent的56个page,这个时候InnoDB就会把下一个extent(区)缓存到buffer pool中。
顺序访问多少个page才缓存下一个extent,由一个参数控制:
show variables like ‘innodb_read_ahead_threshold‘;
第二种叫做随机预读(Random read-ahead),如果buffer pool已经缓存了同一个extent(区)的数据页的个数超过13时,就会把这个extent剩余的所有page全部缓存到buffer pool。
但是随机预读的功能默认是不开启的,由一个参数控制:
show variables like ‘innodb_random_read_ahead‘;
线性预读或者异步预读,能够把可能即将用到的数据提前加载到buffer pool,肯定能提升io的性能,是一种非常有用的机制。但也会导致占用的内存空间更多,剩余的空闲页更少。如果说buffer pool size不是很大,而预读的数据很多,很有可能那些真正的需要被缓存的热点数据被预读的数据挤出buffer pool而淘汰掉,这样下次访问的是会又要先去磁盘。
mysql是如何解决这个问题的?
把LRU List分成两部分,靠近head的叫做new sublist,用来放热数据(把它称为热区)。靠近tail的叫做old sublist,用来放冷数据(把它称作冷区)。中间的分割线叫做midpoint。也就是对buffer pool做一个冷热分离。
附官方innodb-buffer-pool内存结构图:
https://dev.mysql.com/doc/refman/8.0/en/innodb-buffer-pool.html
所有新数据加入到buffer pool的时候,一律先放到冷数据去的head,不管是预读的,还是普通的读操作。所以如果一些预读的数据没有被用到,会在old sublist(冷区)直接被淘汰。
放到LRU List冷区(old sublist)以后,如果再次被访问,都把它移动到热区的head。
如果热区的数据长时间没有被访问,会被移动到冷区的head部,最后慢慢在tail被淘汰。
在默认情况下,热区占了5/8的大小,冷区占了3/8,这个值有innodb_old_blocks_pct控制,它代表的是old区的大小,默认是37%也就是3/8。
innodb_old_blocks_pct的值可以调整,在5%到95%之间,这个值越大,new去越小,这个LRU就越接近传统LRU。
如果这个值太小,old区没有被访问的数据淘汰速度会更快。
另外InnoDB里面通过innodb_old_blocks_time这个参数来控制是否为有效访问,若是有效访问,才会把数据从冷区放到热区。这个参数默认为1秒,也就是说数据被放到冷区后1秒钟之内被访问的不会移动到热区(也就是仍在冷区),1秒钟之后被访问的才会把数据从冷区移动到热区的head。
那。。。为啥要搞这么个参数?
其实是为了尽可能的避免全表扫描或预读的数据污染真正的热数据。啥意思?就是如果没有这个参数的话,你某一次加载后被放入冷区的数据非常大,比如没用索引全表扫描了某个大表或者dump全表数据备份之类的并不会频繁使用的数据,然后立即访问了它,然后这些数据全被放入了热区的head,这就可能会导致很多原来的热点数据被移动到冷区甚至被淘汰,这就造成了缓冲池的污染。
另外为了避免并发的问题,对于LRU链表的操作是要加锁的。也就是说每一次链表的移动,都会带来资源的竞争和等待。从这个角度来说,如果要进一步提升InnoDB LRU的效率,就要尽量地减少LRU链表的移动。
所以对于new区还有一个特殊的优化:如果一个缓存也出于热区,且在热数据区域的钱1/4区域(注意:不是整个链表的1/4),那么当访问这个缓存页的时候,就不用把它移动到热数据的头部;如果缓存页处于热区的后3/4区域,那么当访问这个缓存页的时候,会把它移动到热区的头部。
Change Buffer 写缓冲
Change Buffer是Buffer Pool的一部分。
如果这个数据页不是唯一索引,不存在数据重复的情况,也就不需要从磁盘加载索引页判断数据是不是重复(唯一性检查)。这种情况下可以先把修改记录在内存的缓冲池中,从而提升更新语句(insert、delete、update)的执行速度。
这块区域就是Change Buffer(mysql 5.5之后才有)。
最后把Change Buffer记录到数据页的操作叫做merge。merge操作会在访问这个数据页或者通过后台线程、数据库shut down、redolog写满时等情况下触发。
如果数据库大部分索引都是非唯一性索引,并且业务是写多读少,不会在写数据后立刻读取,就可以使用Change Buffer。
可以通过调大这个值,来扩大Change Buffer的大小,以支持写多读少的业务场景。
show variables like ‘innodb_change_buffer_max_size‘;
代表Change Buffer占Buffer Pool的比例,默认25%。
Adaptive Hash Index
留个遗留问题,按理说索引是应该放在磁盘的。
(redo)Log Buffer
redo log也不是每一次都直接写入磁盘,在buffer pool里面有一块内存区域(log buffer)专门用来保存即将要写入日志文件的数据,默认16M,他一样可以节省磁盘IO。
show variables like ‘innodb_log_buffer_size‘;
redo log的内容主要用于崩溃恢复。磁盘的数据文件,数据来自buffer pool。redo log写入磁盘,不是写入数据文件。
在我们写入数据到磁盘的时候,操作系统本身也是有缓存的,flush就是把操作系统缓冲区写到磁盘。
log buffer写入磁盘的时机,由一个实例控制,默认是1.
show variables like ‘innodb_flush_log_at_trx_commit‘;
值 | 含义 |
---|---|
0(延迟写) | log buffer将每秒一次的写入log file中,并且log file的flush操作同时进行。该模式下在事务提交的时候,不会主动触发写入磁盘的操作。 |
1(默认,实时写,实时刷) | 每次事务提交时,mysql都会把log buffer的数据写入log file,并且刷到磁盘中去。 |
2(实时写,延迟刷) | 每次事务提交时,mysql都会把log buffer的数据写入log file,但是flush操作不会同时进行。该模式下,mysql会每秒执行一次flush操作。 |
刷盘越快,越安全,但是也会约消耗性能。
磁盘结构
表空间可以看做是InnoDB逻辑结构的最高层,所有的数据都存放在表空间中。
系统表空间(system tablespace)
默认情况下InnoDB有一个共享表空间(对应文件/var/lib/mysql/ibdata1),也叫系统表空间。
innoDB系统表空间包含 innODB数据字典和双写缓冲区,Change Buffer 和 UndoLogs),如果没有指定file- per-table,也包含用户创建的表和索引数据。
1、数据字典∶由内部系统表组成,存储表和索引的元数据(定义信息)
2、双写缓冲(innodb的一大特性)
innodb的页和操作系统的页大小不一致, nnoDB页大小一般为16K,操作系统页大小为4K, InnoDB的页写入到磁盘时,一个页需要分4次写。
如果只写了4k就宕机了,这种情况叫部分写失效(partial page write),可能会导致数据丢失。
show variables like ‘innodb_doublewrite‘;
这个时候数据页就被破坏了,也没法使用redo log去做崩溃恢复,需要先把这个页还原之后才可以用redo log进行恢复。double write就用作恢复这个被破坏的页(页的副本),InnoDB通过它实现了数据页的可靠性。
double write由两部分组成,一部分是内存的double write,一部分是磁盘上的double write。double write是顺序写入的,不会带来很大的开销。
默认情况下,所有的表共享一个系统表空间,这个文件(ibdata1)会越来越大,而且他的空间不会收缩。
独占表空间(file-per-table tablespaces)
我们可以让每张表独占一个表空间。这个开关通过 innodb_file_per_table 设置,默认开启。
show variables like ‘innodb_file_per_table‘;
开启后,则每张表会开辟一个表空间,这个文件就是数据目录下的ibd文件(例如var/ib/mysq/ user_innodb.ibd),存放表的索引和数据。
但是其他类的数据,如回滚(undo)信息,插入缓冲索引页、系统事务信息,二次写缓冲( Double write buffer)等还是存放在原来的共享表空间内。
通用表空间(general tablespaces)
通用表空间也是一种共享表空间,跟ibdata1类似。
可以创建一个通用的表空间,用来存储不同的数据库的表,数据路径和文件可以自定义。
创建表空间:
create tablespace xxxts add data file ‘/var/lib/mysql/xxxts.ibd‘ file_block_size=16k engine=innodb;
指定表空间:
create table xxxtb(id integer) tablespace xxxts;
删除表空间
drop table xxxtb; --删除表空间需要先删除里面的所有表
drop tablespace xxxts; --删除表空间
临时表空间(temporary tablespaces)
存储临时表的数据,包括用户创建的临时表和磁盘的内部临时表。
数据库服务正常关闭是,该表空间被删除,下次重新产生。
redo log
就是上面说的那个用来崩溃恢复的重做日志。
undo log tablespace
undo Log的数据默认在系统表空间ibData1文件中,因为共享表空间不会自动收缩,也可以单独创建一个undo表空间。
后台线程
后台线程的主要作用是负责刷新内存池中的数据和把修改的数据页刷新到磁盘。后台线程分为: master thread, lO thread, purge thread, page cleaner thread
master thread负责刷新缓存数据到磁盘并协调调度其它后台进程。
IO thread分为 insert buffer、log、read、wite进程。分别用来处理 insert buffer、重做日志、读写请求的IO回调。
purge thread用来回收undo页。
page cleaner thread用来刷新脏页。
除了 nnoDB架构中的日志文件, MySQL的 Server层也有一个日志文件,叫做binlog,它可以被所有的存储引擎使用。
Binlog
binlog以事件的形式记录了所有的DDL和DML语句(因为它记录的是操作而不是数据值,属于逻辑日志),可以用来做主从复制和数据恢复。
跟 redo log不一样,它的文件内容是可以追加的,没有固定大小限制。
在开启了 binlog功能的情况下,可以把 binlog导出成SQL语句,来实现数据的恢复。
binlog的另一个功能就是用来实现主从复制,它的原理就是从服务器读取主服务器的 binlog,然后执行一遍。
有了这两个日志之后,我们来看一下一条更新语句是怎么执行的(redo不能一次写入了)。
至此,有了redo log和binlog后,一条更新语句的执行顺序再看一下。