MySQL技术内幕(一)


MySQL技术内幕

2. InnoDB存储引擎

2.1 InnoDB存储引擎概述

特点:行锁设计、支持MVCC、支持外键、提供一致性非锁定读

2.2 InnoDB体系架构

2.2.1 后台线程

InnoDB存储引擎时多线程的模型,因此后台有多个不同的后台线程,负责处理不同的任务。

1. Master Thread

非常核心的后台线程,主要负责将缓冲池中的数据异步刷新到磁盘,保证数据的一致性,包括脏页的刷新、合并插入缓冲(INSERT BUFFER)、UNDO页的回收等

2. IO Thread

InnoDB引擎中使用了大量的AIO(Async IO)来处理IO请求,IO Thread的主要工作时负责这些IO请求的回调处理。

4个IO Thread:write、read、insert buffer和log IO Thread。

3. Purge Thread

事务被提交后,其所使用的undolog可能不再需要,因此需要PurgeThread来回收已经使用并分配的undo页。

4. Page Cleaner Thread

作用是将之前版本中脏页的刷新操作都放入到单独的线程中来完成。

2.2.2 内存

1. 缓冲池(Buffer Pool)
  • 读取:在数据库中进行读取页的操作,首先将从磁盘读到的页存放在缓冲池中,这个过程称为将页“FIX”在缓冲池中。下一次再读相同的页时,首先判断该页是否在缓冲池中。若在缓冲池中,称该页在缓冲池中被命中,直接读取该页。否则,读取磁盘上的页。
  • 修改:对于数据库中页的修改操作,则首先修改在缓冲池中的页,然后再以一定的频率刷新到磁盘上。这里需要注意的是,页从缓冲池刷新回磁盘的操作并不是在每次页发生更新时触发,而是通过一种称为Checkpoint的机制刷新回磁盘。

缓冲池中的数据类型:索引页、数据页、undo页、插入缓冲(insert buffer)、自适应哈希索引(adaptive hash index)、锁信息(lock info)、数据字典信息(data dictionary)。

2. LRU算法及优化

典型的LRU算法是最频繁使用的页在LRU列表的前端,而最少使用的页在LRU列表的尾端。当缓冲池不能存放新读取到的页时,将首先释放LRU列表中尾端的页。

问题:

  1. 全表扫描:全表扫描的数据量大,需要淘汰的数据页多,在淘汰的过程中极有可能将频繁使用到的数据页给淘汰了,而新数据却是使用频率很低的数据。
  2. 预读

优化:

  1. 冷热分离:
    • 将LRU链表分为两部分,一部分用来存放冷数据,也就是刚从磁盘读进来的数据,另一部分用来存放热点数据,也就是经常被访问到的数据。
    • 当从磁盘读取数据页后,会先将数据页放到LRU链表冷数据区的head,如果这些数据页在1s后再被访问,则这些数据页会被移动到热点数据区的head。
    • 淘汰数据页时,从冷数据区的tail开始淘汰。
  2. 当一个数据页处于热点数据区,且在热点数据区的前1/4区域,那么当访问这个数据页的时候,不需要移动到热点数据区的head部,如果位于热点数据区的后3/4区域,那么当访问这个数据页的时候,会把它移动到热点数据区的head部。
  3. 支持多个Buffer Pool。
3. Flush列表

在LRU列表中的页被修改后,称该页为脏页(dirty page),即缓冲池中的页和磁盘上的页的数据产生了不一致。这时数据库会通过CHECKPOINT机制将脏页刷新回磁盘,而Flush列表中的页即为脏页列表。需要注意的是,脏页既存在于LRU列表中,也存在于Flush列表中。LRU列表用来管理缓冲池中页的可用性,Flush列表用来管理将页刷新回磁盘,二者互不影响。

4. 重做日志缓冲(redo log buffer)

重做日志在下列三种情况下会将重做日志缓冲中的内容刷新到外部磁盘的重做日志文件中:

  • Master Thread每一秒将重做日志缓冲刷新到重做日志文件;
  • 每个事务提交时会将重做日志缓冲刷新到重做日志文件;
  • 当重做日志缓冲池剩余空间小于1/2时,重做日志缓冲刷新到重做日志文件。

2.3 Checkpoint 技术

脏页:如果一条DML语句,如Update、Delete改变了页中的记录,也就是缓冲池中页的版本比磁盘的新,那么此时页就是脏的。

事务提交时,先写重做日志,再修改页,实现ACID中的D(Durability持久性)。

在InnoDB引擎中,通过LSN(Log Sequence Number)来标记版本,LSN是8字节的数字,单位是字节。

