为什么需要事务

在银行业务中,有一条记账原则,即有借有贷,借贷相等。为了保证这种原则,每发生一笔银行业务,就必须确保会计账目上借方记录和贷方记录各记一笔,并且这两笔账要么同时成功,要么同时失败

下面开始模拟实现转账功能,从张三的账户直接转账 500 元到李四的账户,sql语句如下:

# 转账测试:张三转账给李四 500 元
# 张三的账户少 500 元,李四的账户多 500 元

UPDATE bank SET currentMoney = currentMoney - 500 WHERE NAME="张三";
UPDATE bank SET currentMoney = currentMoney + 500 WHERE NAME="李四";

正常情况下,执行以上的转账操作后,两个余额总和应保持不变。但是,如果在这个过程的其中一个环节出现差错,如在张三的账户减少 500 元之后,这是服务器发生了故障,李四的账户没有立即增加 500 元,此时,第三方读取到两个账户的余额总和变成了 500 元,即账户总额间少了 500 元

为了解决此类问题,MySQL 提供了事务。事务可以将一系列的数据操作捆绑成一个整体进行统一管理,如果某一事务执行成功,则在该事务中进行的所有数据更改均会提交,成为数据库中的永久组成部分。如果事务执行时遇到错误,则必须取消或回滚。取消或回滚后,数据将全部恢复到操作前的状态,所有数据的更改均被清除。

MySQL 通过事务保证了数据的一致性。上述提到的转账过程就是一个事务,它需要两条 UPDATE 语句来完成。这两句是一个整体,如果其中任何一个环节出现问题,则整个转账业务也应取消,两个账户中的余额应恢复为原来的数据,从而确保转账前和转账后的余额总和不变。

事务的概念和特性

数据库的**事务(Transaction)**是一种机制,一个操作序列,包含了一组数据库操作命令。事务把所有的命令作为一个整体一起向系统提交或撤销操作请求,即这一组数据库的命令要么都执行,要么都不执行,因此事务是一个不可分割的工作逻辑单元。

在数据库系统上执行并发操作时,事务是作为最小的控制单元来使用的,特别使用于多用户同时操作的数据库操作系统。例如 航空公司的订票系统,银行,保险公司交易系统等。

数据具有 4 个特性,即原子性(Atomicity),一致性(Consistency),隔离性(Isolation) 和 持久性(Durability),这 4 个特性通常简称为 ACID,事务的 ACID 原则保证了一个事务要么成功提交,要么失败回滚,二者必居其一。

原子性 Atomicity

事务是一个完整的操作。事务的各元素是不可分的(原子的)。事务中的所有元素必须作为一个整体提交或回滚。如果事务中的任何元素失败,则整个事务将失败。

以银行转账事务为例,如果该事务提交了,则这两个账户的数据将会更新。如果由于某种原因,事务在成功更新这两个账户之前终止了,则不会更新这两个账户的余额,并且会撤销对任何账户余额的修改,事务不能部分提交。

一致性 Consistency

当事务完成时,数据必须处于一致状态。也就是说,在事务开始之前,数据库中存储的数据处于一致状态。在进行的事务中,数据可能处于不一致的状态,如数据可能有部分被修改。然而,当事务成功完成时,数据必须再次回到已知的一致状态。通过事务对数据所做的修改不能损坏数据,或者说事务不能使数据存储于不稳定的状态。

以银行转账事务为例,在事务开始之前,所有账户余额的总额处于一致状态。在事务进行过程中,一个账户余额减少了,而另一个账户余额尚未修改,此时,所有账户余额的总额处于不一致状态,事务完成以后,账户余额的总额再次恢复到一致状态。

隔离性 Isolation

对数据进行修改的所有并发事务是彼此隔离的,这表明事务必须是独立的,它不应以任何方式依赖或影响其它事务,修改数据的事务可以在另一个使用相同数据的事务开始之前访问这些数据,或者在另一个使用相同数据的事务结束之后访问这些数据。

另外,当事务修改数据时,如果任何其它进程正在同时使用相同的数据,则直到该事务成功提交之后,对数据的修改才能生效。张三和李四之间的转账与王五和赵二之间的转账,永远是相互独立的

持久性 Durability

事务的持久性指不管系统是否发生了故障,事务处理的结果都是永久的。

一个事务成功完成之后,它对数据库所做的改变是永久性的,即使系统出现故障也是如此,也就是说,一旦事务被提交,事务对数据所做的任何变动都会被永久地保留在数据库中。

语法和流程

MySQL 提供了多种存储引擎来支持事务。支持事务的存储引擎有 InnoDB 和 BDB,其中 InnoDB 存储引擎事务主要通过 UNDO 日志和 REDO 日志实现,MyISAM 存储引擎不支持事务。

任何一种数据库,都会拥有各种各样的日志,用来记录数据库的运行情况,日常操作,错误信息等,MySQL也不例外。例如,当用户 root 登录到 MySQL 服务器,就会在日志文件里记录该用户的登录时间,执行操作等。

