InnoDB存储引擎学习笔记

InnoDB结构:后台线程 + 内存池

后台线程:主要职责是刷新内存池中的数据,保持内存中缓存的数据是最近的、把修改过的数据文件写到磁盘文件中、保存InnoDB在发生异常后能恢复到原来的状态;总共有7个线程,其中有4个IO线程(write thread、read thread、log thread、insert buffer thread)、锁监控线程、错误监控线程、master thread。

内存池:由多个内存块构成。主要分为缓冲池(buffer pool)、重做日志缓存池(redo log buffer)、额外内存池(additional memory pool)。

InnoDB关键特性

插入缓冲:要求索引是辅助索引、索引不是唯一的。

对于非聚集索引的插入或者更新操作,不是每一次直接插入索引页中。而是先判断非聚集索引页是否在缓冲池中。如果在,则直接插入;如果不在,则把操作先放入一个插入缓冲区,好似欺骗数据库这个非聚集索引已经插入到叶子节点了,然后再以一定的频率执行插入缓冲和非聚集索引页子节点的合并操作,这时通常能将多个插入合并为一个操作中(因为在一个索引页中),这就大大提高了性能。

为什么索引不能是唯一的?

因为在插入到插入缓冲池中时,并不会去查找索引页,如果索引是唯一的,那么就需要去索引页查询是否插入重复数据,因此插入缓冲要求索引不能是唯一的。

两次写:在应用重做日志前,当发生写失效时,通过一个页的副本还原该页,在进行重做。

写失效:在数据库宕机时,可能数据库正在写一个页面,而这个页只写了一部分的情况。

写失效为导致什么问题?

因为数据库IO的最小单位为16K,磁盘IO的最小单位为512字节。

因此在写入数据页,就可能导致数据丢失,当发生宕机时,我们可以通过重做日志来还原数据,但是重做日志中记录的是对页的物理操作(如:偏移量800,写'aaa'记录),如果这个页本身已经失效,那么对其重做是没有意义的。

为什么log write不需要double write的支持?

因为log write的写入单位就为512字节。

doublewrite由两部分组成:一部分是内存中doublewrite buffer,大小为2MB;另一部分是物理磁盘上共享表空间中连续的128个页,即两个区,大小同样为2MB。

当缓冲池中脏页刷新时,并不直接写磁盘,而是会通过memcpy函数将脏页先拷贝到内存中的doublewrite buffer,之后再通过duoblewrite buffer再分两次写。第一步:每次写入1MB(即一个区)到共享表空间的物理磁盘上,马上调用fsync函数,同步磁盘,避免缓冲带来的问题。第二步:待写入共享表空间完成后(即第一步完成后),再写入实际的各个表空间文件。

那么即使操作系统在将页写入磁盘过程中崩溃了,在恢复过程中,InnoDB可以从共享表空间中的doublewrite中找到页的一个副本,将其拷贝到私有的表空间文件,在应用重做日志。

自适应哈希索引InnoDB会监控表上索引的查找,如果观察到建立哈希索引可以带来速度的提升,则建立哈希索引,因此称为自适应。

自适应哈希索引是通过缓冲池中的B+树来构造而来,因此建立速度很快。并且不需要对整个表到建立哈希索引,InnoDB会自动根据访问的频率和模式来为某些页建立索引。

需要注意的是:哈希索引只能用于等值查询,不支持范围查找。

日志

binlog:二进制日志记录了对数据库执行更改的所有操作(不包括select和show操作),还包括执行数据库更改操作的时间和执行时间等信息。主要有两个作用:

  • 恢复:某些数据的恢复需要二进制日志,如当一个数据库全备份文件恢复后,我们可以通过二进制日志进行point-in-time的恢复。
  • 复制:原理与恢复类似,通过复制和执行二进制日志使得一台远程的MySQL数据库(slave)与一台MySQL数据库(master)进行实时同步。

当使用事务的表存储引擎(如:InnoDB)时,所有未提交的二进制日志都会被记录到一个缓存中,当事务提交时直接将缓冲的二进制日志写入磁盘上的二进制日志文件。

