架构分层
可以把MySQL分为3层,和客户端对接的是对接层,真正执行操作的服务层,和硬件打交道的存储层。
连接层
可短要连接到mysql服务的端口,必须跟服务端建立连接,管理所有连接,验证客户端身份和权限这些功能就在连接层完成。
服务层
连接层把SQL语句交给服务层,这里面包含一系列流程:
查询缓存的判断,根据SQL调用相应的接口,对SQL语句进行词法和语法的解析,优化器进行优化,最后交给执行器执行
存储引擎
存储引擎是数据真正存放的地方,MySQL里支持不同的存储引擎。
再往下则是内存或磁盘。
更新SQL的执行
更新流程和查询流程的基本流程是一致的。也需要警告解析器、优化器的处理,然后交给执行器去执行。
区别在拿到符合条件的数据之后的操作
缓冲池 Buffer Pool
对于InnoDB来说,数据都是存放在磁盘上的,存储引擎要操作数据,必须先把磁盘上的数据加载到内存中。
操作系统和存储引擎,都有一个预读取的概念。也就是,当磁盘上的一块数据被读取的时候,很可能它附近的位置也会被马上读取到,这叫做局部性原理。所以每次读取的时候会多读取一些,而不是用多少读多少。
InnoDB设定了一个存储引擎从磁盘读取数据到内存的最小单位,叫做页。操作系统也有页的概念,一般为4K,InnoDB中,这个最小单位的默认值为16k。如果需要修改这个值的大小,需要清空数据重新初始化服务。
操作数据的时候,如果每次都需要从磁盘读到内存中,再返回给Server,那性能就有点低,所以InnoDB设计了一个内存的缓冲区。读取数据的时候,先判断是不是在这个内存区域里面,如果是就直接读取,然后操作,不用再次从磁盘加载。如果不是,读取后就写到这个内存的缓冲区。
这个内存区域有一个专属的名字,叫做Buffer Pool。
修改数据的时候,先写到Buffer Pool中,而不是直接写入到磁盘。内存的数据页和磁盘的数据不一致时,叫做脏页。
InnoDB里面有专门的后台线程把Buffer Pool的数据写入到磁盘,每隔一段时间就一次性地把多个修改写入磁盘,这个动作叫做刷脏。
总结:Buffer Pool的作用是为了提高读写效率。
Redo log
刷脏操作不是实时的,如果Buffer Pool里面的脏页还没有刷如磁盘时,数据库宕机或者重启,这些数据就会丢失。
为了避免这种个清空,InnoDB把所有对页的修改操作专门写入一个日志文件。
如果有未同步到磁盘的数据没数据库在启动的时候,就会从这个日志文件进行恢复操作(实现carsh-safe)。事务的ACID中的D(持久性)就是用它来实现的。
记录redo log时是顺序I/O,而刷盘操作是随机I/O,顺序I/O的效率更高,所以,先把修改写入日志文件,保证了内存数据的安全性的情况下,可以延迟刷盘时机,进而提升系统吞吐。
redo log位于/var/lib/mysql目录下的ib_logfile0和ib_logfile1,默认为两个文件,每个48M。
show variables like 'innodb_log%';
参数 | 含义 |
---|---|
innodb_log_file_size | 指定每个文件的打下,默认48 |
innodb_log_files_in_group | 指定文件的数量,默认为2 |
innodb_log_group_home_dir | 指定文件所在的路径,相对或绝对,如果不指定,则为datadir路径 |
redo log的特点
- redo log 是InnoDB存储引擎实现的,并不是所有存储引擎都有。支持崩溃恢复是InnoDB的一个特性
- redo log不是记录数据页更新之后的状态,而是记录在某个数据页上做了什么修改。属于物理日志
- redo log的大小是固定的,前面的内容会被覆盖,一旦写满,就会触发Buffer Pool的磁盘同步,以便腾出空间记录后面的修改。
除了redo log之外,还有一个和修改有关的日志,叫做undo log。redo log和undo log与事务密切相关,统称为事务日志。
Undo log
undo log(撤销日志/回滚日志)记录了事务发生之前的数据状态,分为insert undo log和update undo log。如果修改数据时出现异常,可以用undo log来实现回滚操作(保证原子性)。
可以理解为undo log记录的是反向的操作,比如insert会记录delete,update会记录update原来的值,和redo log记录在哪个物理页做了什么操作不同,所以叫做逻辑格式的日志。
show global variables like '%undo%';
参数 | 含义 |
---|---|
innodb_undo_directory | undo文件的路径 |
innodb_undo_log_truncate | 设置为1,即开启在线回收(收缩)undo log日志文件 |
Innodb_max_undo_log_size | innodb_undo_log_truncate设置Wie1,超过这个大小的时候会触发truncate回收动作,如果page大小是16kb,truncate后空间收缩到10m,默认为1073741824字节=1GB |
Innodb_undo_tablespaces | 设置undo独立表空间个数,范围为0-95,默认为0,0表示开启且undo日志存储在ibdata文件中。此参数已过时。 |
更新过程
设定name字段原值为young。
update user set name='kobe' where id = 1;
- 事务开始,从内存(buffer pool)或者磁盘(data file)取到包含这条数据的数据页,返回给Server的执行器。
- Server的执行器修改数据页的这一行数据的值为kobe
- 记录name=young到undo log
- 记录name=kobe到redo log
- 调用存储引擎接口,记录数据页到Buffer Pool(修改name=kobe)
- 事务提交
InnoDB总体架构
https://dev.mysql.com/doc/refman/5.7/en/innodb-architecture.html
内存结构
Buffer Pool主要分为三个部分:Buffer Pool、Change Buffer、Adaptive Hash Index,另外还有一个(redo)log buffer。
Buffer Pool
Buffer Pool 缓存的是页面信息,包括数据页、索引页
Buffer Pool默认大小是128M(134217728字节),可以调整
查看系统变量
show variables like '%innodb_buffer_pool%';
查看服务器状态中buffer pool
show status like '%innodb_buffer_pool%';
具体含义可以在官网的server-system-variables中查看
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)。也就是说认为访问某个page的数据的时候, 相邻的一些page可能会很快被访问到,所以先把这些page放到buffer pool中缓存起来
https://dev.mysql.com/doc/refman/5.7/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,肯定可以提升I/O的性能。
但是预读肯定也会带来一些副作用,就是导致占用的内存空间更多,剩余的空闲页更少。如果buffer pool size不是很大,而预读的数据很多,很可能那些真正需要被缓存的热点数据被预读的数据挤出buffer pool,淘汰掉,下次访问时又要去磁盘读取。
InnoDB将LRU list分成两部分,靠近head的叫做new sublist,用来放热数据(叫做热区)。靠近tail的叫做old sublist,用来放冷数据(叫做冷区)。中间的分割线叫做midpoint。这就是对buffer pool的冷热分离。
https://dev.mysql.com/doc/refman/5.7/en/innodb-buffer-pool.html
所有新数据加入到buffer pool的时候,一律先放到冷数据区的header,不管是预读还是普通的读操作。所以如果有一些预读的数据没有被用到,会在old sublist直接被淘汰。
放到LRU list之后,如果被再次访问,都把它西东到热区的head。
如果热区的数据长时间没有被范文,会先移动到冷区的head,然后慢慢在tail被淘汰。
默认情况下,热区占了5/8的大小,冷区占用了3/8,这个值由innodb_old_blocks_pct
控制,它代表的是old区的大小,默认为37%。
innodb_old_blocks_pct
的值可以调整,范围在5%-95%之间,值越大,new区越小。如果值过小,old没有被访问的数据淘汰会更快。
假如刚加载到冷区的数据立马就被访问了,而且这个立即被访问的数据量非常打,比如做了一个几千万数据打表的全表扫描或者dump全表备份数据,这种查询属于短时间内访问,后面再也不会用到了。如果这些数据全部移动到热区,会导致很多热点数据被移动到冷区甚至被淘汰,造成了缓冲池的污染。
所以对于记在到冷区然后被访问的数据,设置了一个时间窗口,只有超过这个时间之后被访问的数据,才认为他是有效访问.
InnoDB中通过innodb_old_blocks_time
这个参数来控制,默认是1秒钟。也就是说,1秒钟之内被访问的数据还是继续在冷区。只有超过1秒钟之后访问的数据才会移动到热区。
为了避免并发的问题,对于LRU链表的操作是要进行加锁操作的。也就是说,每次链表的移动,多会带来资源的竞争和等待。所以要提升InnoDB LRU的效率,就需要减少LRU链表的移动。
InnoDB对new区有一个特殊的优化:
如果一个缓存页处于热区,且在热区的前1/4区域,那么当访问这个缓存页的时候,就不会把他移动到热区的头部,如果缓存页处于热区的后3/4区域,那么当访问缓存页的时候,会将他移到到热区的头部。
Change buffer 写缓冲
Change buffer是Buffer Pool的一部分。
如果这个数据页不是唯一索引,不存在数据重复的情况,也就不需要从磁盘加载索引页判断数据是否重复(唯一性检查)。这种情况下,可以先把修改记录在内存的缓冲池中,从而提升更新语句(insert,delete,update)的执行速度。
这一块区域就是Change Buffer。5.5之前叫做Inser Buffer插入缓冲。
最后把Change Buffer记录到数据页的操作叫做merge。
发生merge的几种情况:
- 访问这个数据页时
- 通过后台线程
- 数据库shut down
- redo log写满
如果数据库大部分索引都是非唯一索引,并且业务是写多读少,不会在写数据后立刻读取,就可以使用Change Buffer。
可以通过调大对应的设置值来扩大Change Buffer的大小,以支持写多读少的业务场景。
show variables like 'innodb_change_buffer_max_size';
默认占比为25%。
Adaptive Hash Index
Hash索引
Redo Log Buffer
Redo log也不是每一次都直接写入磁盘的,在Buffer Pool里面有一块内存区域(Log Buffer)专门用来保存即将要写入日志文件的数据,默认16M,它一样可以节省磁盘IO。
show variables like 'innodb_log_buffer_size';
注意
redo log的内容主要是用于崩溃恢复,磁盘的数据文件,数据来自buffer pool。redo log写入磁盘,不是写入数据文件。
log buffer是什么时候写入log file?
在我们写入数据到磁盘的时候,操作系统本身也是有缓存的,flush就是把操作系统缓冲区希尔到磁盘。
log buffer写入磁盘的时间,由一个参数控制,默认是1
show variables like 'innodb_flush_log_at_trx_commit';
值 | 含义 |
---|---|
0(延迟写) | log buffer将每秒一次地写入logfile中,并且logfile的flush操作同时进行。该模式下,在事务提交的时候,不会主动触发写入磁盘的操作 |
1(默认,实时写,实时刷) | 每次事务提交时,MySQL都会把log buffer的数据写入log file,并且刷到磁盘中 |
2(实时写,延迟刷) | 每次事务提交到MySQL都会吧log buffer数据写入log file。犯事flush操作并不会同时进行。该模式下,MySQL会每秒执行一次flush操作。 |
刷盘越快,越安全,同时也会消耗性能。
磁盘结构
表空间可以看做是InnoDB存储引擎逻辑结构的最高层,所有的数据都存放在表空间中。InnoDB的表空间分为5类
系统表空间 system tablespace
默认情况请下,InnoDB存储引擎有一个共享表空间(对应文件/var/lib/mysql/ibdata1),也叫系统表空间。
InnoDB系统表空间包含InnoDB数据字典
和双写缓冲区
,Change Buffer
和 Undo Logs
,如果没有指定file-pre-table,也包含用户创建的表和索引数据。
undo 也可以设置独立的表空间
数据字典:由内部系统表组成,存储表和索引的元数据(定义信息)。
双写缓冲: InnoDB的页和操作系统的页大小不一致,InnoDB页大小一般为16k,操作系统页大小为4k,InnoDB的页写入磁盘时,一个页需要分4次写。
如果存储引擎正在写入页的数据到磁盘时发生宕机,可能出现页只写了一部分的情况,这种情况叫做部分写失效
(partial page write),可能导致数据丢失。
show variables like 'innodb_doublewrite';
这个页本身已经损坏了,用redo log来做崩溃恢复是没有意义的。所以在对于应用redo log之前,需要一个页的副本。如果出现了写入失效,就用页的副本来还原这个页,然后再用redo log。这个页的副本就是double write,InnoDB的双写技术。通过它实现了数据页的可靠性。
double write也是由两部分组成,一部分是内存的,一部分是磁盘上的。double write是顺序写入,不会带来很大的开销。
默认情况下,所有表共享一个系统表空间,这个文件会越来越大,且它的空间不会收缩。
独占表空间 file-per-table-tablespaces
我们可以让每张表独占一个表空间。这个开关通过innodb_file_per_table
设置,默认开启。
show variables like 'innodb_file_per_table';
开启后,每张表会开辟一个表空间,这个文件就是数据目录下的ibd文件,存放表的索引和数据。
但是其他类的数据,如回滚(undo)信息,插入缓冲索引页、系统事务信息,二次写缓冲(double write buffer)等还是存放在原来的共享表空间内。
通用表空间 general tablespaces
通用表空间也是一种共享的表空间,跟ibdata1类似。
可以创建一个通用的表空间,用来存储不同数据库的表,数据路径和文件路径可以自定义。
语法
create tablespace ts2673 add datafile '/var/lib/mysql/ts2673.ibd' file_block_size=16K engine=innodb
创建表的时候可以指定表空间,用ALTER修改表空间可以转移表空间。
create table t2673(id integer) tablespace ts2673;
不同表空间的数据是可以移动的。
删除表空间需要先删除里面的所有表。
drop table t2673;
drop tablespace ts2673;
临时表空间 temporary tablespaces
存储临时表的数据,包括用户创建的临时表和磁盘的内部临时表。对应数据目录下的ibtmp1。当数据服务器正常关闭时,该表空间被删除,下次重新产生。
Redo log
即磁盘结构中的redo log
Undo log tablespace
undo log的数据默认在系统表空间ibdata1文件中,因为共享表空间不会自动收缩,也可以单独创建一个undo表空间
后台线程
后台线程的主要作用就是负责刷新内存池中的数据和把修改的数据页刷新到磁盘。后台线程分为master thread,IO thread,purge thread,page clean thread。
master thread:负责刷新缓存数据到磁盘并协调调度其它后台进程。
IO thread:分为 insert buffer、log、read、write进程。分别用来处理 insert buffer、重做日志,读写请求的io回调
purge thread:用来回收undo页
page clean thread:用来刷新脏页
除了InnoDB架构中的日志文件,MySQL的server层也有一个日志文件,叫做bin log,可以被所有的存储引擎使用
Binlog
binlog以事件的形式记录了所有的DDL和DML语句,因为它记录的是操作而不是数据值,属于逻辑日志,可以用来做主从复制和数据恢复。
与redo log不一样,它的文件内容是可以追加的,没有固定大小限制。
在开启了binlog功能的情况下,可以把binlog导出成SQL语句,把所有的操作重放一遍,来实现数据恢复。
binlog的另一个功能就是用来实现主从复制,它的原理就是从服务器读取主服务器的binlog,然后执行一遍。
update user set name=young where id=1
- 先查询折腾数据,如果有缓存,也会用到缓存
- 把name改成young,然后调用引擎api接口,写入这一行数据到内存,同时记录redo log。这是redo log 进入prepare装,然后告诉执行器,执行完成,可以随时提交
- 执行器收到通知后,记录binlog,然后调用存储引擎接口,设置redo log为commit状态
- 更新完成
图中的几个重点:
- 先记录到内存,再写日志文件
- 记录redo log分为两个阶段
- 存储引擎和Server记录不同的日志
- 先记录redo,再记录binlog
为什么要两阶段提交
如果我们执行的是把name改为young,如果写完redo log,还没有写binlog的时候,MySQL重启了。
因为redo log可以在重启的时候用于恢复数据,所以写入磁盘的是young。但是binlog里面没有记录这个逻辑日志,所以这个时候,用binlog去恢复数据或者同步到从库就会出现数据不一致的情况。
所以在写两个日志的情况下,binlog就充当了一个事务的协调者。通知InnoDB来执行prepare或者commit或者rollback。
如果第六步写入binlog失败,就不会提交。
崩溃恢复时,判断事务是否需要提交:
bin log | redo log | 现象 | 恢复操作 |
---|---|---|---|
无记录 | 无记录 | 在redo log写之前crash | 回滚事务 |
无记录 | prepare状态 | 在binlog写完之前crash | 回滚事务 |
有记录 | prepare状态 | 在binlog写完提交事务之前crash | 提交事务 |
有记录 | commit状态 | 正常完成事务 | 不需要恢复 |