【MySQL】事务

发布于:2025-03-31 ⋅ 阅读:(18) ⋅ 点赞:(0)

在这里插入图片描述

零、CURD不加控制,会有什么问题?

MySQL是支持多线程的,我在多线程这篇文章中讲到过一个卖票的例子,票我们可以理解为共享资源,当多个线程同时访问这个资源并且不加保护的时候,就可能导致数据不一致的问题。

当抢票的为下面这种情况时,两个客户端都认为还有票,最终导致的结果就是数据库中票的数量为-1,但是实际上在生活中票所剩的数量不可能为负数。这就是CURD不加控制会产生的问题。
在这里插入图片描述

那么要如何控制CURD,才能解决上述问题呢?

  1. 买票的过程应该是原子的
  2. 不同客户端之间卖票不能够互相影响
  3. 当一个客户买完票后,它的票应该永久有效
  4. 买票钱和买票后的状态都要是确定的

一、事务是什么?为什么要有事务?

1.1 事务是什么?

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

事务就是要做的或所做的事情,主要用于处理操作量大,复杂度高的数据。举个例子,你使用微信给你的朋友转账100元,首先是你的余额减少100,然后就是你朋友的余额增多100。这个转账逻辑就需要两条SQL语句构成,那么这两条语句就构成了一个事物。

由于MySQL是支持多线程的,所以在一个数据库中,同时就可能有多个事务在同时运行,而每一个事务都至少有一条SQL语句,这么多的SQL语句同时访问同一个数据库中同一个表的数据,在不加以保护的情况下,必定会出现问题。如果说一个事务在执行一半的情况下不想继续执行,或是被迫中断执行了,那么之前执行的SQL语句应该怎么办呢?

所以一个完整事务不仅仅是SQL语句的集合,它还需要满足以下四条属性:

  • 原子性:一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
  • 一致性:在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。
  • 隔离性:数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交( Readuncommitted )、读提交( read committed )、可重复读( repeatable read )和串行化
    ( Serializable )
  • 持久性:事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。

上面四个属性:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability),我们可以简称为 ACID 。

事务就是在四大属性的加持下,由一条或多条MySQL语句构成的集合,在业务逻辑上完成某种具体的需求。


1.2 为什么要有事务?

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


二、事务的版本支持

在 MySQL 中只有使用了 Innodb 数据库引擎的数据库或表才支持事务, MyISAM 不支持。

show engines \G

在这里插入图片描述


三、事务提交方式

事务的提交方式默认有以下两种:

  1. 自动提交
  2. 手动提交

查看事务的提交方式:

show variables like 'autocommit';

在这里插入图片描述

我们可以使用set来改变自动提交模式:

--关闭自动提交
set autocommit=0;

在这里插入图片描述

--开启自动提交
set autocommit=1;

在这里插入图片描述


四、事务常见操作方式

  • 开始事务

    --二选一
    start transaction;
    begin;
    
  • 提交事务

    commit;
    
  • 设置保存点

    savepoint save1; --设置保存点save1
    
  • 回滚

    rollback to save1; --回滚到保存点save1
    rollback; --回滚到最开始
    

4.1 准备工作

通常来说,云服务器上的mysqld服务的端口号为3306。
在这里插入图片描述

将MySQL的默认隔离等级设置为读未提交(最低隔离等级),这里先简单使用以下,后面在详细讲解。在第四小节中演示时,MySQL的默认隔离等级设置都为读未提交

在这里插入图片描述
这里先准备一个测试表。

create table if not exists account(
	id int primary key,
	name varchar(50) not null default '',
	blance decimal(10,2) not null default 0.0
)ENGINE=InnoDB DEFAULT CHARSET=UTF8;

在这里插入图片描述


4.2 正常演示 - 证明事务的开始与回滚

在下面我启动了两台客户端,大家可以按照顺序看看对应的现象。

首先是两台客户端都启动了事务,我再客户端1中向表插入了两条数据,过后再设置了一个保存点,完后我在插入了强中强这条数据,通过客户端2确实查到了这三条数据。

然后当我使用回滚回到保存点save1后,在通过客户端2查看数据时,发现少了强中强这条数据。