为了维护 MySQL 服务器,经常需要在 MySQL 数据库中进行日志操作:

  • UNDO 日志:复制事务执行前的数据,用于在事务发生异常时回滚数据
  • REDO 日志:记录在事务执行中,每条对数据进行更新的操作,当事务提交时,该内容将被刷新到磁盘。

默认设置下,每条 SQL 语句就是一个事务,即执行 SQL 语句后自动提交。为了达到将几个操作作为一个整体的目的,需要使用 BEGINSTART TRANSACTION 开启一个事务,或禁止当前会话的自动提交。

执行事务

SQL 使用下列语句来管理事务

1. 开始事务

BEGIN;

# 或者

# 这个语句显式地标记一个事务的起始点
START TRANSACTION;

2. 提交事务

COMMIT;

COMMIT 表示提交事务,提交事务的所有操作,具体地说,就是将事务中所有对数据库的更新都写到磁盘上的物理数据库中,事务正常结束。

提交事务,意味着将事务开始以来所执行的所有数据都修改成为数据库的永久部分,因此也标志着一个事务的结束。一旦执行了该命令,将不能回滚事务。只有在所有修改该都准备好提交给数据库时,才执行这一操作。

3. 回滚(撤销)事务

ROLLBACK;

ROLLBACK 表示撤销事务,即在事务运行的过程中发生了某种故障,事务不能继续执行,系统将事务中对数据库的所有已完成的操作全部撤销,回滚到事务开始时的状态。这里的操作指对数据库的更新操作。

当事务执行过程中遇到错误时,使用 ROLLBACK 语句事务回滚到起点或指定的保持点处。同时,系统将清除自事务起点或到某个保存点所做的所有的数据修改,并且释放由事务控制的资源。因此,这条语句也标志着事务的结束

总结

BEGIN 或 START TRANSACTION 语句后面的 SQL 语句对数据库数据的更新操作都将记录在事务日志中,直至遇到 ROLLBACK语句 或 COMMIT语句。如果事务中某一操作失败且执行了 ROLLBACK语句,那么在开启事务语句之后所有更新的数据都能回滚到事务开始前的状态。如果事务中的所有操作全部正确完成,并且使用了 COMMIT语句向数据库提交更新数据,则此时的数据又处在新的一致状态。

注意事项

MySQL 事务时一项非常消耗资源的功能,使用中要注意以下几点:

1. 事务尽可能简短

事务的开启到结束会在数据库管理系统中保留大量资源,以保证事务的四大特性,如果在多用户系统中,较大的事务将会占用系统的大量资源,使系统不堪重负,影响软件的运行性能甚至导致系统崩溃

2. 事务中访问的数据量尽量最少

当并发执行事务处理时,事务操作的数据量越少,事物之间对相同数据的操作就越少

3. 查询数据时尽量不要使用事务

对数据进行浏览查询操作并不会更新数据库中的数据,因此尽量不使用事务查询数据,避免占用过量系统资源

4. 在事务处理过程中尽量不要出现等待用户输入的操作

在处理事务的过程中,如果需要等待用户输入数据,那么事务会长时间地占用资源,有可能造成系统阻塞

设置事务自动提交

MySQL 默认开启事务自动提交模式,即除非显示的开启事务 (BEGIN / START Transaction),否则每条 SQL 语句都会被当做一个单独的事务自动执行。但有些情况下,我们需要关闭事务自动提交来保证数据的一致性

在 MySQL中,我们可以通过 show variables 语句来查看当前事务自动提交模式,如下所示:

SHOW VARIABLES LIKE 'autocommit';

结果显示,autocommit 的值是 ON,表示系统开启自动提交模式

在 MySQL中,可以使用 SET autocommit 语句设置事务的自动提交模式,语法格式如下:

set autocommit = 0|1|ON|OFF;

取值的说明:

  • 值为 0 或 OFF : 关闭事务自动提交。如果关闭自动提交,用户将会一直处于某个事务中,只有提交或回滚后才会结束当前事务,重新开始一个新事务
  • 值为 1 或 ON : 开启事务自动提交。如果开启自动提交,则每执行一条 SQL语句,事务都会提交一次

关闭自动提交后,该位置会作为一个事务起点,直到执行 COMMIT 或者 ROLLBACK语句后,该事务才结束,结束之后,这就是下一个事务的起点

关闭自动提交功能后,只有当执行 COMMIT命令后,MySQL 才会将数据表中的资料提交到数据库中,如果执行 ROLLBACK语句,数据将会回滚。如果不提交事务,而终止 MySQL会话,数据库将会自动执行回滚操作

使用 BEGIN 或 START TRANSACTION 开启一个事务后,自动提交将保持禁用状态,直到使用 COMMIT 或 ROLLBACK结束事务。之后,自动提交模式会恢复到之前的状态,即如果 BEGIN 前 autocommit = 1,则完成本次事务后 autocommit 还是 1

事务隔离级别

前边介绍了 MySQL 事务的四大特性,其中事务的隔离性就是指多个事务同时运行时,各事务之间相互隔离,不可互相干扰。

如果事务没有隔离性,就容易出现脏读,不可重复读和幻读等情况

