MySQL之事务与MVCC

参考资料:                                                                  书籍:《MySQL是怎样运行的》

                                                                                    视频:2022黑马程序员MySQL从入门到精通

事务简介

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

就比如: 张三给李四转账1000块钱,张三银行账户的钱减少1000,而李四银行账户的钱要增加 1000。 这一组操作就必须在一个事务的范围内,要么都成功,要么都失败。

 正常情况: 转账这个操作, 需要分为以下这么三步来完成 , 三步完成之后, 张三减少1000, 而李四 增加1000, 转账成功 :

 异常情况: 转账这个操作, 也是分为以下这么三步来完成 , 在执行第三步是报错了, 这样就导致张 三减少1000块钱, 而李四的金额没变, 这样就造成了数据的不一致, 就出现问题了。

为了解决上述的问题,就需要通过数据的事务来完成,我们只需要在业务逻辑执行之前开启事务,执行 完毕后提交事务。如果执行过程中报错,则回滚事务,把数据恢复到事务开始之前的状态。 

注意: 默认MySQL的事务是自动提交的,也就是说,当执行完一条DML语句时,MySQL会立即隐式的提交事务。 

事务的状态

活动的(active)
事务对应的数据库操作正在执行过程中时,我们就说该事务处在 活动的 状态。

部分提交的(partially committed)
当事务中的最后一个操作执行完成,但由于操作都在内存中执行,所造成的影响并没有刷新到磁盘时,我们就说该事务处在 部分提交的 状态。

失败的(failed)
当事务处在 活动的 或者 部分提交的 状态时,可能遇到了某些错误(数据库自身的错误、操作系统错误或者直接断电等)而无法继续执行,或者人为的停止当前事务的执行,我们就说该事务处在 失败的 状态。

中止的(aborted)
如果事务执行了半截而变为 失败的 状态,比如我们前边唠叨的狗哥向猫爷转账的事务,当狗哥账户的钱被扣除,但是猫爷账户的钱没有增加时遇到了错误,从而当前事务处在了 失败的 状态,那么就需要把已经修改的狗哥账户余额调整为未转账之前的金额,换句话说,就是要撤销失败事务对当前数据库造成的影响。书面一点的话,我们把这个撤销的过程称之为 回滚 。当 回滚 操作执行完毕时,也就是数据库恢复到了执行事务之前的状态,我们就说该事务处在了 中止的 状态。

提交的(committed)
当一个处在 部分提交的 状态的事务将修改过的数据都同步到磁盘上之后,我们就可以说该事务处在了 提交的 状态。

随着事务对应的数据库操作执行到不同阶段,事务的状态也在不断变化,一个基本的状态转换图如下所示:

 从图中大家也可以看出了,只有当事务处于提交的或者中止的状态时,一个事务的生命周期才算是结束了。对于已经提交的事务来说,该事务对数据库所做的修改将永久生效,对于处于中止状态的事务,该事务对数据库所做的所有修改都会被回滚到没执行该事务之前的状态。

事务四大特性

原子性(Atomicity):事务是不可分割的最小操作单元,要么全部成功,要么全部失败。

现实世界中转账操作是一个不可分割的操作,也就是说要么压根儿就没转,要么转账成功,不能存在中间的状态,也就是转了一半的这种情况。设计数据库的大叔们把这种要么全做,要么全不做的规则称之为 原子性 。但是在现实世界中的一个不可分割的操作却可能对应着数据库世界若干条不同的操作,数据库中的一条操作也可能被分解成若干个步骤(比如先修改缓存页,之后再刷新到磁盘等),最要命的是在任何一个可能的时间都可能发生意想不到的错误(可能是数据库本身的错误,或者是操作系统错误,甚至是直接断电之类的)而使操作执行不下去。为了保证在数据库世界中某些操作的原子性,设计数据库的大叔需要费一些心机来保证如果在执行操作的过程中发生了错误,把已经做了的操作恢复成没执行之前的样子。
 

一致性(Consistency):事务完成时,必须使所有的数据都保持一致状态。

比如我们现实世界中转账操作完成后,有一个 一致性 需求就是参与转账的账户的总的余额是不变的。如果数据库不遵循 原子性 要求,也就是转了一半就不转了,也就是说给狗哥扣了钱而没给猫爷转过去,那最后就是不符合一致性需求的;类似的,如果数据库不遵循 隔离性 要求,就像我们前边唠叨 隔离性 时举的例子中所说的,最终狗哥账户中扣的钱和猫爷账户中涨的钱可能就不一样了,也就是说不符合 一致性 需求了。所以说,数据库某些操作的原子性和隔离性都是保证一致性的一种手段,在操作执行完成后保证符合所有既定的约束则是一种结果。那满足 原子性 和 隔离性 的操作一定就满足一致性 么?那倒也不一定,比如说狗哥要转账20元给猫爷,虽然在满足 原子性 和 隔离性 ,但转账完成了之后狗哥的账户的余额就成负的了,这显然是不满足 一致性 的。那不满足 原子性 和 隔离性 的操作就一定不满足一致性 么?这也不一定,只要最后的结果符合所有现实世界中的约束,那么就是符合 一致性 的