二进制日志的格式:

  • STATEMENT:记录的是日志逻辑SQL语句。(如果需要通过二进制日志来复制,且SQL语句中存在uuidrand函数,可能会导致复制的数据不一致)。
  • ROW:记录表的行更改情况。
  • MIXED:默认采用STATEMENT模式记录,当出现一些情况时,会使用ROW模式记录,可能情况有:
    • 表的存储引擎为NDB
    • 使用uuiduser等不确定函数。
    • 使用insert delay语句。
    • 使用用户定义函数。
    • 使用临时表。

redo logInnoDB使用重做日志来保证数据的完整性,重做日志只记录有关其本身的事务日志。InnoDB引擎的重做日志文件记录的是关于每个页的更改的物理情况。

写入重做日志的操作不是直接写,而是先写入一个重做日志缓冲中,然后按照一定的条件写入日志文件。

每个InnoDB引擎都有至少有一个重做日志组,每个组下面至少有两个重做日志文件。日志组中重做日志文件大小相等,并以循环的方式使用。InnoDB引擎会先写重做日志文件1,当达到文件的最后时,会切换至重做日志文件2,当重做日志文件2也写满时,会切换到重做日志文件1。

B+树:多路平衡树,所有的节点称为,也就是一个数据块,里面可以放数据,页是固定大小的,在InnoDB中页的大小是16KB,页里面的数据是一些key值,n个key可以划分为n+1个区间,每个区间有一个指向下级的指针,叶子节点之间以双向链表的方式连接,一个页中的key是有序的。

B+树的查找:先通过二分的方式查找key所在的区间,最后定位到叶子节点。对于数据库而言,查找一个key最终会定位到叶子节点,因为只有叶子节点才包含行记录或主键key

B+树

B+树的删除和插入:这个网站有详细的动画演示

  • 在插入节点时,可能存在页分裂的现象。页分裂是指当一个页容纳不了新的key时,分为多个页的过程。并非一个页插满就会发生页分裂现象,而是优先采用旋转的方式来调整,这样可以避免浪费空间。
  • 在删除节点时,可能存在页合并的现象。页合并是指当删除一个节点使得页的key的数量少到一定程度时与相邻的页合并在一起成为新的页。

B+树的优点:高扇出,也就是说一个页存放的数据多,这样的好处是树的高度较小,大概在2~4层,高度越小,查找的IO次数越少。

为什么要使用B+树?

  • 为什么不使用有序数组?

    有序数组可以通过二分的方式查找,时间复杂度为O(logN)。缺点是插入和删除的代价太大,例如:删除0位置的元素,需要移动1~n-1位置的数据,时间复杂度为O(N)。

  • 为什么不使用哈希表?

    InnoDB引擎本身是有使用哈希表的,只不过哈希表是由InnoDB自己建立的,我们无法建立。

    哈希表是一种查找效率很高的数据结构,基本上插入、删除、查询的时间复杂度可以看成O(1)。

    哈希表的底层是一个数组,插入数据时将hashcode与数组长度取模,确定数据在数组中位置,如果放入数据的位置被占用了,就称为哈希冲突或者哈希碰撞。此时可以采用拉链法解决。

    哈希表的缺点:

    • 对范围查询的支持不友好,如果要查找一个区间的数据,那么就要枚举这个区间的所有数据并计算出其对应的hashcode,去哈希表中查找。
    • 对排序不友好:哈希表中的数据是无序的。
  • 为什么不使用搜索二叉树?

    缺点:搜索二叉树的高度会随着节点数的增加而增加,因为数据库的索引是很大的,不可能直接装入内存中,那么查找的时候每往下一层就要读一次磁盘。读磁盘的效率是比较低的,因此需要减少读磁盘的次数,也就是减少树的高度。搜索二叉树当数据很多时,高度就会很高,那么磁盘IO次数就会很多,效率低下。

    另外,数据库是以页的形式存储的,InnoDB存储引擎默认一页16K,一页可以看成一个节点 ,二叉树一个结点只能存储一个一个数据.假如索引字段为int 也就是一个4字节的数字要占16k的空间,极大的浪费了空间。

聚集索引:按照每张表的主键构造一棵B+树,并且叶节点存放整张表的行记录数据,因此也让聚集索引的叶节点称为数据页。并且由于实际数据只能按照一棵B+树进行排序,因此每张表只能拥有一个聚集索引。