发生Fuzzy Checkpoint的情况:

  1. Master Thread Checkpoint:Master Thread每秒或每十秒从缓冲池的脏页列表中异步刷新一定比例的页回磁盘;
  2. FLUSH_LRU_LIST Checkpoint:Page Cleaner线程在LRU列表中空闲页少于某值得时候,会从LRU列表尾端移除一些页,如果这些页中有脏页,那么需要进行Checkpoint;
  3. Async/Sync Flush Checkpoint:重做日志文件不可用时,Page Cleaner Thread强制刷新一些页回磁盘;
  4. Dirty Page too Much:缓冲池中脏页比例超过设定值时,强制刷新一些页回磁盘。

2.4 Master Thread

Master Thread:最高优先级,由主循环、后台循环、刷新循环、暂停循环组成,根据数据库状态自动切换循环。

Master Thread 伪代码:

void master_thread() {
  goto loop;
  
  loop:	// 1. 主循环
  for (int i = 0; i< 10; i++) {
    Thread.sleep(1000); // 休眠一秒
    // 每秒的操作
    1.1.1. 刷新日志缓冲到磁盘,即使这个事务还没有提交;
    // innodb_io_capacity设置的磁盘IO吞吐量
    if (last_one_second_ios < 5% * innodb_io_capacity) { 
      // 如果当前1秒发生IO次数小于5
      1.1.2. 合并插入缓冲(Insert Buffer), 数量:5% * innodb_io_capacity;
    }
    if (buf_get_modified_ratio_pct > innodb_max_dirty_pages_pct) {
      // 如果当前缓冲池中脏页比例大于配置文件中的设定值
      1.1.3.1. 刷新至多innodb_io_capacity个脏页到磁盘;
    } else if (enable adaptive flush) { // 自适应刷新,通过产生redo日志速度判断
      1.1.3.2. 刷新合适量的脏页到磁盘;
    }
    if (no user activity) {
      // 没有用户活动(数据库空闲),则切换到后台循环
      1.1.4. goto background loop;
    }
  }
  // 每十秒的操作
  if (last_ten_second_ios < innodb_io_capacity) {
    1.2.1. 刷新innodb_io_capacity个脏页到磁盘;
  }
  1.2.2. 合并至多5% * innodb_io_capacity个插入缓冲;
  1.2.3. 将日志缓冲刷入到磁盘;
  1.2.4. 删除最多20个无用的undo页;
  if (buf_get_modified_ratio_pct > 70%) {
    1.2.5.1 刷新innodb_io_capacity个脏页到磁盘
  } else {
    1.2.5.2 刷新10% * innodb_io_capacity脏页到磁盘
  }
  goto loop;
  
  background loop: // 2. 后台循环
  	2.1. 删除无用的undo页;
    2.2. 合并innodb_io_capacity个插入缓冲;
    if (not idle) {
      // 跳回主循环
      goto loop;
    } else {
      // 跳到刷新循环
      goto flush loop;
    }
  
  flush loop: // 3. 刷新循环
  	3.1. 刷新innodb_io_capacity个脏页到磁盘;
    if (buf_get_modified_ratio_pct > innodb_max_dirty_pages_pct) {
      // 如果当前缓冲池中脏页比例大于配置文件中的设定值
      goto flush loop;
    }
  	goto suspend loop;
  
  suspend loop: // 4. 暂停循环
  	suspend_thread(); // Master Thread 挂起
  	waiting event; // 等待事件的发送
  	goto loop;
}

InnoDB 1.2.x版本的Master Thread

if InnoDB is idel:
	srv_master_do_idle_tasks();
else:
	srv_master_do_active_tasks();

其中:srv_master_do_idle_tasks()就是之前版本中每10秒的操作,srv_master_do_active_tasks()处理的是之前每秒中的操作。刷新脏页的操作,分离到一个单独的Page Cleaner Thread。

2.5 InnoDB的关键特性

2.5.1 Insert Buffer

对于非聚集且不是唯一索引的插入或者更新操作,不是每一次都直接插入到索引页中,而是先判断插入的非聚集索引页是否在缓冲池中,若在,则直接插入;若不在,则先放到一个Insert Buffer对象中,然后在以一定的频率和情况进行Insert Buffer和辅助索引叶子节点的merge操作。能大大提高对于非聚集索引插入的性能。

Insert Buffer的使用需要满足以下两个条件:

  1. 索引是辅助索引(secondary index);
  2. 索引不是唯一(unique)。

Merge Insert Buffer

可能发生在以下几种情况:

  • 辅助索引页被读取到缓冲池;
  • Insert Buffer Bitmap页追踪到该辅助索引页已无可用空间;
  • Master Thread

2.5.2 两次写(double write)

部分写失效(partial page write):

数据库宕机时,缓冲池的某个页只写了一部分到磁盘中,比如16KB的页,只写了前4KB就发生了宕机,这种情况被称为部分写失效。

redo日志记录的是对页的物理操作,如偏移量,因此重做是无意义的。

2.5.3 自适应哈希

数据库自优化,通过B+树页构造哈希索引,提升查询速度。

2.5.4 异步IO

2.5.5 刷新邻接页