隔离性(Isolation):数据库系统提供的隔离机制,保证事务在不受外部并发操作影响的独立 环境下运行。

我们将狗哥向猫爷同时进行的两次转账操作分别称为 T1 和 T2 ,在现实世界中 T1 和 T2 是应该没有关系的,可以先执行完 T1 ,再执行 T2 ,或者先执行完 T2 ,再执行 T1 ,对应的数据库操作就像这样:

 

 但是很不幸,真实的数据库中 T1 和 T2 的操作可能交替执行,比如这样:

 对于现实世界中状态转换对应的某些数据库操作来说,不仅要保证这些操作以 原子性 的方式执行完成,而且要保证其它的状态转换不会影响到本次状态转换,这个规则被称之为 隔离性 。

持久性(Durability):事务一旦提交或回滚,它对数据库中的数据的改变就是永久的。

当现实世界的一个状态转换完成后,这个转换的结果将永久的保留,这个规则被设计数据库的大叔们称为 持久性 。比方说狗哥向猫爷转账,当ATM机提示转账成功了,就意味着这次账户的状态转换完成了,狗哥就可以拔卡走人了。如果当狗哥走掉之后,银行又把这次转账操作给撤销掉,恢复到没转账之前的样子,那猫爷不就惨了,又得被砍死了,所以这个 持久性 是非常重要的。
当把现实世界的状态转换映射到数据库世界时, 持久性 意味着该转换对应的数据库操作所修改的数据都应该在磁盘上保留下来,不论之后发生了什么事故,本次转换造成的影响都不应该被丢失掉

事务操作

开启事务 —— 我们可以使用下边两种语句之一来开启一个事务:

mysql> BEGIN

mysql> START TRANSACTION

不过比 BEGIN 语句牛逼一点儿的是,可以在 START TRANSACTION 语句后边跟随几个 修饰符 ,就是它们几个:

READ ONLY :标识当前事务是一个只读事务,也就是属于该事务的数据库操作只能读取数据,而不能修改数据

READ WRITE :标识当前事务是一个读写事务,也就是属于该事务的数据库操作既可以读取数据,也可以修改数据

WITH CONSISTENT SNAPSHOT :启动一致性读

提交事务

开启事务之后就可以继续写需要放到该事务中的语句了,当最后一条语句写完了之后,我们就可以提交该事务了,提交的语句也很简单:

mysql> COMMIT;

手动中止事务

mysql> ROLLBACK

这里需要强调一下, ROLLBACK 语句是我们程序员手动的去回滚事务时才去使用的,如果事务在执行过程中遇到了某些错误而无法继续执行的话,事务自身会自动的回滚。

支持事务的存储引擎

MySQL 中并不是所有存储引擎都支持事务的功能,目前只有 InnoDB 和 NDB 存储引擎支持,如果某个事务中包含了修改使用不支持事务的存储引擎的表,那么对该使用不支持事务的存储引擎的表所做的修改将无法进行回滚

并发事务问题

赃读:一个事务读到另外一个事务还没有提交的数据。

比如B读取到了A未提交的数据。

不可重复读:一个事务先后读取同一条记录,但两次读取的数据不同,称之为不可重复读。

事务A两次读取同一条记录,但是读取到的数据却是不一样的。

幻读:一个事务按照条件查询数据时,没有对应的数据行,但是在插入数据时,又发现这行数据 已经存在,好像出现了 "幻影"。

事务隔离级别

为了解决并发事务所引发的问题,在数据库中引入了事务隔离级别。主要有以下几种:

脏写 是怎么回事儿?怎么里边都没写呢?这是因为脏写这个问题太严重了,不论是哪种隔离级别,都不允许脏写的情况发生。

注意:事务隔离级别越高,数据越安全,但是性能越低。

MySQL 的默认隔离级别为 REPEATABLE READ ,我们可以手动修改一下事务的隔离级别。

查看事务隔离级别

SELECT @@TRANSACTION_ISOLATION;

设置事务隔离级别

SET [ SESSION | GLOBAL ] TRANSACTION ISOLATION LEVEL { READ UNCOMMITTED |
READ COMMITTED | REPEATABLE READ | SERIALIZABLE }

事务的原理

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

而对于这四大特性,实际上分为两个部分。 其中的原子性、一致性、持久化,实际上是由InnoDB中的 两份日志来保证的,一份是redo log日志,一份是undo log日志。 而持久性是通过数据库的锁, 加上MVCC来保证的。

