事务
- 一、什么是事务
- 二、为什么会出现事务
- 三、事务的版本支持
- 四、事务提交方式
- 五、事务常见操作方式
-
- 1. 准备工作
- 2. 事务的正常操作
-
- (1)事务的开始与回滚
- (2)客户端崩溃未 commit
- (3)begin 操作会自动更改提交方式,不会受MySQL是否自动提交影响
- (4)单条 SQL 与事务的关系
- 六、事务隔离级别
-
- 1. 初识隔离性
- 2. 隔离级别
- 3. 事务隔离级别的设置与查看
- 4. 读未提交 — RU
- 5. 读提交 — RC
- 6. 可重复读 — RR
- 7. 串行化
- 七、一致性
一、什么是事务
我们先来看一个例子,例如有一个火车售票系统:
当客户端A检查还有一张票时,将票卖掉,还没有执行更新数据库的时候,客户端B检查了票数,发现大于0,于是又买了一次票。然后客户端A将票数更新回数据库。于是就出现了同一张票被卖了两次的情况。
所以数据库的 CURD 应该满足什么属性能解决上面的问题?
- 买票的过程得是原子的吧
- 买票互相应该不能影响吧
- 买完票应该要永久有效吧
- 买前,和买后都要是确定的状态吧
- 什么是事务呢?
事务就是一组 DML 语句组成,这些语句在逻辑上存在相关性,这一组 DML 语句要么全部成功,要么全部失败,是一个整体。MySQL 提供一种机制,保证我们达到这样的效果。事务还规定不同的客户端看到的
数据是不相同的。
事务就是要做的或所做的事情,主要用于处理操作量大,复杂度高的数据。假设一种场景:我给某个人转账,数据库必定需要将我账户上的金额 update ,然后给对方的账户做 add 操作等等,这样,就需要多条 MySQL 语句构成,那么所有这些操作合起来,就构成了一个事务。
正如我们上面所说,一个 MySQL 数据库,可不止一个事务在运行,同一时刻,甚至有大量的请求被包装成事务,在向 MySQL 服务器发起事务处理请求。而每条事务至少一条 SQL ,最多很多 SQL ,这样如果大家都访问同样的表数据,在不加保护的情况,就绝对会出现问题。甚至,因为事务由多条 SQL 构成,那么,也会存在执行到一半出错或者不想再执行的情况,那么已经执行的怎么办呢?
所以,一个完整的事务,绝对不是简单的 SQL 集合,还需要满足如下四个属性:
- 原子性:一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
- 一致性:在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。
- 隔离性:数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交( Read uncommitted )、读提交( read committed )、可重复读( repeatable read )和串行化( Serializable )
- 持久性:事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
上面四个属性,可以简称为 ACID ;原子性(Atomicity,或称不可分割性);一致性(Consistency);隔离性(Isolation,又称独立性);持久性(Durability)。
所以总结,所谓的事务,就是在 ACID 四大属性的加持之下,由一条或者多条 SQL 共同构建成的,我们就称为事务。
二、为什么会出现事务
事务被 MySQL 编写者设计出来,本质是为了当应用程序访问数据库的时候,事务能够简化我们的编程模型,不需要我们去考虑各种各样的潜在错误和并发问题。可以想一下当我们使用事务时,要么提交,要么回滚,我们不会去考虑网络异常了,服务器宕机了,同时更改一个数据怎么办对吧?因此事务本质上是为了应用层服务的,而不是伴随着数据库系统天生就有的。
备注:我们后面把 MySQL 中的一行信息,称为一行记录。
三、事务的版本支持
在 MySQL 中只有使用了 Innodb 数据库引擎的数据库或表才支持事务, MyISAM 不支持。我们可以使用指令 show enginesG
查看各种引擎的属性:
四、事务提交方式
事务的提交方式常见的有两种:
- 自动提交
- 手动提交
查看事务提交方式:show variables like 'autocommit';
我们可以用 SET 来改变 MySQL 的自动提交模式:SET AUTOCOMMIT=0;
禁止自动提交。SET AUTOCOMMIT=1;
开启自动提交。
五、事务常见操作方式
1. 准备工作
- 设置隔离级别
我们将隔离级别设置成最低是为了方便我们观察现象。
为了便于演示,我们将 mysql 的默认隔离级别设置成读未提交。具体操作我们后面会详细讲,现在以使用为主:
set global transaction isolation level read uncommitted;
设置好后需要重启终端,进行查看:select @@tx_isolation;
:
-
创建测试表
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;
以上是一个简单的员工工资表的表结构。
2. 事务的正常操作
(1)事务的开始与回滚
首先我们已经开启自动提交:
我们开始一个事务的语句是:start transaction;
或者 begin
,下面先使用第一个:
我们开始一个事务后,从该语句往后的所有 SQL 语句都是同一个事务。我们开启另一个终端同时也启动一个事务:
首先我们当前的表是空的,我们以左边的终端为主,我们先创建一个保存点 s1,对应的语句为 savepoint s1;
;然后我们往表里插入一个数据;接着再创建一个保存点 s2,然后再插入一个数据,如下图:
然后我们在另一个终端查看该表,是可以看见另一个终端插入的数据的:
上面所有的创建保存点、插入数据的操作都是一个事务,那么我们操作失误了,想要撤回 Mike 的数据,我们就可以定向地回滚,可以回滚到指定的位置,例如我们想撤回 Mike 的数据,我们回滚到 s2 的保存点即可,对应的语句为:rollback to s2;
,此时我们再从另一个终端查看该表时,就会发现 Mike 的数据已经没有了,如下:
如果我们直接 rollback;
,会回滚到最开始的地方。那么表中的数据也就没有了。现在我们把该事务提交,对应的语句为 commit;
。
如果我们插入数据后没有 rollback 而是 commit 那么数据就会持久化地保存到数据库中,这时候 rollback 也没有用了。
(2)客户端崩溃未 commit
假设我们正常开始一个事务,正常插入数据,此时是可以看到插入的数据的:
但是如果当我们的 mysql 异常崩溃,还没有 commit 会怎样呢?下面我们让 mysql 异常崩溃,直接按下 *ctrl + * 即可:
如图,我们可以看到,数据会自动回滚。但是如果我们 commit 之后异常崩溃,数据不会再受影响,因为数据已经持久化。
(3)begin 操作会自动更改提交方式,不会受MySQL是否自动提交影响
我们上面手动启动一个事务并不会受 MySQL 是否自动提交影响,例如我们现在把自动提交关掉:
我们再启动一个事务,插入数据等,重复上面的操作:
如上,异常崩溃会自动回滚。
如上,commit 后再异常崩溃数据已经持久化。
(4)单条 SQL 与事务的关系
我们知道,当我们启动一个事务的时候删除一个数据再手动 commit 之后,数据一定会被删除。但是我们将自动提交关闭,不启动事务,像我们正常一样使用单 SQL 语句删除呢?再异常崩溃会怎样呢?下面我们验证一下:
如上,我们自动提交关闭后,执行单条 SQL 语句异常退出后,数据也会回滚回来!这是为什么呢?因为自动提交已经被关闭了!需要我们手动提交才能保存数据!下面我们验证一下是否需要我们手动提交:
如上,我们手动 commit 后数据确实被删除了!说明我们的单 SQL 语句也是事务,只是以前系统默认打开自动提交,执行完 SQL 语句后就自动 commit 了!
所以根据上面四个场景,我们得出以下结论:
- 只要输入 begin 或者 start transaction,事务便必须要通过 commit 提交,才会持久化,与是否设置 set autocommit 无关;
- 事务可以手动回滚,同时,当操作异常,MySQL 会自动回滚;
- 对于 InnoDB 每一条 SQL 语言都默认封装成事务,自动提交(select 有特殊情况,因为 MySQL 有 MVCC );
- 从上面的例子,我们能看到事务本身的原子性(回滚),持久性(commit)。
事务操作注意事项
- 如果没有设置保存点,也可以回滚,只能回滚到事务的开始。直接使用 rollback (前提是事务还没有提交);
- 如果一个事务被提交了(commit),则不可以回退(rollback);
- 可以选择回退到哪个保存点;
- InnoDB 支持事务, MyISAM 不支持事务;
- 开始事务可以使 start transaction 或者 begin。
六、事务隔离级别
1. 初识隔离性
- MySQL 服务可能会同时被多个客户端进程(线程)访问,访问的方式以事务方式进行;
- 一个事务可能由多条 SQL 构成,也就意味着,任何一个事务,都有执行前,执行中,执行后的阶段。而所谓的原子性,其实就是让用户层,要么看到执行前,要么看到执行后。执行中出现问题,可以随时回滚。所以单个事务,对用户表现出来的特性,就是原子性;
- 但毕竟所有事务都要有个执行过程,那么在多个事务各自执行多个 SQL 的时候,就还是有可能会出现互相影响的情况。比如:多个事务同时访问同一张表,甚至同一行数据;
- 数据库中,为了保证事务执行过程中尽量不受干扰,就有了一个重要特征:隔离性;
- 数据库中,允许事务受不同程度的干扰,就有了一种重要特征:隔离级别。
下面举一个例子,假设有人向数据库进行 update 操作,另外一个人向数据库进行 select 数据,那么如果两个事务对同一个数据库操作,数据库先执行谁的呢?我们最先想到的可能是先 update 再 select 数据,因为需要保证数据是最新的,但是这是有问题的!为什么呢?比如我们在出生之前,能不能看到过去的世界呢?不能,我们也不应该看得到!又比如已经过世的故人,能不能看到我们今天的世界呢?也不能!所以回到事务,事务也是一样的!它不需要看到旧的数据,也不应该看到最新的数据,只需要看到每个事务到来时,它应该看到的数据,这就是隔离性!
那么回到上面的问题,update 和 select 谁先执行呢?这就要取决于它们谁先到来了,如果 update 先来,那么肯定先执行 update,因为要保持事务的原子性。如果 update 执行的很快,select 执行的很久,可能在 update 执行完毕之后,select 还在执行,那么此时 select 应不应该更新后的数据呢?不应该!因为要保证事务的隔离性!
2. 隔离级别
- 读未提交【Read Uncommitted】: 在该隔离级别,所有的事务都可以看到其他事务没有提交的执行结果。(实际生产中不可能使用这种隔离级别的),但是相当于没有任何隔离性,也会有很多并发问题,如脏读,幻读,不可重复读等,我们上面为了更好地观察现象,用的就是这个隔离性。
- 读提交【Read Committed】 :该隔离级别是大多数数据库的默认的隔离级别(不是 MySQL 默认的)。它满足了隔离的简单定义:一个事务只能看到其他的已经提交的事务所做的改变。这种隔离级别会引起不可重复读,即一个事务执行时,如果多次 select, 可能得到不同的结果。
- 可重复读【Repeatable Read】: 这是 MySQL 默认的隔离级别,它确保同一个事务,在执行中,多次读取操作数据时,会看到同样的数据行。但是会有幻读问题。
- 串行化【Serializable】: 这是事务的最高隔离级别,它通过强制事务排序,即事务需要一个一个排队处理,使之不可能相互冲突,从而解决了幻读的问题。它在每个读的数据行上面加上共享锁,但是可能会导致超时和锁竞争(这种隔离级别太极端,实际生产基本不使用)。
3. 事务隔离级别的设置与查看
- 查看
查看全局隔级别:
select @@global.tx_isolation;
查看此次会话(登录)全局隔级别:
select @@session.tx_isolation;
或 select @@tx_isolation;
以上两种查看隔离级别的区别在于,select @@global.tx_isolation;
是全局的隔离级别;select @@tx_isolation;
在此次登录时默认读取全局的隔离级别,然后拷贝一份给自己,它的生命周期是在当我们开始登录到退出客户端。
-
设置隔离级别
set [session | global] transaction isolation level { read uncommitted | read commited | repeatable read | serializable }
其中 [session | global]
是设置当前会话或者全局隔离级别;{}
内部是对应的隔离级别。
设置当前会话隔离性,另起一个会话,不会影响另一个会话,只影响当前会话;而设置全局隔离性,另起一个会话,会被影响。
4. 读未提交 — RU
我们在上面也设置过了我们当前的隔离级别是 RU,如下:
接下来我们开启两个事务并发起来,我们在其中一个事务中插入数据、删除数据、修改数据等,还没有 commit 前,在另一个事务中都可以查看得到,如下:
这就是读未提交,我们在另一个事务中读到了别人还没提交的事务!一个事务在执行中,读到另一个执行中事务的更新(或其他操作)但是未 commit 的数据,这种现象叫做脏读!
5. 读提交 — RC
首先我们将隔离级别改成 RC:
当我们将两个终端的隔离级别都设置为 RC 后,下面我们开始做一些实验。首先我们在两个终端分别启动事务,在其中一个终端插入、修改数据,观察另一个终端是否能见:
如上图,我们发现在一个事务在进行期间,另一个事务进行查看是不能看见它的增加或修改的,而当前事务本身可以看见吗?我们试一下:
是可以的;当我们将第一个终端的事务 commit 之后,看看另一个事务能否看见修改之后的表:
如上图,我们发现在它进行 commit 之后,另一个事务还没有 commit 也能看到对应的修改!这就是读提交!
但是,第二个终端此时还在当前事务中,并未 commit,那么就造成了,同一个事务内,同样的读取,在不同的时间段(依旧还在事务操作中!),读取到了不同的值,这种现象叫做不可重复读。
那么不可重复读是问题吗?是的!在和我们并发运行的事务中,它做了修改表的数据并 commit,而导致我在每次查看数据的时候都是不一样的,这会导致一些问题出现,例如我们在用使用这个事务查看表进行统计数据时,统计到了一半,突然再查一下数据发现数据不一样了,就会导致我们需要重新统计!这就是因为不可重复读可能会引发的问题!
6. 可重复读 — RR
因为 MySQL 默认的隔离级别就是 RR 级别,所以我们重新启动 MySQL 服务即可更换为 RR 级别:
下面我们也并发启动两个事务,其中一个进行修改、新增数据,观察另一个事务查看的情况:
我们看到, 另一个事务是看不见的,这也正常,我们在 RC 级别都看不见,RR 级别也应该看不见,但是下面我们将修改的数据 commit 呢?再看看会有什么变化:
如图,当我们 commit 之后再查看,还是看不见修改的数据。如果我们也将这个事务 commit,再查看,就可以看到修改的数据了:
如上图,这就是可重复读。
在可重复读中,我们假设第一个终端为终端A,第二个为终端B,多次查看,发现终端A在对应事务中 insert 的数据,在终端B的事务周期中,也没有什么影响,也符合可重复的特点。但是,一般的数据库在可重复读情况的时候,无法屏蔽其他事务 insert 的数据,为什么呢?因为隔离性实现是对数据加锁完成的,而 insert 待插入的数据因为并不存在,那么一般加锁无法屏蔽这类问题,所以会造成虽然大部分内容是可重复读的,但是 insert 的数据在可重复读情况被读取出来,导致多次查找时,会多查找出来新的记录,就如同产生了幻觉。这种现象,叫做幻读(phantom read)。很明显,MySQL 在 RR 级别的时候,是解决了幻读问题的,具体的解决方法我们就不研究了。
7. 串行化
串行化就是对所有操作全部加锁,进行串行化,不会有问题,但是只要串行化,效率很低,几乎完全不会被采用。
接下来我们将隔离级别更换为串行化:
接下来我们启动两个事务,分别进行查看数据,是没有问题的,因为两个读取不会串行化,共享锁:
然后我们在终端A修改数据,在终端B读取,即进行读写操作,终端A会卡住,因为终端B中的事务还没有结束:
当终端B中的事务结束,终端A中的事务才能继续:
反过来也一样,在终端A修改数据,在终端B读取,终端B中的读取也会卡住:
当终端A的事务结束终端B才能继续读取:
总结:
- 其中隔离级别越严格,安全性越高,但数据库的并发性能也就越低,往往需要在两者之间找一个平衡点;
- 不可重复读的重点是修改和删除:同样的条件,你读取过的数据,再次读取出来发现值不一样了;幻读的重点在于新增:同样的条件, 第1次和第2次读出来的记录数不一样;
- 说明: mysql 默认的隔离级别是可重复读,一般情况下不要修改;
- 上面的例子可以看出,事务也有长短事务这样的概念。事务间互相影响,指的是事务在并行执行的时候,即都没有 commit 的时候,影响会比较大。
七、一致性
- 事务执行的结果,必须使数据库从一个一致性状态,变到另一个一致性状态。当数据库只包含事务成功提交的结果时,数据库处于一致性状态。如果系统运行发生中断,某个事务尚未完成而被迫中断,而改未完成的事务对数据库所做的修改已被写入数据库,此时数据库就处于一种不正确(不一致)的状态。因此一致性是通过原子性来保证的;
- 其实一致性和用户的业务逻辑强相关,一般 MySQL 提供技术支持,但是一致性还是要用户业务逻辑做支撑,也就是,一致性,是由用户和 MySQL 共同决定的;
- 而技术上,通过 AID 保证 C,也就是有了原子性、持久性、隔离性,就能保证一致性。