辅助索引:也称为非聚集索引,叶节点包含键的数据、相应行数据的聚集索引键。由于辅助索引存在不影响数据在聚集索引中的组织,所以一个表可以有多个辅助索引。因此当通过辅助索引来寻找数据时,InnoDB引擎会遍历索引并通过叶级别的指针来获得指向主键索引的主键,然后通过主键索引来找到一个完整的行记录。

顺序读、随机读与预读取:

顺序读:对于物理磁盘而言,是指顺序地读取磁盘上的块;在数据库中,顺序读是指根据索引的叶节点数据就能顺序地读取所需的行数据,这个顺序读只是逻辑地顺序读,在物理磁盘上可能还是随机读取,但是相对来说,物理磁盘上的数据还是比较顺序的,因为InnoDB是根据区来管理的,区是连续的64个页。

随机读:对于物理磁盘而言,是指访问的块是不连续的;在数据库中,一般是指访问辅助索引叶节点不能完全得到结果(没有覆盖索引),需要根据辅助索引叶节点中的主键去找实际行数据。因为一般来说,辅助索引和主键所在的数据段不同,因此访问是随机的。

预读取:是指通过一次IO请求将多个页预读取到缓冲池中,并且估计预读取的多个页马上会被访问(空间局部性规律)。

InnoDB中有两个预读取方法:

  • 随机预读取:是指一个区(64个连续页)中13个页也在缓冲区中,并在LRU列表前端(即页是被频繁访问的),则InnoDB引擎会将该区的所有页预读取到缓冲区。
  • 线性读取:基于缓冲池中页的访问模式,而不是数量。如果一个区中的24个页都被顺序访问了,则InnoDB引擎会读取下一个区的所有页。

锁:用于管理共享资源的并发访问。

锁的思维导图

InnoDB中的锁:

  • 锁的类型:

    • 共享锁:允许事务读取一行数据。
    • 排他锁:允许事务删除或者更新一行数据。

    锁兼容:当一个事务获取行 r 共享锁,那么另一个事务可以立即获取行 r 的共享锁。

    锁不兼容:如果此时有事务想获得行 r 的排他锁,那么它必须等待事务释放行 r 的共享锁。

    锁类型 X S
    X 冲突 冲突
    S 冲突 兼容
  • 意向锁:

    InnoDB引擎支持多粒度锁定,也就是说允许行级锁和表级锁同时存在。为了支持不同粒度上的加锁操作。InnoDB引擎支持一种额外的锁方式,我们称之意向锁

    即:意向锁也是一种表锁。并且意向锁是由InnoDB引擎维护,用户无法手动操作。在为数据行加共享 / 排他锁之前,InooDB会先获取该数据行所在在数据表的对应意向锁。

    意向锁的类型:

    • 意向共享锁(IS Lock):事务有意向获取表中某几行的共享锁。

      -- 事务要获取某些行的 S 锁,必须先获得表的 IS 锁。
      SELECT column FROM table ... LOCK IN SHARE MODE;
      
    • 意向排他锁(IX Lock):事务有意向获取表中某几行的排他锁。

      -- 事务要获取某些行的 X 锁,必须先获得表的 IX 锁。
      SELECT column FROM table ... FOR UPDATE;
      

    因为InnoDB引擎支持的是行级别的锁,所以意向锁不会阻塞除全表扫描以外的任何请求。也就是说意向锁是与行级锁兼容的。

  • 插入意向锁:

    插入意向锁是间隙锁(Gap Lock)的一种,它是专门针对insert操作的(基于索引)。当多个事务,在同一索引、同一范围区间插入记录时,如果插入位置不冲突时,不会阻塞彼此。

    参考文章:论 MySql InnoDB 如何通过插入意向锁控制并发插入

  • 一致性的非锁定读:

    InnoDB引擎通过多版本的方式来读取当前执行时间数据库中行的数据。如果读取的行正在执行deleteupdate 操作时,这时读取操作不会因此而等待行上的锁释放,而是去读取行的一个快照数据。并且在读已提交和可重复读两种隔离级别下,对快照数据的定义不同。

    快照数据的实现是通过Undo日志来实现的。

    参考文章:MySQL事务隔离级别和MVCC

  • 一致性读:

    即对读操作进行加锁,InnoDBselect语句支持的两种加锁操作:

    • select ... for update:对读取的行记录加一个X锁,其他事务想在这些行上加锁都会被阻塞。
    • select ... lock in share mode:对读取的行记录加一个S锁,其他事务可以向被锁定记录加S锁,但是加X锁,会被阻塞。
  • 自增锁:

    自增锁是一种特殊的表级别锁(table-level lock),专门针对事务插入AUTO_INCREMENT类型的列。最简单的情况,如果一个事务正在往表中插入记录,所有其他事务的插入必须等待,以便第一个事务插入的行,是连续的主键值。

    该锁不是在一个事务完成后才释放,而是在完成自增长值插入的SQL语句后立即释放

  • 外键和锁:

    InnoDB引擎中,对于一个外键列,如果没有显式对该列添加索引,那么InnoDB引擎会自动对其加一个索引。

    对于外键的插入或更新,首先需要查询父表中的记录,即select父表。但是对于父表的select操作,不是使用一致性的非锁定读,而是使用select ... lock in share mode方式,主动对父表加一个S锁。

  • 锁的算法:

    • Record Lock:单个行记录上的锁。
    • Gap Lock:间隙锁,锁定一个范围,但不包含记录本身。
    • Next-Key LockGap Lock + Record Lock,锁定一个范围,并且锁定记录本身。也有共享和互斥之分。