最后我再通过客户端1使用回滚,回滚到事务最开始的时候,再用客户端2查看数据时,表中已经没有数据了。

这就证明了事务的开始与回滚的存在了

在这里插入图片描述

这里我再次进行测试,这里我再次启动了两台客户端,大家可以按照顺序看看对应的现象。

首先是两台客户端都启动了事务,然后我再客户端1中向表插入了两条数据,通过客户端2发现这两条数据确实存在,当我使两台客户端都提交事务以后,使客户端1进行回滚,通过客户端2发现这两条数据还在

证明当提交事务以后,就不能进行回滚了

在这里插入图片描述


4.3 非正常演示1 - 证明未commit,客户端崩溃,MySQL自动会回滚

这里我再次启动了两台客户端,大家可以按照顺序看看对应的现象。

首先是两台客户端都启动了事务,通过客户端1来看,表中已经存在了两条数据。

然后通过客户端1插入一条数据,再通过客户端2查看数据,发现这条数据确实插入进来了。

最后再客户端1事务未提交的情况下,客户端1崩溃了,再通过客户端2查看数据,发现那一条数据没了

证明当事务未提交时,客户端崩溃,MySQL自动会回滚

在这里插入图片描述


4.4 非正常演示2 - 证明commit了,客户端崩溃,MySQL数据不会再受影响

这里我再次启动了两台客户端,大家可以按照顺序看看对应的现象。

首先是两台客户端都启动了事务,通过客户端1来看,表中已经存在了两条数据,

然后通过客户端1插入一条数据,再通过客户端2查看数据,发现这条数据确实插入进来了。

最后再客户端1提交事务以后,客户端1崩溃了,再通过客户端2查看数据,发现那一条数据还在

证明当事务提交后,客户端崩溃,MySQL数据不会再受影响

在这里插入图片描述


4.5 非正常演示3 - 对比实验,用户手动开启事务,是否会受MySQL自动提交的影响

这里我做一次实验,下面第一张图是关闭自动提交的,第二张图是开启自动提交的,其余其他操作均相同。

这里我再次启动了两台客户端,大家可以按照顺序看看对应的现象。

首先是两台客户端都启动了事务,通过客户端1来看,表中已经存在了两条数据。

然后通过客户端1插入一条数据,再通过客户端2查看数据,发现这条数据确实插入进来了。

最后再客户端1事务未提交的情况下,客户端1崩溃了,再通过客户端2查看数据,发现那一条数据没了并且不管是开启还是关闭自动提交,结果都是相同的

证明用户手动开启事务,不会受MySQL自动提交的影响

在这里插入图片描述

在这里插入图片描述


4.6 非正常演示4 - 证明单条 SQL 与事务的关系

首先我们来看关闭自动提交时,并且未手动提交客户端崩溃时,单条 SQL 与事务的关系。

这里我再次启动了两台客户端,大家可以按照顺序看看对应的现象。

首先是两台客户端都启动了事务,通过客户端1来看,表中已经存在了三条数据。

然后通过客户端1插入一条数据,再通过客户端2查看数据,发现这条数据确实插入进来了。

最后再客户端1事务未手动提交的情况下,客户端1崩溃了,再通过客户端2查看数据,发现那一条数据没了

在这里插入图片描述

然后我们来看关闭自动提交时,然后手动提交后,客户端崩溃时,单条 SQL 与事务的关系。

这里我再次启动了两台客户端,大家可以按照顺序看看对应的现象。

首先是两台客户端都启动了事务,通过客户端1来看,表中已经存在了三条数据。

然后通过客户端1插入一条数据,再通过客户端2查看数据,发现这条数据确实插入进来了。

最后再客户端1事务手动提交的情况下,客户端1崩溃了,再通过客户端2查看数据,发现那一条数据还在
在这里插入图片描述

然后我们来看自动提交时,并且未手动提交客户端崩溃时,单条 SQL 与事务的关系。

这里我再次启动了两台客户端,大家可以按照顺序看看对应的现象。

首先是两台客户端都启动了事务,通过客户端1来看,表中已经存在了三条数据。

然后通过客户端1插入一条数据,再通过客户端2查看数据,发现这条数据确实插入进来了。

