MySQL事务管理

一、事务的理解

假设我们在开发过程中有这么一个业务需求,需要我们写一个转账逻辑。比如张三要给李四转账50块钱,我们在数据库层面执行的语句应该是三条,第一条是查找张三的账户余额是否大于或等于50,如果是的话才能够转账;第二条是将张三的账户余额减去50;第三条是将李四的账户余额增加50。也就是说,在我们上层看来一个简单的转帐逻辑,可能对应后端需要做很多工作,这些工作组合起来才是一个完整的需求解决方案。

并且这三条语句的执行工作必须是一个整体的,假设我们刚执行完第二条语句,张三的账户余额刚好减了50服务器就崩了,李四的账户余额增加50这条语句执行失败了。那么这个时候就很尴尬了,张三的钱扣了,却没有到李四的账户里。而且有可能不只是张三和李四,可能这个时间段很多人转账也出现了这个问题,这样就乱套了。

所以必须把上面的三条语句当成一个整体去执行,要么全部执行完,要么一条也不执行,绝对不能出现执行完部分最后失败的中间状态。我们把这三条语句当成一个整体,这个整体其实就是事务的概念,就是一个或者多个SQL语句的集合。事务本身并不是数据库类软件天然就有的,事物本质工作其实是为了简化程序员工作的模型,现在市面上主流的数据库软件一般都会提供事务管理机制。

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

事务就是要做的或所做的事情,主要用于处理操作量大,复杂度高的数据。假设有一种场景:你毕业了,学校的教务系统后台MySQL中不再需要你的数据了,要删除你的所有信息。那么要删除你的基本信息比如姓名、电话、籍贯等,也要删除和你相关的其它信息比如各科成绩、在校表现等等。这样就需要多条MySQL语句构成,那么所有这些操作合起来,就构成了一个事务。

一个MySQL数据库可不止一个事务在运行,同一时刻甚至有大量的请求被包装成事务,在向MySQL服务器发起事务处理请求。而每条事务至少一条SQL语句,最多有很多SQL语句,这样如果大家都访问同样的表数据,在不加保护的情况下,就绝对会出现问题。甚至,因为事务由多条SQL构成,那么也会存在执行到一半出错或者不想再执行的情况。所以一个完整的事务,绝对不是简单的SQL语句集合,还需要满足下面的四个属性:

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

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

二、事务的版本支持

在MySQL中只有使用了InnoDB存储引擎才支持事务管理机制,MyISAM存储引擎是不支持事务的。我们可以输入指令show engines;查看一下所有引擎的详细信息,可以发现InnoDB是支持事务的,其它的都是不支持的。

在这里插入图片描述

三、事务的提交方式

事务的提交方式常见的有两种:自动提交、手动提交。我们可以查看一下当前MySQL的事务提交方式是什么,输入指令show variables like 'autocommit';,我们看到Value一列是on,代表是自动提交。

在这里插入图片描述

我们也可以将自动提交改成手动提交,输入指令set autocommit=0;即代表关闭自动提交,也就是变成手动提交。

在这里插入图片描述

四、事务的常见操作方式

下面我们来演示一下事务的操作方式,并且通过几个实验来验证事务的某一些特性。首先我们要创建一张用于测试的表:

mysql> create table account(
    -> id int primary key,
    -> name varchar(20) not null default '',
    -> blance decimal(10,2) not null default 0.0
    -> )engine=InnoDB;
Query OK, 0 rows affected (0.23 sec)

1.正常演示

  • 开始一个事务
    开始一个事务可以使用start transaction;语句,也可以使用begin;语句,推荐使用后者,因为比较方便简单。
mysql> start transaction;
Query OK, 0 rows affected (0.01 sec)
  • 创建保存点
    在手动操作事务时,我们可以创建保存点,通过保存点能让我们指定回滚到某一位置,这是可以由我们程序员自己控制的,创建保存点使用的语句是savepoint 保存点名字;
mysql> savepoint save1;
Query OK, 0 rows affected (0.00 sec)
  • 插入记录
    在创建保存点之后我们就可以插入记录了,我们创建两个保存点,并且在每个保存点之后依次插入一条记录。
mysql> savepoint save1;
Query OK, 0 rows affected (0.00 sec)

mysql> insert into account values (1, '张三', 100);
Query OK, 1 row affected (0.00 sec)

mysql> savepoint save2;
Query OK, 0 rows affected (0.00 sec)

mysql> insert into account values (2, '李四', 10000);
Query OK, 1 row affected (0.01 sec)

