逻辑存储结构
表空间(.ibd文件):一个表空间有多个段
段分为:
-
数据段:innoDB索引B+Tree的叶子节点
-
索引段:innoDB索引B+Tree的非叶节点
-
回滚段:内部包含1024个undo log segment(存放若干个undo log)
一个段有多个区,每个区1M,以区为资源申请单元,每次申请4-5个区。
每个区有64个连续的页(16K),页为管理的最小单元。
每个页有多个行,行内有字段,其中有三个隐藏字段:
-
DB_TRX_id:最后一次修改该记录的的事务ID(自增)。
-
DB_Roll_ptr:指向这条记录的上一个版本的指针,用于配合undo log指向上一个版 本。每一行都有,那么就变成了一条链表(undo log版本链(版本从新到旧)
- DB_Row_id:隐藏主键,如果表结构没有指定主键,将会生成该隐藏字段。
架构
内存架构
Buffer pool缓冲池
-
缓存操作的数据,以一定频率刷新到磁盘,从而减少io
-
以page为单位,底层采用链表管理Page,其中包含free page(空闲页),clean page(被使用未被修改的页),dirty page(脏页,被使用被修改,未更新回磁盘的页)
-
通常将多达80%的物理内存分配给缓冲池 。参数为innodb_buffer_pool_size;
change buffer更改缓冲区
-
对于非unique的二级索引
-
执行DML时
-
若数据不在buffer pool中,则将变更存在change buffer
-
当下次读取时,将修改合并到buffer pool中
自适应Hash
-
用于优化buffer pool,与其他buffer无关
-
innoDB会监控各个索引的查询,若发现hash能提速,则自动建立hash索引
log buffer
-
保存要写入磁盘的日志(redo log,undo log)
-
大小由参数innodb_log_buffer_size决定,默认16MB
-
刷新机制由参数innodb_log_buffer_at_trx_commit决定,0为每秒写入buffer并刷新到磁盘,1为每次commit后写入buffer并刷新到磁盘(实时写,实时刷),2为每次commit后写入buffer,每秒刷新到磁盘(实时写,延迟刷)
磁盘架构
system tablespace系统表空间
-
存change buffer
-
默认的文件名叫 ibdata1
file-pre-table tablespace“每个表一个文件”表空间
存innoDB数据和索引,每个数据表存一个.ibd文件
general tablespace公共表空间
-
需要手动创建
-
先建立表空间:create tablespace 表空间名 add datafile '文件名' engine = 引擎名
-
再在公共表空间建表:create table 表名 tablespace 表空间名
temporary tablespace临时表空间
存临时表等数据
undo tablespace
存undo log,默认自动创建两个大小相同的文件undo-001,undo-002
doublewrite buffer files双写缓冲区
从bufferpool来的数据会先写到双写缓冲区中,便于系统异常时恢复数据
redo log
-
用于实现事务持久性
-
该日志文件由两部分组成:redo log buffer(内存)和redo log(磁盘)。
-
当事务提交之后会把所有修改信息都会存到该日志中, 用于在刷新脏页到磁盘发生错误时, 进行数据恢复使用。
-
用两个文件循环写,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(即这条数据的这个版本是哪个事务修改的)
-
trx_id == creator_trx_id→ 能访问:这条数据是 “当前事务自己修改的”,所以自己能看(比如事务内修改了数据,之后查自己改的内容) -
trx_id < min_trx_id→ 能访问:trx_id比当时活跃事务的最小 ID 还小,说明这个修改数据的事务在 ReadView 生成前已经提交了,所以可以读。 -
trx_id > max_trx_id→ 不能访问:trx_id比当时 “下一个要分配的事务 ID” 还大,说明这个修改数据的事务是在 ReadView 生成之后才开启的,所以当前事务看不到它的修改。 -
min_trx_id ≤ trx_id ≤ max_trx_id→ 看m_ids:这个事务 ID 在 “当时活跃事务的 ID 范围” 里,此时要查m_ids(当时活跃事务的 ID 集合):-
如果
trx_id不在m_ids里:说明这个事务在 ReadView 生成后、当前判断前已经提交了,所以能访问; -
如果
trx_id在m_ids里:说明这个事务还在活跃(没提交),所以不能访问。
-
若当前trx_id不符合这4条规则,则沿着roll_ptr继续寻找,直到满足后返回那个版本