Mysql技术内幕Innodb存储引擎读书笔记

概述

mysql的实例是一个进程,单进程多线程的架构。
数据库是由一个个文件组成,需要对这些文件执行增删改查等操作需要通过数据库实例来完成。
存储引擎基于表而非数据库。
mysql数据库区别于其他数据库的最重要特点是其插件式的表存储引擎
Innodb存储引擎支持事务,主要面向oltp,特点是行锁设计,支持外键。
通过使用多版本并发控制来获得高并发性,实现了sql标准的四种隔离级别,默认为可重复读。
对表中数据的存储,innodb存储引擎采用了聚集的方式,每张表按主键的顺序进行存放
连接mysql的操作是一个连接进程和数据库实例进行通信,本质上是进程通信,常见的进程通信方式有管道,命名管道,tcp/ip套接字,unix域套接字。tcp/ip套接字是网络中使用最多的连接方式,命名管道共享内存和unix域套接字都需要客户端和数据库实例在一台服务器上。

InnoDB体系架构

InnoDB存储引擎有多个内存块,组成了一个大的内存池,负责如下工作:

  • 维护所有进程/线程需要访问的多个内部数据结构
  • 缓存磁盘上的数据,方便快速的读取,同时在对磁盘文件的数据修改之前在这里缓存
  • 重做日志(redo log)缓冲

后台线程的主要作用是负责刷新内存池中的数据,保证缓冲池中的内存缓存的是最近的数据。此外将已修改的数据文件刷新到磁盘文件,同时保证在数据库发生异常的情况下InnoDB能恢复到正常运行状态。

InnoDB后台线程

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

  2. IO Thread
    InnoDB存储引擎大量使用了Async IO来处理写IO请求,这样可以极大提高数据库性能,而IO Thread的工作是负责这些IO请求的回调处理

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

  4. Page Cleaner Thread
    Page Cleaner Thread作用是进行脏页的刷新,其目的是减轻原Master Thread的工作以及对于用户查询线程的阻塞,进一步提高InnoDB存储引擎的性能。

内存

数据库系统中,由于CPU速度与磁盘速度之间的鸿沟,基于磁盘的数据库系统通常使用缓冲池技术来踢狗数据库的整体性能。缓冲池简单来说就是一块内存区域,通过内存的速度来弥补磁盘速度较慢的影响。在数据库读取页的操作,首先会将磁盘读到的页存放在缓冲池中,称为将页FIX到缓冲池中,下一次再度相同的页时,会首先判断该页是否在缓冲池中,若在,称该页在缓冲池命中,直接读取该页,否则,读取磁盘上的页。

对于数据库中页的修改操作,则首先修改在缓冲池中的页,然后再以一定的频率刷新到磁盘上。需要注意的是,页从缓冲池刷新回磁盘并不是每次页发生变更时触发,而是通过一种称为Checkpoint的机制刷新。

具体看,缓冲池缓存的数据页类型有:索引页,数据页,undo页,插入缓冲,自使用哈希索引,InnoDB存储的锁信息,数据字典信息等。

缓冲池的管理

通常来说,数据库中的缓冲池是通过LRU(Latest Recent Used,最近最少使用)算法来进行管理的,即最频繁使用的页在LRU列表的前端,而最少使用的页在LRU列表的尾端,当缓冲池不能存放新读取到的页时,将首先释放LRU列表尾端的页。

InnoDB存储引擎中,缓冲池中页的大小默认为16KB,同样使用LRU算法对缓冲池进行管理,不过进行了一些优化,新读取的页并不是放到LRU列表的首部,而是列表的midpoint位置,默认时在LRU列表的5/8长度处。

checkpoint技术

Checkpoint技术所做的事情时将缓冲池的脏页刷新回到磁盘,的目的是解决以下几个问题:

  • 缩短数据库的恢复时间
  • 缓冲池不够用时,将脏页刷新到磁盘
  • 重做日志不可用时,刷新脏页

数据库宕机时,数据库不需要重做所有的日志,因为Checkpoint之前的页都已经刷新回到磁盘,只需对Checkpoint后的重做日志进行恢复。

当缓冲池不够用时,根据LRU算法会移除最近最少使用的页,若此页为脏页,需要强制执行Checkpoint,将脏页刷新到磁盘。

重做日志出现不可用的情况时因为重做日志的设计是循环使用的,并不是让其无限增大的。当数据库发生宕机时,不再需要的重做日志部分可以被覆盖,若还需要,则必须强制Checkpoint,将缓冲池的页至少刷新到当前重做日志的位置。

InnoDB关键特性

  • 插入缓冲 (INSERT BUFFER)
  • 两次写 (Double Write)
  • 自适应哈希索引(Adaptive Hash Index)
  • 异步IO(Async IO)
  • 刷新邻接页(Flush Neighbor Page)