redo log

重做日志,记录的是事务提交时数据页的物理修改,是用来实现事务的持久性。

该日志文件由两部分组成:重做日志缓冲(redo log buffer)以及重做日志文件(redo log file),前者是在内存中,后者在磁盘中。当事务提交之后会把所有修改信息都存到该日志文件中, 用 于在刷新脏页到磁盘,发生错误时, 进行数据恢复使用。

如果没有redolog,可能会存在什么问题的? 我们一起来分析一下。

InnoDB 存储引擎是以页为单位来管理存储空间的,我们进行的增删改查操作其实本质上都是在访问页面(包括读页面、写页面、创建新页面等操作)。在真正访问页面之
前,需要把在磁盘上的页缓存到内存中的 Buffer Pool 之后才可以访问。但是在唠叨事务的时候又强调过一个称之为 持久性 的特性,就是说对于一个已经提交的事务,在事务提交后即使系统发生了崩溃,这个事务对数据库中所做的更改也不能丢失。但是如果我们只在内存的 Buffer Pool 中修改了页面,假设在事务提交后突然发生了某个故障,导致内存中的数据都失效了,那么这个已经提交了的事务对数据库中所做的更改也就跟着丢失了,这是我们所不能忍受的。

那么如何保证这个 持久性 呢?一个很简单的做法就是在事务提交完成之前把该事务所修改的所有页面都刷新到磁盘,但是这个简单粗暴的做法有些问题:

我们只是想让已经提交了的事务对数据库中数据所做的修改永久生效,即使后来系统崩溃,在重启后也能把这种修改恢复出来。所以我们其实没有必要在每次事务提交时就把该事务在内存中修改过的全部页面刷新到磁盘,只需要把修改了哪些东西记录一下就好,比方说某个事务将系统表空间中的第100号页面中偏移量为1000处的那个字节的值 1 改成 2 我们只需要记录一下:

将第0号表空间的100号页面的偏移量为1000处的值更新为 2 。

 有了redolog之后,当对缓冲区的数据进行增删改之后,会首先将操作的数据页的变化,记录在redo log buffer中。在事务提交时,会将redo log buffer中的数据刷新到redo log磁盘文件中。 过一段时间之后,如果刷新缓冲区的脏页到磁盘时,发生错误,此时就可以借助于redo log进行数据 恢复,这样就保证了事务的持久性。 而如果脏页成功刷新到磁盘 或 或者涉及到的数据已经落盘,此 时redolog就没有作用了,就可以删除了,所以存在的两个redolog文件是循环写的。

那为什么每一次提交事务,要刷新redo log 到磁盘中呢,而不是直接将buffer pool中的脏页刷新 到磁盘呢 ? 

因为在业务操作中,我们操作数据一般都是随机读写磁盘的,而不是顺序读写磁盘。 而redo log在 往磁盘文件中写入数据,由于是日志文件,所以都是顺序写的。顺序写的效率,要远大于随机写。 这 种先写日志的方式,称之为 WAL(Write-Ahead Logging)。

undo log

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

我们说过 事务 需要保证 原子性 ,也就是事务中的操作要么全部完成,要么什么也不做。但是偏偏有时候事务执行到一半会出现一些情况,比如:

这两种情况都会导致事务执行到一半就结束,但是事务执行过程中可能已经修改了很多东西,为了保证事务的原子性,我们需要把东西改回原先的样子,这个过程就称之为 回滚 (英文名: rollback ),这样就可以造成一个假象:这个事务看起来什么都没做,所以符合 原子性 要求。 

从上边的描述中我们已经能隐约感觉到,每当我们要对一条记录做改动时(这里的 改动 可以指 INSERT 、DELETE 、 UPDATE ),都需要留一手  把回滚时所需的东西都给记下来。比方说:

undo log和redo log记录物理日志不一样,它是逻辑日志。可以认为当delete一条记录时,undo log中会记录一条对应的insert记录,反之亦然,当update一条记录时,它记录一条对应相反的 update记录。当执行rollback时,就可以从undo log中的逻辑记录读取到相应的内容并进行回滚 

MVCC

MVCC:全称 Multi-Version Concurrency Control,多版本并发控制。指维护一个数据的多个版本, 使得读写操作没有冲突,快照读为MySQL实现MVCC提供了一个非阻塞读功能。MVCC的具体实现,还需 要依赖于数据库记录中的三个隐式字段、undo log日志、readView。

隐藏字段

当我们创建了上面的这张表,我们在查看表结构的时候,就可以显式的看到这三个字段。 实际上除了 这三个字段以外,InnoDB还会自动的给我们添加三个隐藏字段及其含义分别是:

而上述的前两个字段是肯定会添加的, 是否添加最后一个字段DB_ROW_ID,得看当前表有没有主键, 如果有主键,则不会添加该隐藏字段。