然后我们新建一个MySQL客户端,用另一个客户端查看一下account表中的数据,可以发现另一个客户端现在是可以查看到刚刚插入的数据的。

在这里插入图片描述

  • 演示回滚
    由于我们刚才手动设置了两个保存点,所以我们现在也能手动回滚。回滚有两种方式,第一种是直接使用语句roolback;可以直接回滚到最开始;第二种是使用语句roolback to 保存点名字;可以回滚到指定的保存点。例如下面的例子,我们先回滚到save2,再回滚到最开始:
mysql> rollback to save2;
Query OK, 0 rows affected (0.00 sec)

mysql> rollback;
Query OK, 0 rows affected (0.01 sec)

在这里插入图片描述

2.非正常演示:证明未提交,客户端崩溃,MySQL自动回滚

我们再来演示一下开始事务插入数据之后,如果遇到了客户端崩溃的情况,MySQL会不会自动回滚?

首先还是先开启事务,然后向account表中插入一条记录。

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> insert into account values (1, '张三', 100); 
Query OK, 1 row affected (0.00 sec)

此时我们在另一个MySQL客户端查看表数据是可以看到插入的记录的:

在这里插入图片描述

但当我们将启动事务插入数据的那个MySQL客户端强行异常终止之后,再看另一个MySQL客户端会发现,之前插入的数据没有了。原因是启动事务的客户端没有提交事务就被异常终止了,所以MySQL会自动回滚到事务最开始。

在这里插入图片描述

3.非正常演示:证明事务提交了,即使客户端崩溃,插入的数据也不会受影响

下面我们再来演示另一种情况:如果事务提交了,之后再异常终止客户端,那么插入的数据是否不会受影响呢?

首先依旧是启动事务,插入一条数据,最后提交记录。

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> insert into account values (1, '张三', 100);
Query OK, 1 row affected (0.00 sec)

mysql> commit;
Query OK, 0 rows affected (0.00 sec)

然后我们终止客户端,再重新启动一个客户端,发现表中的数据依旧是存在的。也就是说一旦事务被提交了,即使客户端崩溃,插入的数据也不会受影响,这些数据已经具有持久性。

在这里插入图片描述

4.非正常演示:证明手动提交事务不会受自动提交的影响

我们先确定一下目前MySQL是默认打开了自动提交事务的:

在这里插入图片描述
也就是说MySQL如果会自动提交的话,那么我们手动启动事务,如果没有提交就关闭了客户端,MySQL的自动提交会不会帮我们执行提交呢?我们做个实验来演示一下。

首先还是先开启事务,插入数据:

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> insert into account values (2, '李四', 10000);
Query OK, 1 row affected (0.00 sec)

数据插入成功了:

在这里插入图片描述

接下来终止掉开启事务的终端,再次查看表中的数据,我们发现MySQL帮我们自动回滚了,刚刚插入的数据没有了。也就是说,MySQL即使设置了自动提交,只要是手动启动的事务,必须手动提交,MySQL不会帮我们自动提交。

在这里插入图片描述

5.非正常演示:证明单条SQL与事务的关系

那么MySQL的自动提交方式到底有什么用呢?既然它在手动启动事务的情况下几乎是不会发生作用的,那么我们尝试一下不在手动启动事务的情况下,它是否会发生作用。

首先我们先关闭自动提交,输入语句set autocommit=0;即可:

在这里插入图片描述

关闭成功之后,我们再向account表中插入一条数据:

mysql> insert into account values (2, '李四', 10000);
Query OK, 1 row affected (0.01 sec)

然后在另一个MySQL终端查看表中的数据,刚刚插入的数据是可以看到的:

在这里插入图片描述

这个时候我们再强制关闭掉插入数据的MySQL终端,再从另一个MySQL终端中查看表中的数据,我们发现刚刚插入的数据消失了。这是因为我们一开始将自动提交关闭了,MySQL不会为我们自动提交事务,并且我们也没有手动启动事务,所以如果没有手动提交事务的话,在强制关闭客户端之后插入的数据就会消失。

在这里插入图片描述

相反,如果我们一开始打开了自动提交,那么即使我们没有手动启动事务,我们插入的每一条SQL都会被当成事务,MySQL会帮我们自动提交。

所以综合上述几个演示,我们可以得出以下的结论

  1. 只要手动启动了事务,事务就必须手动commit提交,才会持久化,与MySQL中是否设置自动提交无关。
  2. 事务可以手动回滚,同时,当操作异常时,MySQL会自动回滚到事务最开始的位置。
  3. 对于InnoDB的每一条SQL语句都会默认被封装成事务,只要打开了自动提交,每一条语句都会被当成事务自动提交,如果没有打开自动提交,那么在执行完SQL语句后要手动commit提交,否则退出后数据不再显示。