1. 脏读

脏读是指一个事务正在访问数据,并且对数据进行了修改该,但是这种修改该还没有提交到数据库中。这时,另一个事务也访问了这个数据,然后使用了这个数据

2. 不可重复读

不可重复读是指在一个事务中,多次读取同一个数据

在这个事务还没有结束时,另一个事务也访问该同一数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的数据可能是不一样的。这样在一个事务两次读到的数据是不一样的,因此称为 不可重复读

3. 幻读

幻读是指当事务不是独立执行时发生的一种现象,例如当第一个事务对一个表中的数据进行了修改,同时,第二个事务也修改了这个数据。那么,当操作第一个事务的用户查询修改结果时,出现了奇怪的结果,就好像发现了幻觉一样

为了解决以上这些问题,标准 SQL 定义了 4 类事务隔离级别,用来指定事务中哪些数据改变是可见的,哪些数据改变时不可见的。

  • 读未提交 (READ UNCOMMITTED)
  • 读提交 (READ COMMITTED)
  • 可重复度 (REPEATABLE READ)
  • 串行化 (SERIALIZABLE)

不同的事务隔离级别可能出现的问题如下表所示:

隔离级别 脏读 不可重复读 幻读
READ UNCOMMITTED
READ COMMITTED ×
REPEATABLE READ × ×
SERIALIZABLE × × ×

四类隔离级别

读未提交

READ UNCOMMITTED ,RU

读未提交就是可以读到未提交的内容

如果一个事务读取到了另一个未提交事务修改过的数据,那么这种隔离级别就被称为读未提交

读提交

READ COMMITTED , RD

读提交就是只能读到已经提交了的内容

如果一个事务只能读取到另一个已提交事务修改过的内容,并且其它事务每对该数据进行一次修改并提交后,该事务都能查询到最新值,那么这种隔离界别就被称为 读提交

该隔离级别满足了隔离的简单定义: 一个事务从开始到提交前所做的任何改变都是不可见的,事务只能读取到已经提交的事务所做的改变

这是大多数据库系统默认的事务隔离级别( 例如 OracleSQL Server ),但不是 MySQL 默认的

可重复读

REPEATABLE READ ,RR

可重复读是专门针对不可重复读这种情况而制定的隔离级别,可以有效避免不可重复读

在一些场景中,一个事务只能读取到另一个已提交事务修改过的数据,但是第一次读过某条记录后,即使其它事务修改该了该条记录的值并提交,之后该事务再读这条记录时,读到的仍是第一次读到的值,而不是每次都读到不同的数据。那么这种隔离级别就称之为 可重复读

可重复读是 MySQL 的默认事务隔离级别,它能确保同一事务的多个实例在并发读取数据时,会看到同样的数据行,在该隔离级别下,如果有事务正在读取数据,就不允许其它事务进行修改操作,这样解决了可重复读的问题

串行化

SERIALIZABLE

当数据库同时要执行多个事务时,让所有的事务排队依次执行,只有当上一个事务执行完成后才能执行下一个事务,严格保障了事务的独立性

串行化 是最高的事务隔离级别,主要通过强制事务排序来解决幻读问题,简单来说,就是在每个读取的数据行上加上共享锁实现,这样就避免了脏读,不可重复读和幻读等问题,但是这种隔离级别 执行效率低下,且性能开销也最大,所以一般情况下不推荐使用

查看隔离级别

在 MySQL 中,可以通过下列语句查看当前事务隔离级别:

show variables like '%tx_isolation%'

# 或者
select @@tx_isolation

另外,还可以使用 下列语句分别查询 全局 和 当前会话的事务隔离级别:

SELECT @@global.tx_isolation;

SELECT @@session.tx_isolation;

在 MySQL 8.0.3 中,tx_isolation 变量被 transaction_isolation 替换了,只要把上述查询语句中的 tx_isolation 变量替换成 transaction_isolation 变量即可

修改隔离级别

MySQL 提供了 set transaction 语句,该语句可以改变单个会话 或者 全局的事务隔离级别

SET [SESSION|GLOBAL] TRANSACTION ISOLATION LEVEL { READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE }
  • SESSION:表示修改的事务隔离级别将应用于当前session(当前cmd窗口)内的所有事务
  • GLOBAL:表示修改的事务隔离级别将应用于所有 session(全局)中的所有事务,且当前已经存在的 session 不受影响
  • 如果省略 SESSION 和 GLOBAL,表示修改的事务隔离级别将应用于当前 session 内的下一个还未开始的事务

任何用户都能改变会话的事务隔离级别,但是只有拥有 SUPER 权限的用户才能改变全局的事务隔离级别

备注:回滚失败的问题

MySQL中,事务内仅支持 DML 语句,也就是仅支持 CURD 操作,如果在事务中执行 DDL 语句会怎样呢?

当执行到 DDL语句时,会隐式的将当前会话的事务进行一次 “COMMIT” 操作

后续就无法进行回滚数据了!

因此在 MySQL 中执行 DDL 语句时,应该严格地将 DDL 和 DML 完全分开,不能混合在一起执行!!!