版本链

比方说我们的表 hero 现在只包含一条记录

 假设插入该记录的 事务id 为 80 ,那么此刻该条记录的示意图如下所示:

 假设之后两个 事务id 分别为 100 、 200 的事务对这条记录进行 UPDATE 操作,操作流程如下:

 能不能在两个事务中交叉更新同一条记录呢?哈哈,这不就是一个事务修改了另一个未提交事务修改过的数据,沦为了脏写了么?InnoDB使用锁来保证不会有脏写情况的发生,也就是在第一个事务更新了某条记录后,就会给这条记录加锁,另一个事务再次更新时就需要等待第一个事务提交了,把锁释放之后才可以继续更新。

每次对记录进行改动,都会记录一条 undo日志 ,每条 undo日志 也都有一个 roll_pointer 属性( INSERT 操作对应的 undo日志 没有该属性,因为该记录并没有更早的版本),可以将这些 undo日志 都连起来,串成一个链表,所以现在的情况就像下图一样:

对该记录每次更新后,都会将旧值放到一条 undo日志 中,就算是该记录的一个旧版本,随着更新次数的增多,所有的版本都会被 roll_pointer 属性连接成一个链表,我们把这个链表称之为 版本链 ,版本链的头节点就是当前记录最新的值。每个版本中还包含生成该版本时对应的 事务id。

ReadView

对于使用 READ COMMITTED 和 REPEATABLE READ 隔离级别的事务来说,都必须保证读到已经提交了的事务修改过的记录,也就是说假如另一个事务已经修改了记录但是尚未提交,是不能直接读取最新版本的记录的,核心问题就是:需要判断一下版本链中的哪个版本是当前事务可见的。为此,设计 InnoDB 的大叔提出了一个 ReadView 的概念,这个 ReadView 中主要包含4个比较重要的内容:

 而在readview中就规定了版本链数据的访问规则:

 如果某个版本的数据对当前事务不可见的话,那就顺着版本链找到下一个版本的数据,继续按照上边的步骤判断可见性,依此类推,直到版本链中的最后一个版本。如果最后一个版本也不可见的话,那么就意味着该条记录对该事务完全不可见,查询结果就不包含该记录

快照读

简单的select(不加锁)就是快照读,快照读,读取的是记录数据的可见版本,有可能是历史数据, 不加锁,是非阻塞读。

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

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

• Serializable:快照读会退化为当前读。

在 MySQL 中, READ COMMITTED 和 REPEATABLE READ 隔离级别的的一个非常大的区别就是它们生成ReadView的时机不同。

READ COMMITTED —— 每次读取数据前都生成一个ReadView

在事务5中,查询了两次id为30的记录,由于隔离级别为Read Committed,所以每一次进行快照读 都会生成一个ReadView,那么两次生成的ReadView如下。

那么这两次快照读在获取数据时,就需要根据所生成的ReadView以及ReadView的版本链访问规则, 到undolog版本链中匹配数据,最终决定此次快照读返回的数据。

先来看第一次快照读具体的读取过程:

 在进行匹配时,会从undo log的版本链,从上到下进行挨个匹配:

 再来看第二次快照读具体的读取过程:

 在进行匹配时,会从undo log的版本链,从上到下进行挨个匹配:

 REPEATABLE READ —— 在第一次读取数据时生成一个ReadView

RR隔离级别下,仅在事务中第一次执行快照读时生成ReadView,后续复用该ReadView。 而RR 是可 重复读,在一个事务中,执行两次相同的select语句,查询到的结果是一样的。

我们看到,在RR隔离级别下,只是在事务中第一次快照读时生成ReadView,后续都是复用该 ReadView,那么既然ReadView都一样, ReadView的版本链匹配规则也一样, 那么最终快照读返 回的结果也是一样的。

MVCC小结

所以呢,MVCC的实现原理就是通过 InnoDB表的隐藏字段、UndoLog 版本链、ReadView来实现的。 而MVCC + 锁,则实现了事务的隔离性。 而一致性则是由redolog 与 undolog保证。

所谓的 MVCC (Multi-Version Concurrency Control ,多版本并发控制)指的就
是在使用 READ COMMITTD 、 REPEATABLE READ 这两种隔离级别的事务在执行普通的 SEELCT 操作时访问记录的版本链的过程,这样子可以使不同事务的 读-写 、 写-读 操作并发执行,从而提升系统性能。 READ COMMITTD 、REPEATABLE READ 这两个隔离级别的一个很大不同就是:生成ReadView的时机不同,READ COMMITTD在每一次进行普通SELECT操作前都会生成一个ReadView,而REPEATABLE READ只在第一次进行普通SELECT操作前生成一个ReadView,之后的查询操作都重复使用这个ReadView就好了。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小颜-

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值