五、事务的隔离级别

1.如何理解隔离性

MySQL服务可能会同时被多个客户端进程访问,访问的方式是以事务的方式。一个事务可能由多条SQL构成,也就意味着,任何一个事务,都有执行前、执行中和执行后的阶段。而所谓的原子性,其实就是让用户层要么看到执行前,要么看到执行后,不会看到执行中的出现的问题。如果执行中出现了问题,事务可以随时回滚。所以单个事务对用户表现出来的特性就是原子性。

但是毕竟所有的事务都有个执行过程,那么在多个事务各自执行多个SQL语句的时候,就还是有可能会出现互相影响的情况,比如多个事务同时访问同一张表,多个事务都处于执行中阶段,那么这些事务修改的数据对于其它用户而言可不可以感知得到可不可以看得到呢?

所以在数据库中,为了保证事务执行过程中尽量不受干扰,就有了一个重要的特性即隔离性。在数据库中允许事务受不同程度的干扰,就有了一种重要的特征即隔离级别。

2.隔离级别

数据库一共有下面四种隔离级别,越往下隔离级别越高,相对应的效率也越低。

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

所谓的脏读指的是读未提交时,如果插入数据的客户端还没有提交插入的数据,就被其它客户端看到了,这些数据就叫作脏数据,这种现象就是脏读现象。

所谓不可重复读问题,是读提交这种隔离级别存在的问题。虽然看起来好像问题不大,但在现实使用过程中其实存在很大的风险问题。假设我们有一张学生表,表中有很多个学生的个人信息数据。现在有两个MySQL客户端A和客户端B,分别启动一个事务。客户端A和客户端B互相都不知道对方的存在,客户端A启动事务的目的是想修改一个同学的个人信息,比如张三同学语文成绩150分,客户端A想把张三同学的语文成绩改成140分。而客户端B想要先查询表中语文成绩为150分的同学,假如在客户端A修改或者提交之前,客户端B先查询,那么张三同学会出现在语文成绩为150分的查询结果当中。此时客户端A也修改完了也提交了,客户端B再查询表中语文成绩为140分的同学,就出现问题了,因为同一个张三同学又出现在了语文成绩为140分的查询结果当中,这就是不可重复读带来的问题。所以两个同时运行的事务如果不可重复读,就有可能会出现问题。

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

六、一致性

事务的原子性、隔离性、持久性是手段,而事务的一致性是目的。事务的执行结果,必须使数据库从一个一致性状态,变到另一个一致性状态。数据库的每一个阶段预期的结果我们是可以预见的,不能说执行了这一条语句我们也不知道接下来会发生什么样的结果。当数据库只包含事务成功提交的结果时,数据库处于一致性状态。如果系统运行发生中断,某个事务尚未完成而被迫中断,而该未完成的事务对数据库所做的修改已经被写入数据库,此时数据库就处于一种不正确的不一致的状态。因此一致性是用过原子性来保证的。其实一致性和用户的业务逻辑强相关,一般MySQL提供技术支持,但是一致性还是要用户业务逻辑做支撑,也就是,一致性,是由用户决定的。

七、隔离性的原理

1.数据库并发的三种场景

数据库并发有三种场景,分别是:

  • 读-读:即一个客户端读数据,另一个客户端也读数据。这种场景不会存在任何问题,也不需要并发控制。
  • 读-写:即一个客户端读数据,另一个客户端写数据。这种场景有线程安全的问题,可能会造成事务隔离性问题,可能会遇到脏读、幻读、不可重复读的问题。
  • 写-写:即一个客户端写数据,另一个客户端也是写数据。这种场景有线程安全问题,可能会存在更新丢失问题,比如第一类更新丢失、第二类更新丢失。

其中,读-写冲突问题是通过多版本并发控制(MVCC)来解决的,这是一种无锁并发控制的机制。

2.三个记录隐藏列字段

  • DB_TRX_ID:占6个字节,用来表示最近修改(修改/插入)的事务ID,记录创建这条记录/最后一次修改该记录的事务ID。
  • DB_ROLL_PTR:占7个字节,回滚指针,用来指向这条记录的上一个版本。这些数据一般存在undo log中。
  • DB_ROW_ID:占6个字节,是隐含的自增ID,即隐藏主键。如果数据表中没有设置主键,那么InnoDB会自动生成隐藏主键,并以此产生一个聚簇索引。
  • 补充:实际上还有一个删除flag隐藏字段,即用来记录被更新或删除。其实删除并不代表真的删除,而是改变了删除flag字段。