最后再客户端1事务未手动提交的情况下,客户端1崩溃了,再通过客户端2查看数据,发现那一条数据还在
在这里插入图片描述

通过上面三次实验可以得出:
实际上自动提交是作用在单条SQL语句上的,也就是说单条SQL语句就是一个事务。当用户关闭自动提交后,用户需要手动提交事务才能够使数据持久化,当用户开启自动提交后,用户不需要手动提交事务就能够使数据持久化。


4.7 结论和注意事项

结论

  • 只要手动开启事务(begin或者start transaction),事务便必须要通过手动提交事务(commit),才能是数据持久化,与是否设置自动提交(set autocommit)无关。
  • 事务可以手动回滚,同时,当操作异常,MySQL会自动回滚
  • 对于 InnoDB 每一条 SQL 语言都默认封装成事务,自动提交。
  • 从上面的例子,我们能看到事务本身的原子性(rollback),持久性(commit)

注意事项

  • 如果没有设置保存点,也可以回滚,只能回滚到事务的开始。直接使用 rollback,当然前提是事务还没有提交
  • 如果一个事务被提交了(commit),则不可以回退(rollback)
  • 用户可以选择回退到哪个保存点
  • InnoDB 支持事务, MyISAM 不支持事务

五、事务隔离级别

5.1 如何理解隔离性

  • MySQL服务可能会同时被多个客户端进程(线程)访问,访问的方式以事务方式进行
  • 一个事务可能由多条SQL构成,也就意味着,任何一个事务,都有执行前,执行中,执行后的阶段。而所谓的原子性,其实就是让用户层,要么看到执行前,要么看到执行后。执行中出现问题,可以随时回滚。所以单个事务,对用户表现出来的特性,就是原子性。
  • 但是,毕竟所有事务都要有个执行过程,那么在多个事务各自执行多个SQL的时候,就还是有可能会出现互相影响的情况。比如:多个事务同时访问同一张表,甚至同一行数据。
  • 就如同你妈妈给你说:你要学你就好好学,不想学就别学了。你中间的学习过程,你妈妈不关心。那么你的学习,对你妈妈来讲,就是原子的。那么你学习过程中,很容易受别人干扰,此时,就需要将你的学习隔离开,保证你的学习环境是健康的。
  • 数据库中,为了保证事务执行过程中尽量不受干扰,就有了一个重要特征:隔离性
  • 数据库中,允许事务受不同程度的干扰,就有了一种重要特征:隔离级别

5.2 隔离级别

  • 读未提交(Read Uncommitted): 在该隔离级别,所有的事务都可以看到其他事务没有提交的执行结果。相当于没有任何隔离性,也会有很多并发问题,如脏读,幻读,不可重复读等,上面为了做实验方便,用的就是这个隔离性。(实际生产中不可能使用这种隔离级别的)
  • 读提交(Read Committed) :该隔离级别是大多数数据库的默认的隔离级别(不是 MySQL 默认的)。它满足了隔离的简单定义:一个事务只能看到其他的已经提交的事务所做的改变。这种隔离级别会引起不可重复读,即一个事务执行时,如果多次 select, 可能得到不同的结果。
  • 可重复读(Repeatable Read): 这是 MySQL 默认的隔离级别,它确保同一个事务,在执行中,多次读取操作数据时,会看到同样的数据行。但是会有幻读问题。
  • 串行化(Serializable): 这是事务的最高隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决了幻读的问题。它在每个读的数据行上面加上共享锁,但是可能会导致超时和锁竞争(这种隔离级别太极端,实际生产基本不使用)。

5.3 查看与设置隔离级别

5.3.1 查看隔离级别

MySQL8.0以下版本:
:MySQL 8.0 中仍然可用以保持向后兼容性,但有些MySQL会有些其他问题,导致查看隔离级别出现问题。

select @@global.tx_isolation;  -- 全局隔离级别

select @@session.tx_isolation;  -- 当前会话隔离级别

select @@tx_isolation;  -- 当前会话隔离级别

MySQL8.0版本:

select @@global.transaction_isolation;  -- 全局隔离级别

select @@session.transaction_isolation;  -- 当前会话隔离级别

