binlog用于记录数据库执行的写入性操作(不包括查询)信息,以二进制的形式保存在磁盘中。binlog是mysql的逻辑日志,并且由Server层进行记录,使用任何存储引擎的mysql数据库都会记录binlog日志。binlog是通过追加的方式进行写入的,可以通过max_binlog_size参数设置每个binlog文件的大小,当文件大小达到给定值之后,会生成新的文件来保存日志。
对于InnoDB存储引擎而言,只有在事务提交时才会记录biglog,此时记录还在内存中,Mysql通过sync_binlog参数控制biglog的刷盘时机,取值范围是0-N,
N代表多少条以后开始进行刷盘,当设置为0的时候由系统自行判断何时写入磁盘,当设置为1的时候,相当于每次Commit就进行刷盘一次,但是这个时候要注意与redo log日志可能存在不一致的情况,这个时候需要设置innodb_support_xa参数也为1,这样就能保证两个两份日志是同步的。
redo log包括两部分:redo log buffer和redo log file,redo log buffer是在内存中,redo log file是在磁盘上,当MySQL执行DML语句的时候,首先写入redo log buffer,然后按照一定条件顺序写入redo log file,什么时候会触发buffer内容写入到file当中呢?
redo log记录数据页的变更,在设计上redo log采用了大小固定,循环写入的方式,当写到结尾时,会回到开头循环写日志,本质上就是一个环状。
-
checkpoint表示已经完整刷到磁盘上data page上的LSN,因此恢复时仅需要恢复从checkpoint开始的日志部分,LSN表示写入日志的字节的总量,例如,当数据库在上一次checkpoint的LSN为10000时宕机,且事务是已经提交过的状态。启动数据库时会检查磁盘中数据页的LSN,如果数据页的LSN小于日志中的LSN,则会从检查点开始恢复。
-
在宕机前正处于checkpoint的刷盘过程,且数据页的刷盘进度超过了日志页的刷盘进度。这时候一宕机,数据页中记录的LSN就会大于日志页中的LSN,在重启的恢复过程中会检查到这一情况,这时超出日志进度的部分将不会重做,因为这本身就表示已经做过的事情,无需再重做。
在恢复的过程中因为redo log记录的是数据页的物理变化,因此恢复的时候速度比逻辑日志(如binlog)要快很多;
使用的场景
MySQL用来确保事务的持久性。redo log记录事务执行后的状态,用来恢复未写入data file的已成功事务更新的数据。防止在发生故障的时间点,尚有脏页未写入磁盘,在重启mysql服务的时候,根据redo log进行重做,从而达到事务的持久性这一特性。
undo log
undo log记录数据的逻辑变化,用户事务的回滚操作和MVCC, undo log 存放在共享表空间中,以段(rollback segment)的形式存在。
undo log日志格式
逻辑格式的日志,在事务进行回滚的时候,可以将数据从逻辑上恢复至事务之前的状态。
使用的场景
保证数据的原子性,保存了事务发生之前的数据的一个版本,可以用于回滚,同时可以提供多版本并发控制下的读(MVCC),也即非锁定读。
当事务提交之后,undo log并不能立马被删除,而是放入待清理的链表,由Purge线程判断是否由其他事务在使用undo段中表的上一个事务之前的版本信息,决定是否可以清理undo log的日志空间。
InnoDB 存储引擎是基于磁盘存储的,也就是说数据都是存储在磁盘上的,由于 CPU 速度和磁盘速度之间的鸿沟, InnoDB 引擎使用缓冲池技术来提高数据库的整体性能。内存池简单来说就是一块内存区域.在数据库中进行读取页的操作,首先将从磁盘读到的页存放在内存池中,下一次读取相同的页时,首先判断该页是不是在内存池中,若在,称该页在内存池中被命中,直接读取该页。否则,读取磁盘上的页。对于数据库中页的修改操作,首先修改在内存池中页,然后再以一定的频率刷新到磁盘,并不是每次页发生改变就刷新回磁盘。
内存池中缓存的信息主要有:index page、data page、insert buffer、自适应哈希索引、 lock info、数据字典信息等。索引页和数据页占缓冲池的很大一部分。在InnoDB中,内存池中的页大小默认为16KB,和磁盘的页的大小默认一样。我们已经介绍过数据文件的存储结构相信大家对缓存结构的内容也会有一定理解,我们就不单独介绍了,后面只会重点强调一下insert buffer和自适应哈希索引这两块内容,以及扩展下内存池的设计原理。
Insert Buffer
Insert Buffer的设计,对于非聚集索引的插入和更新操作,不是每一次直接插入到索引页中,而是先判断插入非聚集索引页是否在缓冲池中,若存在,则直接插入,不存在,则先放入一个Insert Buffer对象中。数据库这个非聚集的索引已经插到叶子节点,而实际并没有,只是存放在另一个位置。然后再以一定的频率和情况进行Insert Buffer和辅助索引页子节点的merge(合并)操作,这时通常能将多,这就大大提高了对于非聚集索引插入的性能。这个时候可能会照成一种情况,当MySQL数据库发生宕机的时候有有大量的Insert Buffer没有被合并到非聚集索引的页当中的时候,这个时候MySQL恢复需要很长的时间。
需要满足的条件:
索引是非聚集索引,索引不是唯一的;
对于具体的实现我们下次再聊;
自适应哈希索引
InnoDB存储引擎会监控对表上各索引页的查询。如果观察到建立哈希索引可以提升速度,这简历哈希索引,称之为自适应哈希索引。AHI是通过缓冲池的B+树页构造而来的。因此建立的速度非常快,且不要对整张表构建哈希索引。InnoDB存储引擎会自动根据访问的频率和模式来自动的为某些热点页建立哈希索引。
Master Thread
这是最核心的一个线程,主要负责将缓冲池中的数据异步刷新到磁盘,保证数据的一致性,包括赃页的刷新、合并插入缓冲等。
IO Thread
在 InnoDB 存储引擎中大量使用了异步 IO 来处理写 IO 请求, IO Thread 的工作主要是负责这些 IO 请求的回调处理。
Purge Thread
事务被提交之后, undo log 可能不再需要,因此需要 Purge Thread 来回收已经使用并分配的 undo页. InnoDB 支持多个 Purge Thread, 这样做可以加快 undo 页的回收。
完成整体功能介绍以后,我们开始聊聊数据如何插入到InnoDB引擎上的:
假设场景如下:
首先我们创建一张表T,主键为Id,辅助索引为a
create table T(id int primary key, a int not null, name varchar(16),index (a))engine=InnoDB;
接下来插入一条数据,
insert into t(id,a,name) values(id1,a1,'哈哈'),(id2,a2,'哈哈哈');
我们介绍过MySQL读取数据的流程,Server层我们还是会经过连接器、解析器、优化器、执行器这些东西,这些我们就不介绍了,我们主要介绍剩下的操作:
插入数据时候可能有两种场景:
第一种场景:假设Id1这条数据在内存池中,
-
直接更新Buffer Pool中的Index Page和Data Page;
-
写入redo log中,处于预提交状态;
-
写入binlog中,
-
提交事务,处于commit状态,两阶段提交;
-
后台线程写入到数据文件的索引段和数据段中;
第二种场景假设id2这条数据不再内存池中,
-
数据写入到内存池中,非聚集索引写入到Insert Buffer,其他数据写入Data Page中;
-
后续的动作保持和上面剩下的步骤一样。
我们来聊聊内存池(Buffer Pool)运行原理,可以从以下3个方面来看:
-
如何管理缓存的页?
InnoDB为每一个缓存页都创建了一些控制信息,这些控制信息包括该页所属的表空间编号、页号、页在Buffer Pool中的地址、LSN等信息,每个缓存页对应的控制信息占用的内存大小是相同的,我们就把每个页对应的控制信息占用的一块内存称为一个控制块吧,控制块和缓存页是一一对应的,它们都被存放到 Buffer Pool 中,其中控制块被存放到 Buffer Pool 的前边,缓存页被存放到 Buffer Pool 后边,所以整个Buffer Pool对应的内存空间看起来就是这样的:
碎片就是空间不够分配的缓存页。
当我们最初启动MySQL服务器的时候,需要完成对Buffer Pool的初始化过程,就是分配Buffer Pool的内存空间,把它划分成若干对控制块和缓存页。但是此时并没有真实的磁盘页被缓存到Buffer Pool中,之后随着程序的运行,会不断的有磁盘上的页被缓存到Buffer Pool中,接下来会有一个问题就是怎么区分Buffer Pool中哪些缓存页是空闲的,哪些已经被使用?我们最好在某个地方记录一下哪些页是可用的,我们可以把所有空闲的页包装成一个节点组成一个链表,这个链表也可以被称作Free链表。因为刚刚完成初始化的Buffer Pool中所有的缓存页都是空闲的,所以每一个缓存页都会被加入到Free链表中,整体设计如下图:
从图中可以看出,Free链表包含着链表的头节点地址,尾节点地址,以及当前链表中节点的数量等信息。每个Free链表的节点中都记录了某个缓存页控制块的地址,而每个缓存页控制块都记录着对应的缓存页地址,所以相当于每个Free链表节点都对应一个空闲的缓存页。
每当需要从磁盘中加载一个页到Buffer Pool中时,就从Free链表中取一个空闲的缓存页,并且把该缓存页对应的控制块的信息填上,然后把该缓存页对应的Free链表节点从链表中移除,表示该缓存页已经被使用了。
-
缓存的淘汰?
机器的内存大小是有限的,所以MySQL的InnoDB Buffer Pool的大小同样是有限的,如果需要缓存的页占用的内存大小超过了Buffer Pool大小,InnoDB Buffer Pool采用经典的LRU算法来进行页面淘汰,以提高缓存命中率。当Buffer Pool中不再有空闲的缓存页时,就需要淘汰掉部分最近很少使用的缓存页。
当我们需要访问某个页时,可以这样处理LRU链表:
1.如果该页不在Buffer Pool中,在把该页从磁盘加载到Buffer Pool中的缓存页时,就把该缓存页包装成节点塞到链表的头部。
2.如果该页在Buffer Pool中,则直接把该页对应的LRU链表节点移动到链表的头部。
但是这样做会有一些性能上的问题,比如你的一次全表扫描或一次逻辑备份就把热数据给冲完了,就会导致导致缓冲池污染问题!Buffer Pool中的所有数据页都被换了一次血,其他查询语句在执行时又得执行一次从磁盘加载到Buffer Pool的操作,而这种全表扫描的语句执行的频率也不高,每次执行都要把Buffer Pool中的缓存页刷新一次,这严重的影响到其他查询对 Buffer Pool 的使用,降低了缓存命中率。
针对这种场景InnoDB存储引擎对传统的LRU算法做了一些优化,在InnoDB中加入了midpoint。新读到的页,虽然是最新访问的页,但并不是直接插入到LRU列表的首部,而是插入LRU列表的midpoint位置。这个算法称之为midpoint insertion stategy。默认配置插入到列表长度的5/8处。midpoint由参数innodb_old_blocks_pct控制。
midpoint之前的列表称之为new列表,之后的列表称之为old列表。可以简单的将new列表中的页理解为最为活跃的热点数据。
-
脏页如何实现刷新?
更新是在缓存池中先进行的,那它就和磁盘上的页不一致了,这样的缓存页也被称为脏页。所以需要考虑这些被修改的页面什么时候刷新到磁盘?当然,最简单的做法就是每发生一次修改就立即同步到磁盘上对应的页上,但是频繁的往磁盘中写数据会严重的影响程序的性能。所以每次修改缓存页后,我们并不着急立即把修改同步到磁盘上,而是在未来的某个时间点进行同步,由后台刷新线程依次刷新到磁盘,实现修改落地到磁盘。
但是如果不立即同步到磁盘的话,那之后再同步的时候我们怎么知道Buffer Pool中哪些页是脏页,哪些页从来没被修改过呢?我们需要创建一个存储脏页的链表,凡是在LRU链表中被修改过的页都需要加入这个链表中,因为这个链表中的页都是需要被刷新到磁盘上的,所以也叫Flush链表,链表的构造和Free链表差不多,这里的脏页修改指的此页被加载进Buffer Pool后第一次被修改,只有第一次被修改时才需要加入Flush链表,如果这个页被再次修改就不会再放到Flush链表了,因为已经存在。需要注意的是,脏页数据实际还在LRU链表中,而Flush链表中的脏页记录只是通过指针指向LRU链表中的脏页。
欢迎大家点点关注,点点赞,感谢!