本文章将基于黑马程序员的 "Mysql数据库从入门到精通"教程 的视图部分进行归纳总结,如有侵权请联系删除!!
【MySQL事务与MVCC高并发核心!破解ACID与隔离级别终极指南】事务提交为何神秘回滚?MVCC如何实现读不加锁?本文手把手拆解InnoDB事务原理,揭秘版本链、ReadView机制如何平衡性能与一致性!详解4大隔离级别实战差异,直击幻读、脏读痛点。掌握这些底层逻辑,轻松应对电商秒杀、金融交易等高并发挑战,让你的数据库稳如磐石!🔥速戳提升架构师思维!
(一)事务原理
一,事务的特点
事务的四种特性主要可以分为两类:
- 原子性、持久性、一致性实际上都是由InnoDB存储引擎底层的两份日志来保障的,就是Redo Log与Undo Log
- 隔离性是由InnoDB存储引擎底层的锁机制与MVCC多版本并发控制来实现的
二、Redo Log重做日志
持久性实际上就是由Redo Log来保障的。
(1)使用Redo Log的具体操作流程
- 首先客户端在进行事务操作时会发起请求去操作mysql服务器,而在mysql服务器的InnoDB引擎当中分为内存结构与磁盘结构。磁盘结构中存放了很多数据文件;内存结构中存在一片Buffer Pool缓冲池区域,缓冲了一个个数据页的信息。
- 接下来当客户端发起了一次包含多条update与delete语句的事务操作,当执行update语句时首先它会去操作Buffer Pool缓冲池,查找有没有所更新的这一块的数据,若没有则会通过后台线程把数据从磁盘中读取出来(xxx.ibd),然后再缓存在Buffer Pool缓冲区当中,接下来就可以直接执行更新以及删除操作,直接去操作缓冲区当中的数据即可。
- 此时缓冲区当中的数据发生了变更,但是磁盘当中的数据并没有变化,此时这个数据页也就称之为脏页。这个脏页也就会在一定时机把脏页的数据通过后台线程刷新到磁盘当中,这时缓冲区与磁盘当中的数据就保持了一致。
- 假如脏页的数据在往磁盘中刷新时出错了,那么此时内存当中的数据没有刷新到磁盘当中,但是事务已经提交成功,这时数据的持久性就没有得到保障。
- Redo Log的作用
在Redo Log出现后,当我们对Buffer Pool缓冲区当中的数据进行增删改以后,它首先会把增删改的数据记录在Redo Log Buffer当中。在Redo Log Buffer中就会去记录数据页的物理变化,接下来再去提交事务时就会把Redo Log Buffer当中的数据页变化直接刷新到磁盘当中,并持久化地保存在磁盘文件中。解下来在进行脏页刷新时假装出错了,此时就可以通过Redo Log来进行数据的恢复。
(2)为什么每次提交事务时要把Redo Log中的数据刷新到磁盘当中而不是将Buffer Pool中变更的数据页刷新到磁盘
如果我们在每次提交时不使用Redo Log而是直接把Buffer Pool中数据刷新到磁盘文件中,那么就会存在严重的性能问题。因为在事务当中进行一组操作时,通常会操作多条记录,而这些记录都是随机去操作数据页的,这时就会涉及到大量的随机磁盘IO,性能较低;而如果在事务提交时使用到了Redo Log,就不会把脏页直接刷新,而是先把Redo Log文件进行异步刷新到磁盘中,由于它是log日志文件,日志文件都是追加的,那么此时它就是顺序磁盘IO,性能也就要高于随机磁盘IO。这种机制实际上叫做WAL(Write-Ahead Logging)先写日志。
- 如果脏页的数据顺利地刷新到磁盘当中了,那么此时Redo Log日志里所记录的数据变更实际上就不需要了,所以会每隔一段时机就去清理Redo Log日志。所以这两份日志是循环写的,并不会保留下来。
三、Undo Log回滚日志
(1)特点
Undo Log是用于解决事务的原子性的,事务原子性的实现就要依赖于Undo Log日志。
简单来说就是执行一条update语句时,Undo Log里面就会记录这条语句在更新之前长什么样。
MVCC多版本并发控制也需要依赖于Undo Log,找到它的历史版本。
Redo Log物理日志主要记录的是数据里面的内容是什么样子的,Undo Log逻辑日志主要记录的是每一步执行什么样的操作。
(2)主要作用
- 在事务执行失败进行回滚时,就需要依赖于Undo Log进行回滚。
- 使用MVCC多版本并发控制时也需要用到Undo Log。
(二)MVCC多版本并发控制
一、基本概念
(1)当前读
①在RR可重复读隔离级别下,普通select语句的运行情况
- 在客户端A与B中分别开启事务,并在A中查询表数据,在B中更新表数据,此时两边都能正常运行。
- 此时在A中再次查询表数据,发现数据并没有更新,因为B的事务还未提交。
- 当B的事务提交后在A中再次查询数据,发现数据还是没有更新。
- 为什么事务提交了还是查不到对应的数据
因为当前隔离级别是Repeatable Read可重复读,所以此时即便客户端B的事务提交了,客户端A中也还是查询不到的,这就保证了可重复读。也就意味着客户端A中的select语句并不是当前读,因为它只是普通的select语句。
②当前读演示
此时为select语句加上lock in share mode参数查询到的就是最新数据,这就是当前读。
- 当前读简单来说就是读取到的就是最新的数据记录。
(2)快照读
刚才即便客户端B中提交了事务,在客户端A中仍读取不到最新数据,就是因为正常的select语句实际上就是快照读,读取到的是历史版本。
对于当前这种Repeatable Read隔离级别,在第一条select语句才是产生快照读的地方,后续再查询时实际上查的是前面产生的这个快照数据。
如果隔离级别为Serializable串形化,那么此时快照读会退化为当前读,每一次读取操作都会加锁。
二、MVCC记录中的隐藏字段
当我们使用InnoDB引擎创建了带有三个字段的表结构后,它实际上还会为这张表再额外增加两个字段隐式字段DB_TRX_ID与DB_ROLL_PTR,而第三个隐式字段为DB_ROW_ID。
①隐藏字段含义
- TRX代表的是transaction,transaction id指的是最近修改这一行记录的事务的id。当我们进行记录插入或修改时,存储引擎就会自动为该DB_transaction_id去赋值。
- ROLL_PTR指的是poll pointer回滚指针。
- DB_ROW_ID隐藏主键并不是在每张表中都会自动生成的,只有当某一张表结构没有主键时才会自动生成一个隐式字段作为隐藏主键,如果这张表有主键,那么就不会出现隐藏字段。
②到磁盘文件中去查看创建的表是否具有这三个隐式字段
- 对于刚刚进行过操作的stu表的表空间文件,我们可以去查看它的表结构以及是否有生成对应的隐式字段。
在mysql中提供了一条指令,用于查看ibd文件当中的数据字典信息:ibd2sdi xxx.ibd,也可用于查看表空间文件。
在文件当中有非常多基本数据,这里我们只需要去关注columns表中字段即可
可以看到表结构当中显示的三个字段
继续往下走就能看到隐式字段DB_TRX_ID与DB_ROLL_PTR
但是并没有看到DB_ROW_ID隐藏主键,因为此时表结构当中存在主键字段id - 当创建一个没有主键的表结构后再去查看它的表空间文件,就能看到DB_ROW_ID隐藏主键字段,同时也存在另外两个隐式字段DB_TRX_ID与DB_ROLL_PTR。
这也就验证了在InnoDB引擎的表结构当中,对应的三个隐藏字段是存在的。
三、Undo Log回滚日志
(1)Undo Log版本链
当我们向表中插入一条数据后在表结构当中就会维护这样一个记录,由于是新插入的记录所以这里只有事务id没有回滚指针。
假设接下来在并发访问的情况下,有多个事务都需要来访问这条记录。
以下是各个事务执行的操作以及执行的时机:
- 事务2 3 4 5都是同时开启的,首先事务2要去修改这条记录。在修改记录之前InnoDB引擎会去记录一条Undo Log日志,用来进行数据回滚,里面记录了数据原来的样子。首先会把原来的数据复制一份放到Undo Log,前面记录了这条Undo Log日志的地址值,日志记录完毕之后事务2就会再去对记录执行更新操作。
age就会更新为3;DB_TRX_ID最近修改事务ID也会更新为2,因为记录的是当前执行最后一次操作的事务的id;DB_ROLL_PTR回滚指针就指向了下面的这条回滚日志。也就是说将来如果提交事务时出错了,要进行回滚,就要通过这个回滚指针来找到数据原来的样子(版本)。
- 紧接着执行事务3,在执行更新操作之前也是要记录原数据到Undo Log日志,接下来再更新表结构当中的记录。此时DB_ROLL_PTR回滚指针需要指向0x00002,而因为0x00002记录的上一个版本是0x00001,所以此时0x00002要指向下面的0x00001记录。
- 在前两个事务提交后Undo Log日志并不会立即删除,因为还有其他活动的事务正在用到这条Undo Log日志。
- 在事务4中要执行修改操作前首先要去记录Undo Log日志,然后更新表结构中的记录数据。
- 可以看到这样就形成了Undo Log的版本链,链表的头部就是表结构中的记录,链表的尾部也就是Undo Log日志中最旧的记录。
四、ReadView读视图
(1)readview介绍
在执行操作时到底要返回哪个版本的数据实际上不是由Undo Log日志的版本链来控制的,而是涉及到MVCC的readview组件。
m_ids参数中活跃的事务指的是还未提交的事务。
快照读读取的可能是Undo Log日志版本链的任意历史记录,而最后读取的是哪一版本是由readview来决定的,实际上就是依据于这四个核心字段。
(2)版本链数据访问规则
trx_id指的是当前Undo Log记录对应的当前事务id,在获取历史版本数据的时候就是拿当前事务id与刚才readview当中的四个属性值进行比对。
(3)在RC读已提交的隔离级别下具体访问的规则(主要去分析事务5的操作)
①生成ReadView
- 因为在RC隔离级别下事务中每次执行快照读都会生成一个ReadView,所以事务5在第一次执行select语句时会生成一个ReadView。因为在执行这条select语句时事务2已经提交了,而事务3 4 5都还在活跃,所以m_ids活跃事务集合内包含了事务3 4 5,min_trx_id最小事务id也就是3,max_trx_id预分配事务id为6(5+1),creator_trx_id也就是当前的事务5。
- 而在事务5中第二次执行select语句时,活动事务只有4与5了,最小事务为4,预分配事务依旧为6,当前事务依旧为5。
②规则匹配
当这一块的并发事务执行时会产生一个版本链,这里要结合四个访问规则来进行分析两个快照读应该读取哪一个历史数据。在套用时实际上就是带着该条数据记录与Undo Log日志当中的记录去一个一个地套用该四条规则。
- 第一个ReadView
比如首先拿着该条数据记录去比对,因为该条记录的DB_TRX_ID为4,不匹配所有规则,所以说明这次快照读查找的数据不是该条。然后就需要沿着版本链来查找其他记录(3,2,1)的匹配情况。
最后发现Undo Log日志当中事务id为2的记录可以匹配第二条条件trx_id < min_trx_id,代表当前这条记录的事务id小于最小活动事务的id,就说明当前这条记录已经提交了,此时说明这一次快照读找的就是这一个版本。就会把这个版本的记录直接返回。
因为事务2已提交了,而我们是RC读已提交,所以此时会把事务2的这条记录提取出来 - 第二个ReadView
首先把具体的四个参数标注在规则当中便于分析,并让四条记录依次进行匹配,最后发现事务id为3的记录满足规则2,那么就可以直接把该版本的数据直接返回
(4)在RR可重复读的隔离级别下具体访问的规则(主要去分析事务5的操作)
在事务5中进行第一次select语句的快照读时会生成一个ReadView,但是因为当前是在RR隔离级别之下,所以在进行第二次快照读时不会再生成一个ReadView,而是会复用先前的ReadView,也就是说两个ReadView都是相同的。而在RR可重复读隔离级别下,在同一事务中读取两条数据,它们的ReadView是相同的,它们的匹配规则也是一样的,所以在版本链当中查找出来的数据一会是一模一样的,这就保证了可重复读。