插入缓冲

  1. Insert Buffer

    插入聚集索引一般是顺序的,不需要磁盘的随机读取,速度比较快。对于辅助索引,需要离散的访问辅助索引页,随机读取导致插入操作性能下降。

    Insert Buffer,对于非聚集索引的插入或更新操作,不是每一次直接插入到索引页中,而是先判断插入的非聚集索引页是否在缓冲池中,若在,则直接插入,若不在,则放入到一个Insert Buffer对象中,好似欺骗数据库这个索引已经插入到叶子结点,实际并没有,只是存放在一个位置,然后再以一定的频率进行Inert Buffer和辅助索引页子节点的merge操作,这时通常能将多个插入合并到一个操作中(因为在一个索引页中),大大提高非聚集索引插入的性能。

    Insert Buffer的使用需要同时满足一下两个条件,索引是辅助索引,索引不是唯一(unique)的。辅助索引不能是唯一的,因为在插入缓冲时,数据库并不去查找索引页来判断插入的记录的唯一性,如果去查找肯定又会有离散读取的情况发生,从而使Insert Buffer失去了意义。

    存在的一个问题时写密集的情况下,插入缓冲会占用过多的缓冲池内存,默认最大可以占用1/2。

  2. Change Buffer

    Change Buffer可视为Insert Buffer的升级,对Insert, Delete,Update进行缓冲。适用的对象依然是非唯一的辅助索引。

  3. Insert Buffer的内部实现

    内部实现是一颗B+树

  4. Merge Insert Buffer
    将Insert Buffer 合并(merge)到真正的辅助索引中有如下三个情况:
  • 当辅助索引页被读取到缓冲池时,例如执行正常的Select查询,这时需要检查Insert Buffer Bitmap页,确认该辅助索引页是否有记录存放在Insert Buffer B+树中,若有,则将该页的记录插入到辅助索引页中。
  • Insert Buffer Bitmap页可以用来检测每个辅助索引页的可用空间,如果小于1/32页,则会强制进行一个合并操作
  • Master Thread 每秒或者每十秒进行一次Merge Insert Buffer的操作

两次写

屏幕快照 2018-12-12 6.58.09.png
当数据库宕机时,如果某个16KB的页只写了前4KB,这种情况被称为部分写实效,这时如果使用重做日志也无法恢复,因为此时这个页已经发生了损坏,在应用重做日志前,用户需要一个页的副本,当写入失效时,先通过页的副本还原该页,再进行重做。

在对脏页进行刷新时,并不直接写磁盘,而是通过memcpy函数将脏页先复制到内存中的doublewrite buffer,之后通过doublewrite buffer写入共享表空间的物理磁盘上,然后马上调用fsync函数,同步磁盘。

如果操作系统将页写入磁盘时发生了崩溃,在恢复过程中,InnoDB存储引擎可以从共享表空间的doublewrite中找到该页的一个副本,将其复制到表空间文件,然后应用重做日志。

自适应哈希索引

一般情况下哈希的时间复杂度为O(1), 而B+树则取决于树的高度。InnoDB会监控对各表上各索引页的查询,如果观察到建立哈希索引可以带来速度提升,则建立哈希索引,称为自适应哈希索引。是通过缓冲池的B+树构造而来,建立的速度很快,而且不需要对整张表构建哈希索引。InnoDB存储引擎会自动根据访问的频率和模式来自动喂某些热点数据页建立哈希索引。哈希索引只能用来进行等值的查询。

异步IO

与AIO对应的是Sync IO,即每进行一次IO操作,需要等待此次操作结束才能继续接下来的操作。如果用户发出的是一条索引扫描的查询,可能需要扫描多个索引页,每扫描一个页并等待其完成后进行下一次的扫描是没有必要的。用户可以在发出一个IO请求后立即再发出另一个IO请求,当全部IO请求发送完毕后,等待所有IO操作的完成,这就是AIO。

AIO的另一优势是可以进行IO merge操作,将多个IO合并为一个IO,例如要访问三个相邻的页,AIO会判断出是连续的,从而只发送一个IO请求。

在InnoDB存储引擎中,read ahead方式的读取以及脏页的刷新都是由AIO完成。

刷新邻接页

刷新邻接页的工作原理是: 当刷新一个脏页时,InnoDB存储引擎会检测该页所在区(extent)的所有页,如果是脏页,那么一起进行刷新。这样的好处是通过AIO将多个IO写入操作合并为一个IO操作,但该特性只适用于机械硬盘,固态硬盘由于超高的IOPS性能,建议关闭该特性。

二进制日志

