从概念到实战:手把手带你吃透 MySQL 事务核心原理

🔥海棠蚀omo个人主页

❄️个人专栏《初识数据结构》《C++:从入门到实践》《Linux:从零基础到实践》《Linux网络:从不懂到不会》《MySQL:新手入门指南》

追光的人,终会光芒万丈

博主简介:

目录

一.CURD操作不加控制,会有什么问题?

二.事务的概念

三.为什么会出现事务?

四.事务的版本支持

五.事务的提交方式

六.事务常见的操作方式

6.1正常演示:证明事务的开始与回滚

6.2非正常演示1:证明未commit,客户端崩溃,MySQL会自动回滚(隔离级别是:读未提交)

6.3非正常演示2:证明commit了,客户端崩溃,MySQL的数据不会再受影响,已经持久化

6.4非正常演示3:对比试验,证明begin操作会自动更改提交方式,不会受MySQL是否自动提交

6.5结论

七.事务隔离级别

7.1查看与设置隔离级别

[7.2读提交(Read Committed)](#7.2读提交(Read Committed))

[7.3可重复读(Repeatable Read)](#7.3可重复读(Repeatable Read))

7.4串行化(Serializable)

7.5总结

八.一致性

前言:

在上一篇文章中我们讲解了MySQL中的一个重要知识点:索引,索引大大提高了MySQL查询的效率,那么在了解完索引,今天这篇文章我们就来了解MySQL中另一个重要知识点:事务,事务是什么?事务又能解决什么问题?

这些问题的答案都将在这篇文章中揭晓,下面我们就一起来看看吧。

一.CURD操作不加控制,会有什么问题?

那么我们具体谈事务之前,我们先来思考一个问题:我们之前都在MySQL客户端的命令行中进行CURD操作,但是使用客户端进行CURD操作可不只有我们自己,还会有很多其他的客户端也在进行CURD操作,那么这会导致什么问题呢?

这里我以最常见的火车票售票系统来举例,上图中这种并发情况导致的结果我们也能够看到,后果还是比较严重的,那么针对这种问题,我们该怎么解决呢?或者说该怎么去约束CURD操作,才能解决上面的问题?

1.买票的过程得是原子的

2.买票互相不能影响

3.买完票应该永久有效

4.买前和买后都要是确定的状态

上面的这些针对CURD操作的约束就是经过总结后得出来的能够解决上面问题的方式。

二.事务的概念

那么事务到底是什么呢?

其实事务就是由一组DML语句组成的,这些语句在逻辑上存在相关性,这一组DML语句要么全部成功,要么全部失败,它们是一个整体。而MySQL提供了一种机制,保证我们可以达成这样的效果,事务还规定不同的客户端看到的数据是不相同的。

而事务要做的或者所做的事情,主要在于处理操作两大,复杂度高的数据。我们可以假设一种场景:你从学校毕业了,在学校的教务系统后台MySQL中,不再需要你的数据,要删除你的所有信息,那么就包括删除你的基本信息(姓名,电话,籍贯等),你的各科成绩,你在校的表现等内容,而要做到上面的这些事情,就需要执行多条sql语句来完成,那么这些操作合起来,就构成了一个事务!!!

而正如我们上面所说,一个MySQL数据库,可不只有一个事务在运行,同一时刻,甚至有大量的请求被包装成事务,在向MySQL服务器发起事务处理请求。而每条事务至少一条SQL,这样如果大家都访问同样的数据,在不加保护的情况下,就会出问题。

甚至,因为事务由多条sql语句组成,那么,也会存在执行到一半出错或者不想再执行的情况,那么已经执行的sql语句该怎么办呢?

所以,一个完整的事务,可不是简单的sql语句的集合,还要满足如下的四个属性

1.原子性:一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。

2.一致性:在事务开始之前和事务结束之后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度,串联性以及后续数据库可以自发性地完成预定的工作。

3.隔离性:数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交(Read uncommitted),读提交(read committed),可重复读(repeatable read)和串行化(serializable)。

4.持久性:事务处理结束后,对数据的修改就是永久的,即使系统故障也不会丢失。

对于上面的四种属性,原子性我们早在讲解Linux操作系统阶段就讲过其概念,简单理解就是操作只会有两种状态:完成与未完成,不会出现中间状态。

而最下面的持久性我们看了介绍也能理解,并不难理解,那么就剩下一致性和隔离性这两个属性了。

我们先说一致性,我们只看上面的介绍有点模棱两可,不知道具体表达的是什么意思,那么一致性简单来说就是当我们通过增删改等操作之后,我们就能预料到经过这些操作之后表中的数据的变化,并且最终表中数据的变化就是我们所预期的,而这就是一致性。

但是问题就来了,我们怎么保证经过这些操作之后表中数据的变化就是我们所预期的呢?或者说通过什么来保证一致性?

答案就是通过剩下的三种属性来保证,也就是一致性是建立在原子性,隔离性和持久性的基础上的,不过这样说是不完整的,后半部分我们下面再讲。

而剩下最后的隔离性是这里最难理解的,只靠上面的介绍是不够的,隔离性我们留在后面再讲。

三.为什么会出现事务?

其实事务被MySQL编写者设计出来的本质是为了当应用程序访问数据库的时候,事务能够简化我们的编程模型,不需要我们去考虑各种各样的潜在错误和并发问题,可以想象一下当我们使用事务时,要么提交,要么回滚,我们不会去考虑网络异常了,服务器宕机了的同时更改了一个数据该怎么办对吧?

因此事务本质上就是为了应用层服务的,而不是伴随着数据库系统天生就有的。

备注:我们后面把MySQL中的一行信息,称为一行记录。

四.事务的版本支持

在MySQL中只有使用了Innodb数据库引擎或表才支持事务,MyIsam不支持,那么怎么证明呢?下面我们可以通过一个指令来查看到底是不是真的如此:

通过show engines这个指令我们就可以看到MySQL所支持的存储引擎的相关信息,从上图中可以看到Innodb是支持事务的,其他的存储引擎都是不支持事务的,包括:MyIsam。

五.事务的提交方式

事务常见的提交方式有两种:

1.自动提交

2.手动提交

那么该如何查看当前事务的提交方式呢?我们来看:

通过上图中的指令我们就能查看当前事务的提交方式,而后面的Value显示ON,就代表现在事务是自动提交,那么如果我们想改成手动提交该怎么做呢?

我们可以通过set指令来将autocommit的值改为0,那么我们再次通过show指令来查看autocommit的值就可以发现此时的Value就变为了OFF,说明现在就变为了手动提交。

而我们在通过相同的指令将autocommit的值改为1,那么此时就变回自动提交了。

六.事务常见的操作方式

上面讲了那么多,下面我们就来看看事务具体应该怎么去操作:

那么首先呢为了便于演示我们先将事务的隔离级别设置为最低档次的读未提交(read-uncommitted)。

注:下面的操作中对表中数据做修改的是终端A,而通过select语句查看表中数据的是终端B。

那么通过上面的指令更改MySQL的隔离级别之后,我们是需要重启终端再来进行查看的,而通过上面的select语句我们就可以看到此时的隔离级别就变为:READ-UNCOMMITTED,即读未提交。

6.1正常演示:证明事务的开始与回滚

首先我们先创建一张表便于演示,并将存储引擎设置为Innodb以此来使用事务。

那么首先我们就要启动事务,指令:start transaction,或者:begin,这两个我更推荐使用begin来启动事务。

既然我们要演示事务的回滚,那么我们就再来认识一个指令:savepoint + 保存点名字,这条指令就是在创建一个保存点,用于定向回滚。

那么下面我们就向这张表中填入了几条数据,并在填入每条数据之后我们会创建出一个保存点。

那么下面我们就来演示一下事务的回滚操作:

回滚操作我们需要通过rollback指令,正因为我们之前创建了几个保存点,所以这里我们就可以通过rollback指令进行定向回滚,而上面我们回滚到了保存点save3,那么保存点save3所在的位置就是id = 3这条数据插入之前的阶段,那么在回滚之后我们再查看表中的数据就可以发现最后插入的id = 3的这条数据已经不在了。

那么回滚到之前的任何一个保存点都可以这样做,那么这里就有一个问题了:如果我在操作的期间没有创建保存点,该怎么去回滚呢?这种情况下又会回滚到那种程度呢?

那么我们就只能使用rollback;来进行回滚,并且这个回滚操作是直接回滚到事务刚启动时的状态,你在事务启动期间所做的所有操作都会失效,从上图可以看到,当我们这样做之后,表中一条记录也没有了!!!

6.2非正常演示1:证明未commit,客户端崩溃,MySQL会自动回滚(隔离级别是:读未提交)

那么这里我们重新向表中插入一些数据,可以看到表中此时是有数据的,那么下面就开始我们的表演:

那么这里我就是通过ctrl + \来使MySQL客户端直接退出,并没有commit,那么从上图我们可以看到再次通过select语句查看表中的数据的时候,表中已经没有数据了,这也就说明了,客户端崩溃,MySQL自动会进行回滚操作!!!

6.3非正常演示2:证明commit了,客户端崩溃,MySQL的数据不会再受影响,已经持久化

依旧是向表中插入两条数据,并通过select语句确认现在的表中确实有两条数据。

那么下面就开始我们的表演吧:

上面这三张图的顺序就是我们执行指令的顺序,可以看到当我们执行commit后,客户端崩溃,MySQL中的数据不会再受到影响,而这就证明了事务的持久性,而保证持久性的操作就是commit。

6.4非正常演示3:对比试验,证明begin操作会自动更改提交方式,不会受MySQL是否自动提交

那么通过上面这个例子相信我们大家就会有一个疑惑:我们不是设置了自动提交吗?为什么还要手动commit才能保证事务的持久性呢?

**先说结论:因为你手动进行了begin,即手动启动了事务,那么你就要手动commit,即手动提交,begin操作之后会自动修改提交方式,不会受MySQL是否自动提交。**其实我们通过上面的这些列子已经证明了这一点,不过下面我们专门针对这方面来进行一个验证:

可以看到现在的autocommit的Value是ON,也就是启动了自动提交,那么我们下面将其关闭:

现在已经将自动提交给关闭了,那么下面我们就重复和上面一样的操作,看会不会影响最终的结果:

此时我们又向表中插入了一条数据,再终端B中我们可看到表中确实已经多了一条数据,那么下面我们就开始验证:

从上图的结果中我们可以看到在未commit的前提下,客户端崩溃,MySQL会自动进行回滚,这个结果和我们之前开启自动提交的结果是一样的,间接说明了begin操作会自动更改提交方式,不会受MySQL自动提交的影响。

那么这时候就会有人你问了:那什么时候事务会受到自动提交的影响呢?

我们可以回想一下,我们在没有讲解事务之前,我们增加,删除,更新一张表中的数据,表中的数据会随着我们关闭客户端而进行回滚吗?

答案并不会吧,这不就做到了数据的持久性了吗?那么是怎么做到的呢?

答案就是在Innodb引擎中,每一条sql语句都会被默认封装成事务,并且默认是启动自动提交的,而MySQL的自动提交就是在这种情况下发挥作用的,相当于我们执行完一条sql语句,MySQL就会帮我们自动commit,所以才会有我们之前所看到的那些现象!!!

6.5结论

那么通过上面几个例子后,下面我们总结一下对于事务操作的结论:

1.只要输入begin或者start transaction,事务必须要通过commit提交,才会持久化,与是否设置set autocommit无关

2.事务可以手动回滚,同时,当操作异常时,MySQL会自动回滚

3.对于Innodb存储引擎,每一条sql语句都默认封装成事务,自动提交。(select有特殊情况,因为MySQL有MVCC)

4.在上面的例子中,我们能看到事物本身的原子性(回滚),持久性(commit)

事务操作注意事项:

1.如果没有设置保存点,也可以回滚,不过只能回滚到事务的开始,世界使用rollback(前提是事务还没有提交)

2.如果一个事务被提交了(commit),则不可以回滚(rollback)

3.可以选择回退到某个保存点(定向回滚)

4.Innodb支持事务,MyIsam不支持事务

5.开始事务可以使用start transaction或者begin

七.事务隔离级别

那么在讲解事务的隔离级别之前,我们要先弄清楚隔离性到底是什么意思,下面我们来看:

1.MySQL服务可能会被多个客户端进程(线程访问),访问的方式以事务方式进行

2.一个事务可能由多条sql语句构成,也就意味着,任何一个事务,都有执行前,执行中,执行后的阶段。而所谓的原子性,其实就是让用户层,要么看到执行前,要么看到执行后,执行中出问题,可以随时回滚,所以单个事务,对用户表现出来的特性,甚至同一行数据

3.但是,毕竟所有的事物都要有个执行过程,那么在多个事务各自执行多个sql语句的时候,就还是有可能会出现:多个事务同时访问同一张表,甚至同一行数据。

4.那么在这种情况下,各个事物之间的运行就可能会相互影响,所以在数据库中,为了保证事务执行过程中尽量不受干扰,就有了一个重要特征:隔离性。

5.如果说实现隔离性是目标,那么隔离级别就是实现这个目标的程度,或者说隔离级别就是控制事务之间所受影响的程度,而我们上面所提到的读未提交就是最低档的隔离级别,所以这种隔离级别对于实现隔离性的目标的程度是很低的。

那么通过上面的了解,我们对于隔离性和隔离级别有了一个大致的了解,那么下面我们就来看看具体都有哪些隔离级别:

1.读未提交(Read Uncommitted):在该隔离级别,所有的事务都可以看到其他事务没有提交的执行结果。(实际生产中不可能使用这种隔离级别的),就相当于没有任何隔离性,也会有很多并发问题,如脏读,幻读,不可重复读等,我们上面为了做实验方便,用的就是这个隔离性。

2.读提交(Read Committed):该隔离级别是大多数数据库的默认的隔离级别(不是 MySQL 默认的)。它满足了隔离的简单定义:一个事务只能看到其他的已经提交的事务所做的改变。这种隔离级别会引起不可重复读,即一个事务执行时,如果多次 select, 可能得到不同的结果。

3.可重复读(Repeatable Read):这是 MySQL 默认的隔离级别,它确保同一个事务,在执行中,多次读取操作数据时,会看到同样的数据行。但是会有幻读问题。

4.串行化(Serializable):这是事务的最高隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决了幻读的问题。它在每个读的数据行上面加上共享锁。但是可能会导致超时和锁竞争(这种隔离级别太极端,实际生产基本不使用)。

而读未提交的效果我们在上面的例子中已经看到效果了,终端A只要对数据进行操作,没有commit,终端B就能立刻就能看到表中数据的变化,所以对于读未提交这里就不再过多赘述了。

那么下面我们就来讲解后面3种隔离级别,看看他们是什么效果。

7.1查看与设置隔离级别

那么首先我们要能看到我们当前的隔离级别是什么,那么该如何做呢?

当前这个指令是用来查看全局的隔离级别的。

这两个指令则是用来查看当前会话的隔离级别的。

那么下面我们就来看看如何设置隔离级别:

那么就要用到set指令来进行操作,我们可以设置全局的隔离级别,也可以来设置当前会话的隔离级别,而在指令的最后面跟的就是我们要设置的隔离级别的名称,就上面那四个。

7.2读提交(Read Committed)

那么下面我们就来看看读提交这个隔离级别的效果:

那么首先我们就要先将隔离级别设置一下,并且设置之后要重新启动终端才会生效,这里我就直接将全局的隔离级别设置为读提交了。

那么下面我们就开始演示:

这里我们依旧启动两个终端A和B,这里我们就是通过终端B来查看此时表中的数据,可以看到目前就只有两条记录。

当我们在终端A种向表中新插入了一条记录后,在终端B中再次通过select指令来查看表中的数据,可以看到并没有显示新插入的记录,说明读提交的隔离级别此时就发挥作用了。

那么在终端A提交之后,在终端B就可以看到数据的更新了,但是,对于终端B而言,在同一个事务中,同样的读取,在不同的时间段,读取到了不同的值,那么这种现象就叫做不可重复读(non reapeatable read)!!!

不可重复读会导致的问题就是:同一个事务内,你依据刚才看到的数据做了一个决定,但真要去执行这个决定时,数据已经变了,导致你的操作基于过期的信息来执行的,那么就有可能出现问题。

7.3可重复读(Repeatable Read)

那么首先我们就要先将隔离级别切换为可重复读,那么可重复读相较于读提交又有哪些不同呢?下面我们就来看看:

终端A向表中插入一条记录,在终端Acommit之前,终端B看不到表中更新的内容,这没问题,本来可重复读就是在读提交的基础上控制更加严格,那么下面我们接着看:

区别到这里就出来了,终端A在commit之后,终端B依旧看不到表中新增的一条记录,那么问题就来了:终端B要怎么做才能看到终端A新增加的记录呢?

答案就是终端B也commit之后才能看到表中数据的变化。

那么可重复读的这种特性就解决了上面读提交因为不可重复读所导致的问题。但是,一般的数据库在可重复读的情况下,无法屏蔽其他食物insert的数据,因为隔离性实现是对数据加锁完成的, 而insert待插入的数据并不存在,那么一般加锁无法屏蔽这类问题,会造成虽然大部分内容是可重复读的,但是insert的数据在可重复读的情况下被读取出来,会导致多次查找时,多查找出来新的记录,就如同产生了幻觉。

而这种现象,就叫做幻读,但是很明显我们上面在实验的时候并没有出现幻读现象,那是因为MySQL在RR级别的时候,底层解决了幻读问题,具体是如何解决的,这就不是我们要讲的重点了。

7.4串行化(Serializable)

那么最后我们就来要来讲解隔离级别最高的串行化,有了上面对其他隔离级别的理解,再结合串行化这个名字,其实我们不难猜到它是如何发挥作用的:对所有操作全部加锁,事务的运行是串行的,和我们之前在解决多线程并发问题时的思路是一样的。

那么下面我们就来看看串行化相较于上面的可重复读又有什么不一样:

那么首先依旧是先切换隔离级别,注意串行化单词的拼写。

而当我们通过终端A想去执行delete操作时会发现程序卡在这了,这是为什么呢?

答案就是串行化这三个,既然要串行,那么就不能有多个事务并发运行,即一个事务想要对表中的数据进行插入,删除或者修改,就必须保证只有当前这一个事务在运行,指令才能成功的执行,当然,通过select语句来查看表中数据的操作是不会被阻塞的。

那么上面这句话其实也点明了我们如果想要上面的指令正常执行,就必须让终端B执行commit来结束当前这个事务,事实真是如此吗?我们来看:

可以看到在终端Bcommit之后终端A成功执行了这个指令,并且其实在终端Bcommit的一瞬间终端A就会完成这个指令,时间是很短的。

下面我们通过一张图来看看这中间的过程:

上面的图就很清晰明了地指出了整个执行的过程,原理就和我们处理多线程并发加锁是非常相似的,我们之前讲过多线程的并发问题,结合这部分内容我相信是比较好理解的。

7.5总结

那么在讲解完了上面的知识后,我们来总结一番:

1.其中隔离级别越严格,安全性越高,数据库的并发性能也就越低,往往需要在两者之间找一个平衡点(可重复读就是)。

2.不可重复读的重点是修改和删除:同样的条件,你读取过的数据,再次读取出来发现值不一样了;幻读的重点在于新增:同样的条件,第1次和第2次读出来的记录数不一样。

3.说明:MySQL默认的隔离级别是可重复读,一般情况下不要修改。

下面我们用一张图来看看各种隔离级别:

上图就列举出了我们上面讲过的各个隔离级别它们会导致的问题。

八.一致性

那么前面三种属性我们都讲过了,那么就剩最后的一致性我们没有谈,那么下面我们就讲一下:

1.事务的执行结果,必须是数据库从一个一致性状态,变成另一个一致性状态,当数据库只包含事务成功提交的结果时,数据库处于一致性状态。如果系统运行发生中断,某个事务尚未完成而被迫中断,而改未完成的事务对数据库所作的修改已经被写入数据库,此时数据库就出一种不正确(不一致)的状态。因此一致性是通过原子性来保证的。

2.但是,在技术上支持保证了原子性,隔离性和持久性,在用户的业务逻辑上也是需要保持的,比如:一个程序员要从一个账户中转走100元,但是他只写了该账户资金-100的sql语句,但是没有写转账目标账户+100的sql语句,那么导致的结果就是这100元不翼而飞了。

那么这你能说是MySQL的问题吗?

MySQL在技术上通过操作的原子性,隔离性和持久性保证了一致性,但是要想真正实现一致性还与用户的业务逻辑强相关,也就是一致性,是由用户决定的!!!

以上就是从概念到实战:手把手带你吃透 MySQL 事务核心原理的全部内容。

相关推荐
justjinji2 小时前
如何限制MongoDB副本集初始同步的网络带宽_maxSyncSourceLagSecs等参数
jvm·数据库·python
WangJunXiang62 小时前
NoSQL之Redis配置与优化
数据库·redis·nosql
u0109147602 小时前
CSS 中实现同类型兄弟元素悬停联动效果(如所有红色行同时高亮)
jvm·数据库·python
m0_640309302 小时前
MySQL如何备份非常大的数据库_mydumper多线程逻辑导出工具
jvm·数据库·python
m0_743623922 小时前
如何在Bootstrap中自定义Modal的弹出动画效果
jvm·数据库·python
2301_817672262 小时前
SQL如何实现分段式分组统计_使用CASE WHEN划分区间
jvm·数据库·python
qq_413847403 小时前
HTML怎么限制输入字符数_HTML input maxlength属性用法【详解】
jvm·数据库·python
liuyouzhang5 小时前
将基于Archery的web数据库审计查询平台封装为jdbc接口的可行性研究(基于AI)
前端·数据库
Meepo_haha8 小时前
配置 Redis
数据库·redis·缓存