select @@transaction_isolation; -- 当前会话隔离级别

在这里插入图片描述


5.3.2 设置隔离级别

会话隔离级别实际上就是全局隔离级别的一份拷贝,当用户启动客户端时,本次会话的会话隔离级别会从全局隔离级别那里拷贝一份。之后用户可以改变本次会话的会话隔离级别。

SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL 
{READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE}
  • 当用户修改全局隔离级别时,并不会影响本次会话的会话隔离级别,只会影响之后用户再次启动客户端时的会话隔离级别。
    在这里插入图片描述

  • 当用户修改本次会话的会话隔离级别时,并不会影响全局隔离级别。
    在这里插入图片描述


5.4 读未提交(Read Uncommitted)

首先这里我启动了两台客户端,然后将两台客户端的隔离级别设置为读未提交,最后再在两台客户端上做一些操作,大家可以按照顺序看看对应的现象。

首先是两台客户端都启动了事务,然后我再客户端1中从表中删除果冻这条数据,通过客户端2确实看到这条数据被删除了。

然后我通过客户端1修改表中id为1370的数据为1375,再通过客户端2查看数据时,对应的id值确实发生了改变。

最后我再通过客户端1使用回滚,回滚到事务最开始的时候,再用客户端2查看数据时,表已经回到了事务开始的状态了。

这就是隔离级别中的读未提交,两个同时在执行,但一个事务的正在操作的结果,另一个正在执行的事务却能看到。

一个事务在执行中,读到另一个执行中事务的更新(或其他操作)但是未提交事务的数据,这种现象叫做脏读(dirty read)。

在这里插入图片描述


5.5 读提交(Read Committed)

首先这里我启动了两台客户端,然后将两台客户端的隔离级别设置为读提交,最后再在两台客户端上做一些操作,大家可以按照顺序看看对应的现象。

首先是两台客户端都启动了事务,然后我再客户端1中从表中删除果冻这条数据,但是通过客户端2却看不到这条数据被删除了。

然后我通过客户端1修改表中id为1370的数据为1375,再通过客户端2查看数据时,也没有看到对应的id值发生改变。

最后当客户端1提交事务后,再用客户端2查看数据时,却看到了客户端1对表的操作,注意此时客户端2并未提交事务

这就是隔离级别中的读提交,两个同时在执行,一个事务未提交时,其中操作的结果,另一个正在执行的事务是看不到,当一个事务提交后,它操作完的结果另一个未提交的事务可以看到。

同一个事务内,同样的读取,在不同的时间段(依旧还在事务操作中!),读取到了不同的值,这种现象叫做不可重复读(non reapeatable read)!!

在这里插入图片描述

那么读提交是问题吗??!!!是问题!!!

这里就来举一个例子:

假设某小学中一个班级中老师们已经批改完所有的卷子,并且已经把所有的成绩录入到了数据库中了,由于要放假了,语文老师需要按照成绩来给同学们布置作业。成绩与作业的对应关系为下图。

在这里插入图片描述

此时班主任拿着所有的卷子到教室后,将试卷分别发个了对应的同学,班级中有一个同学名为王中王,他拿到了语文老师批改完后的卷子,看到了自己的语文成绩为76分,而它有一位死对头名为强中强,强中强这位同学看到了王中王这位同学的卷子有一个选择题本该是错的,语文老师却改成对的了,强中强这位同学就告诉了班主任,班主任一看确实是错的,这时候班主任就回到办公室准备将王中王同学的成绩减少两分,此时语文老师正在另一个办公室按照成绩将所有同学划分出来,他进行了以下的操作。

正当语文老师查询完75 ~ 84分的同学后,班主任就更新了王中王这位同学的语文成绩,从76分改为了74分,然后语文老师又继续查询60 ~ 74分的同学,最后语文老师将同学对应要做的作业发布到了家长群中,同学们就回家放假了。

当王中王这位同学回家后,家长告诉他,他既要抄50首古诗,又要抄100首古诗,王中王同学当时就不开心了,打电话问语文老师为什么他要做两份作业,这时候语文老师就会感觉到疑惑,明明自己的操作是正确的,为什么会查到这样的结果呢?