事务:要么什么都做修改,要么都不做。用于保证数据库的完整性。

事务的四个特性:ACID

  • 原子性(atomicity):指真个数据库事务是不可分割的工作单位。
  • 一致性(consistency):指数据库从一种状态转变为下一种一致的状态。即在事务开始前和事务结束后,数据库的完整性约束没有被破坏。
  • 隔离性(isolatoin):一个事务的影响在该事务提交前对其他事务都不可见。【通过锁来实现】
  • 持久性(durability):指事务一旦提交,其结果就是永久性的。即使发生宕机等故障,数据库也能将数据恢复。

原子性、持久性、一致性通过redo和undo来实现。

事务的四个隔离性:

  • READ UNCOMMITED:读未提交
  • READ COMMITED:读已提交
  • REPEATABLE READ:可重复读
  • SERIALIZABLE:串行化

redo

InnoDB存储引擎中,事务通过重做(redo)日志和日志缓冲区(InnoDB Log Buffer)实现。

当开始一个事务时,会记录该事务的一个LSN(Log Sequence Number,日志序列号);当事务执行时,会往日志缓冲里插入事务日志;当事务提交时,必须将日志缓冲写入磁盘。也就是在写数据前,先写日志,这种方式称为预写日志方式(Write-Ahead Logging,WAL)

InnoDB存储引擎通过预写日志的方式来保证事务的完整性。即磁盘上存储的数据页和内存缓冲池中的页是不同步的,对于内存缓冲池中页的修改,先是写入重做日志文件,在写入磁盘,因此这是一种异步的方式。

undo

undo存放在数据库内部的一个特殊段中,这称为undo段,undo段位于共享表空间。

undo只能将数据库逻辑地恢复到原来的样子,所有修改都被逻辑取消,但是数据结构本身在回滚后可能大不相同,因为在多并发系统中,可能会有数十、数百甚至数千个并发事务。数据库的主要任务就是协调对于数据记录的并发访问,如:一个事务在修改当前一个页中的某几条记录,但同时还有别的事务再对同一个页中另几条记录进行修改。因此,不能将一个页回滚到事务开始的样子,因为这会影响其他事务正在进行的工作。

为什么执行完rollback后,表空间没有减少?

因为,InnoDB存储引擎回滚时,它实际上做的是与之前相反的工作。对于每个insertInnoDB引擎会完成一个delete操作;对于每个delete,则完成一个insert操作;对于每个update,则完成一个相反的update操作。

MVCC和事务隔离性:
参考文章:MySQL事务隔离级别和MVCC

posted @ 2021-02-14 21:55  yghr  阅读(69)  评论(0)    收藏  举报