1、逻辑存储结构:
表空间(ibd文件):一个Mysql实例可以对应多个表空间,用于存储记录、索引等数据。
段:分为数据段、索引段、回滚段、
InnoDB是索引组织表,数据段就是B+树的叶子节点,索引段就是B+树的非叶子节点,段用来管理多个(Extent)区
区:表空间中的单元结构,每个区的大小为1M,默认情况下,InnoDB存储引擎页大小为16K,即一个区中共有64个连续的页
页:是InnoDB存储引擎磁盘管理的最小单元,每个页大小16KB,为保证页的连续性,InnoDB存储引擎每次从磁盘申请4-5个区
行:InnoDB存储引擎是按行进行存放的
Trx_id:每次对某条记录进行改动时,都会把对应事务的ID赋值给 trx_id 隐藏列,也即是最后一次操作事务的ID
Roll_pointer:每次对某条记录进行改动时,都会把旧版本写入到 undo 日志中,然后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息
2、InnoDB-架构
2.1 内存结构
2.1.1 BufferPool:缓冲池
缓冲池是属于内存中的一个区域,里面可以缓存磁盘上经常操作的真实数据,在执行增删改查的时候会先操作缓冲池中的数据(如果此时缓冲区没有数据,则从磁盘加载并缓存),然后再以一定的频率或者规则刷新到磁盘,以减少磁盘的IO次数,加快处理速度。
如果没有缓冲区的话,每一次增删改查操作都会去操作磁盘空间,就会存在大量的磁盘IO,在业务中磁盘IO是随机IO,非常的耗时耗性能,所以需要尽量减少磁盘IO。
缓冲池的处理单位是页,底层采用的是链表数据结构来管理Page,根据状态,将Page分为三种类型:
- free page:空闲page,未被使用
- clean page:被使用page,数据没有被修改过
- dirty page:脏页,被使用page,数据被修改过,数据与磁盘中的数据不一致
2.2.2 Change Buffer:更改缓冲区
在执行DML语句时,如果这些数据page没有在Buffer Pool中,不会直接就去操作磁盘,而会将当前的数据变更存储在Change Buffer(更改缓冲区)中,在未来数据被读取的时候,再将数据合并恢复到Buffer Pool中,再将合并后的数据刷新到磁盘。
作用:每一次操作磁盘会造成大量的磁盘IO,有了ChangeBuffer之后,可以在缓冲池中进行合并处理,就会大量减少磁盘IO
2.2.3 Adaptive Hash Index:
自适应hash索引,用于优化对Buffer Pool(缓冲池)数据的查询,InnoDB会监控对表上各索引页的查询,如果发现Hash索引可以提升速度,则创建Hash索引。注意:自适应Hash索引,无需人工干涉,是系统根据情况自动完成。
自适应哈希索引有一个标志开关来设置是否启用:adaptive_hash_index.
2.2.4 Log Buffer:日志缓冲区
日志缓冲区,保存要写入磁盘中的Log日志数据(redo log、undo log),默认16MB,日志会定期刷新到磁盘中,如果需要更新、插入或删除许多行的事务,增加日志缓冲区大小可以节省磁盘IO
参数:
InnoDB_log_buffer_size:缓冲区大小、
InnoDB_flush_log_at_trx_commit:日志刷新到磁盘的时机(该参数有3个值:1、0、2)
1、每次事务提交刷新到磁盘
0、每秒日志写入并刷新到磁盘
2、每次事务提交后,并每秒刷新到磁盘
2.2 磁盘结构
2.2.1 System Tablespace:系统表空间,是内存结构中【更改缓冲区(Change Buffer)】的存储区域。参数:innodb_data_file_path
2.2.2 File-Per-Table Tablespace:每个表文件的表空间,包含单个InnoDB表的数据和索引,并存储在文件系统上的单个数据文件中。参数:innodb_file_per_table (默认开启)
2.2.3 General Tablespace:通用表空间,需要通过create TableSpace语法创建,创建表时可以指定。(相当于我们可以自己手动创建表空间,然后在新建表时指定到我们手动创建的表空间)
2.2.4 Undo Tablespace:撤销表空间,MySQL实例在初始化时会自动创建两个默认的undo表空间(默认16MB),用于存放undo log日志。
2.2.5 Temporary Tablespace:临时表空间,InnoDB会使用会话临时表空间和全局临时表空间,存储用户创建的临时表数据等。
2.2.6 Doublewrite Buffer Files:双写缓冲区,InnoDB引擎将数据页从Buffer Pool刷新到磁盘前,先将数据页写入双写缓冲区文件中,便于系统异常时恢复数据
2.2.7 Redo Log:重做日志,实现事务的持久性,由重做日志缓冲(redo buffer)和重做日志文件(redo log)组成,前者是在内存中,后者在磁盘中。当事务提交后,会把所有修改信息都放到该日志中,用于刷新脏页到磁盘时发生错误,进行数据恢复。
2.3 后台线程
作用:将InnoDB缓冲池中的数据在合适的时机刷新到磁盘文件中。
2.3.1 Master Thread
核心后台线程,负责调度其他线程,还负责将缓冲池中的数据异步刷新到磁盘中,保持数据的一致性,还包括脏页的刷新、合并插入缓存、undo页的回收。
2.3.2 IO Thread
在InnoDB存储引擎中大量使用了AIO来处理IO请求,这样可以极大地提高数据库的性能,而IO Thread主要负责这些IO请求的回调
2.3.3 Purge Thread
主要用于回收事务已经提交了的undo Log,在事务提交之后,undo Log可能不用了,就用它来回收
2.3.4 Page Cleaner Thread
协助 Master Thread 刷新脏页到磁盘的线程,它可以减轻Master Thread 的工作压力,减少阻塞
3、事务原理
事务是一组操作的集合,它是一个不可分割的工作单位,事务会把所有的操作作为一个整体一起向系统提交或撤销操作请求,这些操作要么同时成功,要么同时失败
1.原子性(Atomicity) 一个事务必须被视为一个不可分割的最小单元, 整个事务中的所有操作要么全部提交成功, 要么全部失败,对于一个事务来说,不可能只执行其中的一部分操作
2.一致性(Consistency) 如果在执行事务之前数据库是一致的,那么在执行事务之后数据库也还是一致的;
3.隔离性(Isolation) 事务操作之间彼此独立和透明互不影响。事务独立运行。这通常使用锁来实现。 一个事务处理后的结果,影响了其他事务,那么其他事务会撤回。 事务的100%隔离,需要牺牲速度。
4.持久性(Durability) 事务一旦提交,其结果就是永久的。即便发生系统故障,也能恢复。
原子性、一致性、持久性由redolog 和undo log 控制完成
隔离性由锁和MVCC来控制完成
—> 持久性由redo log保证
Redo Log:重做日志,实现事务的持久性,由重做日志(redo buffer)和重做日志(redo log)组成,前者是在内存中,后者在磁盘中,事务提交后,会把所有修改信息都放到该日志中,用于刷新脏页到磁盘时发生错误,进行数据恢复。
说明:图中说明的是当一个事务被提交之后在InnoDB中是怎么的处理机制来保证事务的持久性,首先事务提交之后,会到内存结构中的Buffer Pool中对应的数据页的进行数据修改等操作,完成操作后,此时在内存中事务已经执行完成,而并没有及时刷新到磁盘中,当前内存中这个被更新的数据页叫做脏页,而在Buffer Pool中进行操作时,在内存区中的Redolog Buffer中会记录所有的操作,然后由后台线程定期刷新到磁盘中的Redo Log中,然后在Buffer Pool中的数据刷新到磁盘中发生错误时,可以通过磁盘中的Redo Log进行数据的恢复。当Buffer Pool数据正确同步到磁盘后,磁盘中的Redolog也就没有用了,所以在磁盘中的两个redolog通过互相复制来达到及时更新。 这种先写日志,后同步数据的做法叫WAL(Write-Ahead Log)
那为什么要多此一举,先写到Redolog Buffer中,再传到Redo Log中呢,直接每次事务之后将数据从Buffer Pool中刷新到磁盘不就行了嘛?这里存在一个问题,事务中对数据页的操作绝大部分时候是随机的,如果每次事务都立即刷新到磁盘中的话,会产生多次的磁盘IO,这将会很耗费性能,所以我们通过Redolog这样来保证数据的持久性。
—> 原子性由undo log保证
undo log:回滚日志,用于记录数据被修改前的信息,作用包含:提供回滚和MVCC(多版本控并发制)
undo log 和 redo log 记录物理日志不一样,它是逻辑日志,可以认为当delete一条记录时,undo log中会记录一条对应的insert记录,反之亦然,当update一条记录时,会记录一条对应相反的update记录,执行rollback时,就可以从undo log中的逻辑记录读取到相应内容并回滚。
undo log 销毁:在事务执行时产生,事务提交时,并不会立即删除undo log,这些日志可能还用于MVCC。
undo log 存储:采用段的方式进行管理和记录,存放在rollback回滚段中,内部包含1024个undo log segment。
4、MVCC
4.1 概 念
什么是MVCC?
MVCC是在并发访问数据库时,通过对数据做多版本管理,避免因为写数据时要加写锁而阻塞读取数据的请求,造成写数据时无法读取数据的问题。
通俗的讲就是MVCC通过保存数据的历史版本,根据比较数据的版本号来决定数据的是否显示,在不需要加读锁的情况就能达到事务的隔离效果,最终可以在读取数据的时候可以同时进行修改,修改数据时候可以同时读取,极大的提升了事务的并发性能。
4.2 InnoDB MVCC实现的核心知识点
4.2.1 事务版本号
每个事务开启前都会从数据库获得一个自增长的事务ID,可以从事务ID判断事务的执行先后顺序。
4.2.2 表的隐藏列
DB_TRX_ID | 记录操作该数据事务的事务ID; |
DB_ROLL_PTR | 指向上一个版本数据在undo log 里的位置指针; |
DB_ROW_ID | 隐藏ID ,当创建表没有合适的索引作为聚集索引时,会用该隐藏ID创建聚集索引; |
4.2.3 Undo Log
Undo Log 主要用于记录数据被修改之前的日志,在表信息修改之前先会把数据拷贝到undo log 里,当事务进行回滚时可以通过undo log 里的日志进行数据还原。
Undo Log的用途
(1)保证事务进行rollback时的原子性和一致性,当事务进行回滚的时候可以用undo log的数据进行恢复。
(2)用于MVCC快照读的数据,在MVCC多版本控制中,通过读取undo log的历史版本数据可以实现不同事务版本号都拥有自己独立的快照数据版本。
4.2.4 事务版本号、表格的隐藏列、undo log的关系
我们通过一次数据修改的模拟过程来理解事务版本号、隐藏列、undolog之间的使用关系
(1)首先准备一张原始数据表
(2)开启一个事务A:对user_info表执行 update user_info set name =“李四”where id=1 会进行如下流程操作
1、首先获取一个事务编号104 |
2、把user_info表修改前的数据拷贝到undo log |
3、修改user_info表 id=1的数据 |
4、把修改后的数据事务版本号改成 当前事务版本号,并把DB_ROLL_PTR 地址指向undo log数据地址。 |
(3) 最后执行完的结果如图
4.2.5 undolog 版本链
不同事务或相同事务对同一条记录进行修改,会导致该记录的undo log生成一条记录版本链表,该链表的头部是最新的旧记录,链表的尾部是最早的旧记录。
4.2.6 Read View
在InnoDB中每个事务开启后都会得到一个(Read view)。副本主要保存了当前数据库系统中正处于活跃(没有commit)的事务的ID号,其实简单的说这个副本中保存的是系统中当前不应该被本事务看到的其他事务id列表。(当每个事务开启时,都会被分配一个 ID , 这个 ID 是递增的,所以最新的事务,ID 值越大)
所以我们知道 Read View 主要是用来做可见性判断的, 即当我们某个事务执行快照读的时候,对该记录创建一个 Read View 读视图,把它比作条件用来判断当前事务能够看到哪个版本的数据,既可能是当前最新的数据,也有可能是该行记录的undo log里面的某个版本的数据。
Read View遵循一个可见性算法,主要是将要被修改的数据的最新记录中的 DB_TRX_ID(即当前事务 ID )取出来,与系统当前其他活跃事务的 ID 去对比(由 Read View 维护),如果 DB_TRX_ID 跟 Read View 的属性做了某些比较,不符合可见性,那就通过 DB_ROLL_PTR 回滚指针去取出 Undo Log 中的 DB_TRX_ID 再比较,即遍历链表的 DB_TRX_ID(从链首到链尾,即从最近的一次修改查起),直到找到满足特定条件的 DB_TRX_ID , 那么这个 DB_TRX_ID 所在的旧记录就是当前事务能看见的最新老版本
Read View的几个重要属性:
m_ids: 当前系统活跃(未提交)事务版本号集合 |
min_trx_id: 最小活跃事务ID |
max_trx_id: 预分配事务ID,当前最大事务ID+1(因为事务ID是自增的) |
creator_trx_id: 创建当前read view的事务版本号 |
Read view 匹配条件:
1、数据事务ID==creator_trx_id
成立的话可以访问该版本,说明数据是当前这个事务更改的
2、数据事务ID <min_trx_id 则显示
如果数据事务ID小于read view中的最小活跃事务ID,则可以肯定该数据是在当前事务启之前就已经存在了的,所以可以显示。
3、数据事务ID>=max_trx_id则不显示
如果数据事务ID大于read view 中的当前系统的最大事务ID,则说明该数据是在当前read view 创建之后才产生的,所以数据不予显示。
4、min_trx_id<=数据事务ID<max_trx_id 则与活跃事务集合trx_ids里匹配
如果数据的事务ID大于最小的活跃事务ID,同时又小于等于系统最大的事务ID,这种情况就说明这个数据有可能是在当前事务开始的时候还没有提交的。
所以这时候我们需要把数据的事务ID与当前read view 中的活跃事务集合trx_ids 匹配:
情况1: 如果事务ID不存在于trx_ids 集合(则说明read view产生的时候事务已经commit了),这种情况数据则可以显示。
情况2: 如果事务ID存在trx_ids则说明read view产生的时候数据还没有提交,但是如果数据的事务ID等于creator_trx_id ,那么说明这个数据就是当前事务自己生成的,自己生成的数据自己当然能看见,所以这种情况下此数据也是可以显示的。
情况3: 如果事务ID既存在trx_ids而且又不等于creator_trx_id那就说明read view产生的时候数据还没有提交,又不是自己生成的,所以这种情况下此数据不能显示。
5、不满足read view条件时候,从undo log里面获取数据
当数据的事务ID不满足read view条件时候,从undo log里面获取数据的历史版本,然后数据历史版本事务号回头再来和read view 条件匹配 ,直到找到一条满足条件的历史数据,或者找不到则返回空结果;
4.3 InnoDB实现MVCC的原理
4.3.1 模拟MVCC实现流程
下面我们通过开启两个同时进行的事务来模拟MVCC的工作流程。
(1)创建user_info表,插入一条初始化数据
(2) 事务A和事务B同时对user_info进行修改和查询操作
事务A:update user_info set name =”李四”
事务B:select * fom user_info where id=1
问题:
先开启事务A ,在事务A修改数据后但未进行commit,此时执行事务B。最后返回结果如何。
执行流程如下图:
上图执行流程说明:
1、事务A:开启事务,首先得到一个事务编号102;
2、事务B:开启事务,得到事务编号103;
3、事务A:进行修改操作,首先把原数据拷贝到undolog,然后对数据进行修改,标记事务编号和上一个数据版本在undo log的地址。
4、事务B: 此时事务B获得一个read view ,read view对应的值如下
5、事务B: 执行查询语句,此时得到的是事务A修改后的数据
6、事务B: 把数据与read view进行匹配
发现不满足read view显示条件,所以从undo lo获取历史版本的数据再和read view进行匹配,最后返回数据如下。
4.4 快照读和当前读
当前读:
读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。对于日常操作,如:select ... lock in share mode(共享锁),select ... for update、update、insert、delete(排它锁)都是一种当前读
快照读:
简单的select(不加锁)就是快照读,快照读读取的是记录数据的可见版本,有可能是历史数据,不加锁是非阻塞锁
Read Committed:每次select,都生成一个快照读
Repeatable Read:开启事务后第一个select语句才是快照读的地方
Serializable:快照读会退化为当前读
数据库并发场景有三种:
读-读:不存在任何问题,也不需要并发控制
读-写:有线程安全问题,可能会造成事务隔离性的问题,可能遇到脏读,幻读,不可重复读
写-写:有线程安全问题,可能会存在更新丢失的问题,比如第一类