从上面这个故事我们就可以看出读提交的问题了。

在这里插入图片描述


5.6 可重复读(Repeatable Read)

首先这里我启动了两台客户端,然后将两台客户端的隔离级别设置为可重复读,最后再在两台客户端上做一些操作,大家可以按照顺序看看对应的现象。

首先是两台客户端都启动了事务,然后我通过客户端1修改表中id为1375的数据为1370,通过客户端2查看数据时,也没有看到对应的id值发生改变。

然后我再通过客户端1向表中插入了一条数据,通过客户端2查看数据时,依旧没有看到表中插入了数据,当客户端1提交事务后,再用客户端2查看数据时,依旧没有看到了客户端1对表的操作。

最后当客户端2提交事务后,再用客户端2查看数据时,就看到了客户端1对表的操作。

这就是隔离级别中的可重复读,两个同时在执行,一个事务未提交时,其中操作的结果,另一个正在执行的事务是看不到,当一个事务提交后,其中操作的结果,另一个正在执行的事务是依旧是看不到,只有当剩下的这个未提交的事务提交后,它操作完的结果另一个未提交的事务可以看到。

一般的数据库在可重复读情况的时候,无法屏蔽其他事务insert的数据(因为隔离性实现是对数据加锁完成的,而insert待插入的数据因为并不存在,那么一般加锁无法屏蔽这类问题),会造成虽然大部分内容是可重复读的,但是insert的数据在可重复读情况被读取出来,导致多次查找时,会多查找出来新的记录,就如同产生了幻觉,这种现象,叫做幻读(phantom read)。

在这里插入图片描述


5.7 串行化(Serializable)

首先这里我启动了两台客户端,然后将两台客户端的隔离级别设置为串行化,最后再在两台客户端上做一些操作,大家可以按照顺序看看对应的现象。

首先是两台客户端都启动了事务,我先通过客户端1查询表中的数据,然后再通过客户端2删除表中name为果冻的这条数据,但是这条删除命令却卡住了

然后我通过客户端1修改表中id为1370的数据为1375,此时客户端2的那条删除命令还是卡住的状态,通过客户端1查看数据,看到了客户端1相关操作后的结果,此时客户端2的那条删除命令依旧是卡住的状态

直到客户端1提交了事务一瞬间,客户端2的那条删除命令一下子就完成了

当然,当两个客户端分别做着查询工作时,并不会卡住。

这就是隔离级别中的串行化,对所有操作修改表的操作全部加锁,进行串行化,不会有问题,但是只要串行化,效率很低,几乎完全不会被采用。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述


5.8 一致性(Consistency)

  • 事务执行的结果,必须使数据库从一个一致性状态,变到另一个一致性状态。当数据库只包含事务成功提交的结果时,数据库处于一致性状态。如果系统运行发生中断,某个事务尚未完成而被迫中断,而改未完成的事务对数据库所做的修改已被写入数据库,此时数据库就处于一种不正确(不一致)的状态。因此一致性是通过原子性来保证的。
  • 其实一致性和用户的业务逻辑强相关,一般MySQL提供技术支持,但是一致性还是要用户业务逻辑做支撑,也就是,一致性,是由用户决定的
  • 而技术上,通过AID保证C,也就是通过原子性、隔离性和持久性来保证一致性

5.9 总结

  • 其中隔离级别越严格,安全性越高,但数据库的并发性能也就越低,往往需要在两者之间找一个平衡点。
  • 不可重复读的重点是修改和删除:同样的条件,用户读取过的数据,再次读取出来发现值不一样了
  • 幻读的重点在于新增:同样的条件,第1次和第2次读出来的记录数不一样
  • 上面的例子可以看出,事务也有长短事务这样的概念。事务间互相影响,指的是事务在并行执行的时候,即都没有提交事务的时候,影响会比较大。
  • 注意: MySQL 默认的隔离级别是可重复读,一般情况下不要修改
    在这里插入图片描述

结尾

如果有什么建议和疑问,或是有什么错误,大家可以在评论区中提出。
希望大家以后也能和我一起进步!!🌹🌹
如果这篇文章对你有用的话,希望大家给一个三连支持一下!!🌹🌹

在这里插入图片描述