二进制日志(binary log)记录了对数据库执行更改的所有记录。作用:

  • 恢复: 某些数据的恢复需要二进制日志,例如,在一个数据库全贝文件恢复后,用户可以通过二进制日志进行point-in-time的恢复。
  • 复制(replication):原理与恢复蕾丝,通过复制和执行二进制日志使一台远程的数据库(slave)与一台数据库(master)进行实时同步。
  • 审计:用户可以通过二进制日志中的信息进行审计,判断是否有对数据库进行注入的攻击。

InnoDB索引概述

InnoDB支持的哈希是自适应的,InnoDB会根据表的使用情况自动为表生成哈希索引,不能认为干预是否在一张表中生成哈希索引。

B+树索引的构造类似于二叉树,但并不能找到一个给定键值的具体行,只是被查找数据行所在的页,然后数据库通过把页读入到内存,再在内存中进行查找。

聚集索引

表中数据按主键顺序存放,叶子结点存放的是整张表的行记录数据,叶子之间通过双向链表进行连接。聚集索引的存储并不是物理上连续的,而是逻辑上连续的。

辅助索引

每张表可以有多个辅助索引,当通过辅助索引来寻找数据时,InnoDB会遍历辅助索引并通过页级别的指针获得指向的主键,然后通过主键索引来找到完整的行记录。

Fast Index Creation

mysql 5.5版本之前对于索引的添加或删除的操作过程如下:

  • 创建一张新的临时表,表结构为通过命令alter table新定义的结构
  • 然后把原表中数据导入到临时表
  • 接着删除原表
  • 最后把临时表重命名为原来的表名

Fast Index Creation(快速索引创建)简称FIC。对于辅助索引的创建,InnoDB会对创建索引的表加上一个S锁,创建过程中,不需要重建表,因此速度提高较多;删除辅助索引就更简单了,InnoDB只需更新内部视图,并将辅助索引的空间标记为可用,同时删除数据库内部视图上对该表的索引定义即可。

FIC在索引的创建过程中加了S锁,所以创建过程中只能对该表进行读操作,若有事务需要进行写操作,数据库的服务同样不可用。FIC只限定于辅助索引,对于主键的创建和删除同样需要重建一张表。

Online Schema Change

todo

lock

lock的对象是事务,用来锁定的是数据库中的对象,如表、页、行,并且一般lock的对象仅在事务commit或rollback后进行释放。

InnoDB实现了如下两种标准的行级锁:

  • 共享锁(S Lock),允许事务读一行数据
  • 排他锁(X Lock), 允许事务删除或更新一行数据

    共享锁之间兼容,排他锁与任何锁都不兼容,兼容是指对同一记录(row)锁的兼容性情况。

意向锁

InnoDB支持在多粒度上进行加锁操作,为了支持在不同粒度上进行加锁操作,InnoDB支持一种额外的锁方式,称为意向锁。意向锁是将锁定的对象分为多个层次,意向锁意味着事务希望在更细粒度上进行加锁。

若将上锁的对象看成一棵树,那么对最下层的对象上锁,首先需要对粗粒度的对象上锁,例如对页上的记录r进行上X锁,分别需要对数据库、表、页上意向锁IX,最后对记录r上X锁。若其中任何一个部分等待,那么该操作需要等待粗粒度锁的完成。举例来说,在对记录r加X 锁之前,已经有事务对表1进行了S表锁,之后事务需要对记录r在表1上加上IX, 由于不兼容,所以该事务需要等待表锁操作的完成。

InnoDB支持两种意向锁:

  • 意向共享锁(IS Lock), 事务想要获得一张表中某几行的共享锁
  • 意向排他锁(IX Lock), 事务想要获得一张表中某几行的排他锁

锁的兼容性:

屏幕快照 2018-12-30 上午11.19.51.png

一致性非锁定读

一致性的非锁定读是指InnoDB存储引擎通过行多版本控制的方式来读取当前执行时间数据库中行的数据。如果读取的行正在执行delete或update操作,这时读取操作不会因此去等待行上锁的释放,而是去读取一个快照数据。快照数据是该行的之前版本的数据,是通过undo段来完成的,而undo用于在事务中回滚数据,因此快照数据本身没有额外的开销。读取快照数据也无需上锁,因为没有事务需要对历史数据进行修改。

非锁定读极大提高了数据可的并发性。一个行记录可能有不止一个历史版本,称为行多版本技术,由此带来的并发控制,称为多版本并发控制(MVCC).

一致性锁定读

有时候用户需要对读取数据进行加锁,有两种操作

  • select … for update
  • select .. lock in share mode

for udpate对读取的行记录加一个X锁,share mode对读取的行加一个S锁。