3.undo日志

MySQL是以服务进程的方式在内存中运行的。我们之前所讲的所有机制,包括索引、事务、隔离性、日志等,都是在内存中完成的。即在MySQL内部的相关缓冲区中,保存相关数据,完成各种操作。然后在合适的时候,将相关数据刷新到磁盘当中。所以,undo log可以简单理解为就是MySQL中的一段内存缓冲区,用来保存日志数据的。

下面我们结合undo log来理解一下MVCC多版本并发控制。首先我们创建一个student表,并且在表中插入一条数据:

mysql> create table if not exists student(
    -> name varchar(20) not null,
    -> age int not null
    -> );
Query OK, 0 rows affected (0.07 sec)

mysql> insert into student (name, age) values ('张三', 28);
Query OK, 1 row affected (0.00 sec)

mysql> select * from student;
+--------+-----+
| name   | age |
+--------+-----+
| 张三   |  28 |
+--------+-----+
1 row in set (0.00 sec)

表中记录所表达的意思是:

在这里插入图片描述

假设我们现在有一个事务ID为10的事务,对student表中的记录进行修改(Update):将张三的name改为李四,修改过程如下:

  1. 因为要修改,所以要先给该记录加行锁。
  2. 在修改之前,要先将该行记录拷贝到undo log中,所以,undo log中就有了一行副本数据,这个原理就和写时拷贝类似,在要写入时先将原来的数据拷贝一份再写入修改。
  3. 拷贝好副本之后,现在MySQL中就有了两行同样的记录,undo log里的副本保存历史版本记录,原始记录的内容就可以开始修改了。并且原始记录的事务ID要变为10,隐藏主键不变,回滚指针指向undo log中副本数据的地址,即表示当前版本的上一个版本就是它。
  4. 提交事务,释放行锁,修改结束。

在这里插入图片描述

此时如果我们又要对student表中的记录做修改,这次想把age从28改为38,修改过程如下:

  1. 因为要修改,所以要先给该记录加行锁。
  2. 在修改之前,要先将该行记录拷贝到undo log中,所以,undo log中就有了一行副本数据,这个原理就和写时拷贝类似,在要写入时先将原来的数据拷贝一份再写入修改。
  3. 现在可以修改原始记录中的age,改成38.并且修改原始记录的事务ID,改为11,并且修改原始记录中的回滚指针,改为undo log中的副本数据的地址。
  4. 提交事务,释放行锁,修改结束。

在这里插入图片描述

这样我们就有了一个基于链表记录的历史版本链。而所谓的回滚,无非就是用历史的数据,覆盖当前的数据。上面undo log里的一个个版本,我们可以称之为一个个的快照。

上面的例子是以update为主演示的,如果是delete呢?其实是一样的,因为delete删除数据并不是真正的删除数据,只是设置删除flag为删除,也可以形成历史版本。

如果是insert呢?因为insert是插入操作,也就是说之前并没有数据,那么insert按理说就不应该有历史版本。但是一般为了回滚操作,insert的数据的相反记录也是要被放入undo log中的,这个相反记录指的就是delete语句,因为你插入如果要回滚的话,就是将插入的数据删除了,就回到插入的上一个版本了。如果当前事务提交了,那么这个undo log的历史insert记录也就可以被清空了。

所以总结一下就是,update和delete可以形成版本链,insert暂时不考虑。

也就是说,增删改都是对最新版本的记录做操作,那么select读取记录是读取最新的版本还是读取历史版本呢?

一般而言,select是读取历史版本,这种读取叫做快照读,读取历史版本的话,是不受加锁限制的,也就是可以并行执行,这样也提高了效率,这就是MVCC的意义所在。但select也可以读取最新的记录,叫做当前读。如果是当前读的话那么select必须要加锁,这就是串行化的读取。

那么为什么要有隔离性和隔离级别呢?

通过上述的分析我们可以发现,事务从begin->CURD->commit,是有一个阶段的,也就是说事务有执行前、执行中和执行后三个阶段,并且不管启动多少个事务,这些事务总是有先有后的。那么多个事务在执行过程中,CURD操作是会交织在一起的。那么为了保证事务的“有先有后”,就必须让不同的事务看到它该看到的内容,这就是隔离性和隔离级别所要解决的问题。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值