MYSQL-InnoDB存储引擎

34 阅读10分钟

逻辑存储结构

表空间(.ibd文件):一个表空间有多个段

段分为:

  1. 数据段:innoDB索引B+Tree的叶子节点

  2. 索引段:innoDB索引B+Tree的非叶节点

  3. 回滚段:内部包含1024个undo log segment(存放若干个undo log)

一个段有多个区,每个区1M,以区为资源申请单元,每次申请4-5个区。

每个区有64个连续的页(16K),页为管理的最小单元。

每个页有多个行,行内有字段,其中有三个隐藏字段:

  • DB_TRX_id:最后一次修改该记录的的事务ID(自增)。

  • DB_Roll_ptr:指向这条记录的上一个版本的指针,用于配合undo log指向上一个版 本。每一行都有,那么就变成了一条链表(undo log版本链(版本从新到旧)

image.png

  • DB_Row_id:隐藏主键,如果表结构没有指定主键,将会生成该隐藏字段。

架构

内存架构

Buffer pool缓冲池

  1. 缓存操作的数据,以一定频率刷新到磁盘,从而减少io

  2. 以page为单位,底层采用链表管理Page,其中包含free page(空闲页),clean page(被使用未被修改的页),dirty page(脏页,被使用被修改,未更新回磁盘的页)

  3. 通常将多达80%的物理内存分配给缓冲池 。参数为innodb_buffer_pool_size;

change buffer更改缓冲区

  1. 对于非unique的二级索引

  2. 执行DML时

  3. 若数据不在buffer pool中,则将变更存在change buffer

  4. 当下次读取时,将修改合并到buffer pool中

自适应Hash

  1. 用于优化buffer pool,与其他buffer无关

  2. innoDB会监控各个索引的查询,若发现hash能提速,则自动建立hash索引

log buffer

  1. 保存要写入磁盘的日志(redo log,undo log)

  2. 大小由参数innodb_log_buffer_size决定,默认16MB

  3. 刷新机制由参数innodb_log_buffer_at_trx_commit决定,0为每秒写入buffer并刷新到磁盘,1为每次commit后写入buffer并刷新到磁盘(实时写,实时刷),2为每次commit后写入buffer,每秒刷新到磁盘(实时写,延迟刷)

磁盘架构

system tablespace系统表空间

  1. 存change buffer

  2. 默认的文件名叫 ibdata1

file-pre-table tablespace“每个表一个文件”表空间

存innoDB数据和索引,每个数据表存一个.ibd文件

general tablespace公共表空间

  1. 需要手动创建

  2. 先建立表空间:create tablespace 表空间名 add datafile '文件名' engine = 引擎名

  3. 再在公共表空间建表:create table 表名 tablespace 表空间名

temporary tablespace临时表空间

存临时表等数据

undo tablespace

存undo log,默认自动创建两个大小相同的文件undo-001,undo-002

doublewrite buffer files双写缓冲区

从bufferpool来的数据会先写到双写缓冲区中,便于系统异常时恢复数据

redo log

  1. 用于实现事务持久性

  2. 该日志文件由两部分组成:redo log buffer(内存)和redo log(磁盘)。

  3. 当事务提交之后会把所有修改信息都会存到该日志中, 用于在刷新脏页到磁盘发生错误时, 进行数据恢复使用。

  4. 用两个文件循环写,ib_logfile0,id_logfile1

后台线程

后台线程中,分为4类,分别是:Master Thread 、IO Thread、Purge Thread、 Page Cleaner Thread。

Master线程

用于协调管理各个线程工作,并且负责一些数据的刷新到磁盘

IO线程

innoDB中有大量的AIO(异步IO)的线程:其中默认4个读线程,4个写线程,1个log,1个写change buffer

purge线程

用于回收事务提交了的undo log

page cleaner线程

用于刷新脏页

事务原理

事务 是一组操作的集合,它是一个不可分割的工作单位,事务会把所有的操作作为一个整体一起向系统提交或撤销操作请求,即这些操作要么同时成功,要么同时失败。

事务特性

ACID

  • A原子性:事务是一个不可分割的最小单位,要么同时成功,要么同时失败

  • C一致性:事务完成时,必须使所有数据都保持一致性状态

  • I隔离性:事务与事务之间相互隔离,保证事务在不受外部并发操作影响的独立环境下运行

  • D持久性:事务一旦提交,数据就被永久保存到磁盘

那实际上,我们研究事务的原理,就是研究MySQL的InnoDB引擎是如何保证事务的这四大特性的。

原子性、一致性、持久性

由InnoDB中的redo log和undo log来保证。其中采用的都是先写日志,后写数据的模式

redo log

该日志文件由两部分组成:重做日志缓冲(redo log buffer)以及重做日志文件(redo log file),前者是在内存中,后者在磁盘中。

当事务提交之后会把所有修改信息都会存到该日志中, 用于在刷新脏页到磁盘发生错误时, 进行数据恢复使用。

如果没有redo log?

脏页则会在一定的时机通过后台线程刷新到磁盘中,从而保证缓冲区与磁盘的数据一致。

而缓冲区的脏页数据并不是实时刷新的,假如刷新到磁盘的过程出错了,而提示给用户事务提交成功,而数据却没有持久化下来,这就出现问题了,没有保证事务的持久性。

redo log怎么解决这个问题?

当对缓冲区的数据进行增删改之后,会首先将操作的数据页的变化记录在redo log buffer中。

在事务提交时,会将redo log buffer中的数据刷新到redo log磁盘文件中

如果刷新缓冲区的脏页到磁盘时发生错误,此时就可以借助于redo log进行数据恢复,这样就保证了事务的持久性。

怎么回收redo log

如果脏页成功刷新到磁盘,此时redolog就没有作用了,就可以回收了,通过两个redo log文件循环写来回收。

为什么选择刷新redo log到磁盘,而不是直接将脏页刷回?

因为在业务操作中,我们操作数据一般都是随机读写磁盘的,而不是顺序读写磁盘。 而redo log是日志文件,所以都是顺序写的,顺序写的效率,要远大于随机写

这种先写日志的方式,称之为 WAL(Write-Ahead Logging)。

undo log

用于记录数据被修改前的信息,作用包含两个 : 提供回滚(保证事务的原子性) 和 MVCC(多版本并发控制) 。 

回滚

回滚日志,在insert、update、delete的时候产生的便于数据回滚的日志。

undo log和redo log记录物理日志不一样,它是逻辑日志(redo记数据,undo记操作)。可以认为当delete一条记录时,undo log中会记录一条对应的insert记录,反之亦然,当update一条记录时,它记录一条对应相反的update记录。

当执行rollback时,就可以从undo log中的逻辑记录读取到相应的内容并进行回滚。 

undo log销毁

insert的时候,产生的undo log日志只在回滚时需要,在事务提交后,可被立即删除

update、delete的时候,产生的undo log日志不仅在回滚时需要,在快照读时也需要,不会立即被删除。

隔离性

数据库事务的隔离性,主要通过 “锁机制(保障当前读)+ MVCC(保障快照读)” 共同实现,二者分工互补,覆盖不同读写场景。

MVCC多版本并发控制(提升并发性能的技术,通过维护数据的多个历史版本,让读写操作可以 “互不干扰”)

基本概念

当前读是读最新数据快照读是读历史版本数据

当前读

读取的是记录的最新版本读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁

对于我们日常的操作,如:select ... lock in share mode(共享锁),select ... for update、update、insert、delete(排他锁)都是一种当前读。

快照读

简单的select(不加锁)就是快照读,读取的是记录数据的可见版本,有可能是历史数据,基于 undo log 实现, 不加锁,是非阻塞读(提升并发)

快照读时机(Readview创建时机)

• Read Committed:每次select都生成一个快照读。

• Repeatable Read:开启事务后第一个select语句才是快照读的地方,后续复用。

简单记:RC 是 “语句级快照”,RR 是 “事务级快照”

加锁原因

1. 当前读加锁的原因

当前读的目标是获取数据的最新版本并确保操作的原子性。如果不加锁,可能出现 “一个事务刚读到最新数据,另一个事务就修改了它” 的情况,导致数据不一致(比如更新丢失)。所以必须加锁,保证操作期间数据不会被其他事务篡改。

2. 快照读不加锁的原因

快照读的目标是读取历史版本数据、提升并发效率,它本身不修改数据,只是查询。通过 MVCC 维护的历史版本(存在 undo log 里),快照读可以直接读历史版本,不用和写操作竞争锁,这样读写操作能并行执行,大幅减少阻塞,提升数据库的并发能力。

简单说:当前读要 “独占最新数据” 所以加锁;快照读要 “高效读历史数据” 所以不加锁。

MVCC原理(隐式字段、undo log、readView)

维护一个数据的多个版本, 使得读写操作没有冲突。快照读为MySQL实现MVCC提供了一个非阻塞读功能。

MVCC的具体实现还需要依赖于数据库记录中的三个隐式字段(上述有提到,配合undo log)、undo log(记录版本内容)、readView(该返回哪一个版本?)

readView

ReadView(读视图)是 快照读 SQL执行时MVCC提取数据的依据,记录并维护系统当前活跃的事务 (未提交的事务) id,

四个核心字段
  • m_ids:当前活跃的事务 ID集合

  • min_trx_id: 最小活跃事务 ID

  • max_trx_id: 预分配事务 ID,当前最大事务 ID+1 (因为事务 ID 是自增的)

  • creator_trx_id: ReadView创建者的事务ID(即当前查询语句自己这个事务)

版本链数据访问4条规则

trx_id表示当前查询的语句中的数据,其最近一次修改对应的事务ID(即这条数据的这个版本是哪个事务修改的)

  1. trx_id == creator_trx_id → 能访问:这条数据是 “当前事务自己修改的”,所以自己能看(比如事务内修改了数据,之后查自己改的内容)

  2. trx_id < min_trx_id → 能访问trx_id比当时活跃事务的最小 ID 还小,说明这个修改数据的事务在 ReadView 生成前已经提交了,所以可以读。

  3. trx_id > max_trx_id → 不能访问trx_id比当时 “下一个要分配的事务 ID” 还大,说明这个修改数据的事务是在 ReadView 生成之后才开启的,所以当前事务看不到它的修改。

  4. min_trx_id ≤ trx_id ≤ max_trx_id → 看m_ids:这个事务 ID 在 “当时活跃事务的 ID 范围” 里,此时要查m_ids(当时活跃事务的 ID 集合):

    • 如果trx_id不在m_ids:说明这个事务在 ReadView 生成后、当前判断前已经提交了,所以能访问;

    • 如果trx_idm_ids:说明这个事务还在活跃(没提交),所以不能访问。

若当前trx_id不符合这4条规则,则沿着roll_ptr继续寻找,直到满足后返回那个版本