行锁的3种算法

  • Record Lock: 单个行记录上的锁
  • Gap Lock: 间隙锁,锁定一个范围,但不包含记录本身
  • Next-Key Lock: Gap Lock + Record Lock, 锁定一个范围,并且锁定记录本身

Next-Key Lock是为了解决幻像问题(phantom problem)。若一个索引上有3, 7, 10这几个值,则锁定的区间为:(-∞,3], (3, 7], (7, 10], (10, +∞).当查询的索引是唯一索引时,InnoDB会对Next-Key Lock降级为Record Lock, 即仅锁住索引本身(因为没有范围了).

解决phantom problem

在默认的事务隔离级别即repeatable read下,InnoDB采用Next-Key Locking机制避免幻像问题。phantom read是指在同一个事务下,连续执行两次同样的sql可能导致不同的结果,第二次可能返回之前不存在的行。

例如表t由1、2、5三个值组成,执行select * from t where a > 2 for update会对(2, +∞)这个范围加X锁,因此任何对于这个范围的插入都是不允许的,从而避免phantom read.

脏度

脏页指的是缓冲池中已经被修改的页,但是还没有刷新到磁盘中,即数据库实例内存中的页和磁盘中的页的数据是不一样的,而所谓脏数据是事务对缓冲池中行记录的修改,并且还没有被提交(commit).

脏读发生的条件是事务的隔离级别为READ UNCOMMITTED, 而目前绝大部分的数据库都至少设置成READ COMMITTED.

不可重复读

不可重复读是指在一个事务哪多次读取同一数据集合,由于两次读之间有一个新的事务进行了修改,导致读取的数据可能是不一样的。

不可重复读和脏读的区别是:脏读读到未提交的数据,而不可重复读读到的却是已经提交的数据。

在Next-Key Lock算法下,对于索引的扫描,不仅是锁住扫描到的索引,还锁住这些索引覆盖的范围,因此在这个范围内的插入都是不允许的,这样就避免了不可重复读的现象。

丢失更新

简单来说就是一个事务的更新操作会被另一个事务的更新操作所覆盖,从而导致数据的不一致,例如:

事务T1将行记录r更新为v1, 但是事务T1未提交,与此同时,事务T2将行记录r更新为v2, 事务T2未提交,接着事务T1提交,然后事务T2提交。

要避免丢失更新发生,需要让事务在这种情况下的操作变成串行行,而不是并行的操作,可以在用户读取的记录上加上一个排他X锁,使用select .. for update.

事务的ACID特性

  • 原子性, 要么都执行成功,要么都不执行
  • 一致性,指事务将数据库从一种状态转变为下一种一致性的状态,事务开始之前和结束之后,数据库的完整性约束没有被破坏,例如一个字段是唯一约束的,不会出现事务提交或回滚之后就不唯一了。
  • 隔离性,事务的隔离性要求每个读写事务的对象对其他事务的操作对象能相互分离,即该事务提交前对其他事务都不可见,通常使用锁来实现。
  • 持久性,事务一旦提交,结果就是永久性的,即使发生宕机等故障,数据库也能将数据恢复。

redo log

重做日志用来实现事务的持久性,由两部分做成,一个是内存中的重做日志缓冲,是易失的,一个是重做日志文件,是持久的。防止在发生故障的时间点,尚有脏页未写入磁盘,在重启mysql服务的时候,根据redo log进行重做,从而达到事务的持久性这一特性。

记录的是物理格式的日志,页面的修改的信息。事务开始之后就产生redo log, redo log的落盘并不是随着事务的提交才写入的,而是事务执行过程中,便开始写入redo log文件中。当对应事务的脏页写入到磁盘之后,redo log的使命也就完成了,重做日志占用的空间就可以被覆盖。

有三种方式将日志缓冲区的日志刷新到磁盘(日志文件):

  • Master Thread每秒一次执行innodb_log_buffer刷新到重做日志文件
  • 每个事务提交时会将重做日志刷新到重做日志文件
  • 当重做日志缓冲可用空间少于一半时,重做日志缓存被刷新到重做日志文件

因此重做日志的写盘,是随着事务的开始就进行了的,即使事务没有提交,也仍然会每秒将重做日志缓存刷新到重做日志文件,这可以很好的解释再大的事务提交(commit)的时间也很短暂。

undo log

保存事务发生之前的数据的一个版本,可以用于回滚,同时提供多版本并发控制下的读(MVCC), 即非锁定读

逻辑格式的日志,在事务发生之前,将当前的版本生成undo log, undo也会产生redo来保证undo log的可靠性。事务提交之后,undo log并不能立即被删除,而是放入待清理的链表,由purge线程判断是否有其他事务在使用undo段中表的上一个事务之前的版本,决定是否清理。