一,事务的起源

1.原子性(Atomicity)

现实世界中转账操作是一个不可分割的操作,也就是说要么压根儿就没转,要么转账成功,不能存在中间的状态,也就是转了一半的这种情况。设计数据库的大叔们把这种要么全做,要么全不做的规则称之为原子性。但是在现实世界中的一个不可分割的操作却可能对应着数据库世界若干条不同的操作,数据库中的一条操作也可能被分解成若干个步骤(比如先修改缓存页,之后再刷新到磁盘等),最要命的是在任何一个可能的时间都可能发生意想不到的错误(可能是数据库本身的错误,或者是操作系统错误,甚至是直接断电之类的)而使操作执行不下去。为了保证在数据库世界中某些操作的原子性,MySQL需要保证如果在执行操作的过程中发生了错误,把已经做了的操作恢复成没执行之前的样子。

2.隔离性(Isolation)

现实世界中的两次状态转换应该是互不影响的,所以对于现实世界中状态转换对应的某些数据库操作来说,不仅要保证这些操作以原子性的方式执行完成,而且要保证其它的状态转换不会影响到本次状态转换,这个规则被称之为隔离性。这时MySQL就需要采取一些措施来让访问相同数据的不同状态转换对应的数据库操作的执行顺序有一定规律。

3.一致性(Consistency)

我们生活的这个世界存在着形形色色的约束,比如身份证号不能重复,性别只能是男或者女,高考的分数只能在0~750之间,人民币面值最大只能是100(现在是2019年),红绿灯只有3种颜色,房价不能为负的。 只有符合这些约束的数据才是有效的。数据库世界只是现实世界的一个映射,现实世界中存在的约束当然也要在数据库世界中有所体现。如果数据库中的数据全部符合现实世界中的约束(all defined rules),我们说这些数据就是一致的,或者说符合一致性的。

如何保证数据库中数据的一致性(就是符合所有现实世界的约束)呢?这其实靠两方面的努力:

  • 数据库本身能为我们保证一部分一致性需求(就是数据库自身可以保证一部分现实世界的约束永远有效)。
    我们知道MySQL数据库可以为表建立主键、唯一索引、外键、声明某个列为NOT NULL来拒绝NULL值的插入。比如说当我们对某个列建立唯一索引时,如果插入某条记录时该列的值重复了,那么MySQL就会报错并且拒绝插入。除了这些我们已经非常熟悉的保证一致性的功能,MySQL还支持CHECK语法来自定义约束,比如这样:
    上述例子中的CHECK语句本意是想规定balance列不能存储小于0的数字,对应的现实世界的意思就是银行账户余额不能小于0。但是很遗憾,MySQL仅仅支持CHECK语法,但实际上并没有一点卵用,也就是说即使我们使用上述带有CHECK子句的建表语句来创建account表,那么在后续插入或更新记录时,MySQL并不会去检查CHECK子句中的约束是否成立。
    虽然CHECK子句对一致性检查没什么卵用,但是我们还是可以通过定义触发器的方式来自定义一些约束条件以保证数据库中数据的一致性。

    1. CREATE TABLE account (
    2. id INT NOT NULL AUTO_INCREMENT COMMENT '自增id',
    3. name VARCHAR(100) COMMENT '客户名称',
    4. balance INT COMMENT '余额',
    5. PRIMARY KEY (id),
    6. CHECK (balance >= 0)
    7. );

    其它的一些数据库,比如SQL Server或者Oracle支持的CHECK语法是有实实在在的作用的,每次进行插入或更新记录之前都会检查一下数据是否符合CHECK子句中指定的约束条件是否成立,如果不成立的话就会拒绝插入或更新。

  • 更多的一致性需求需要靠写业务代码的程序员自己保证。
    为建立现实世界和数据库世界的对应关系,理论上应该把现实世界中的所有约束都反应到数据库世界中,但是在更改数据库数据时进行一致性检查是一个耗费性能的工作,比方说我们为account表建立了一个触发器,每当插入或者更新记录时都会校验一下balance列的值是不是大于0,这就会影响到插入或更新的速度。仅仅是校验一行记录符不符合一致性需求倒也不是什么大问题,有的一致性需求简直变态,比方说银行会建立一张代表账单的表,里边儿记录了每个账户的每笔交易,每一笔交易完成后,都需要保证整个系统的余额等于所有账户的收入减去所有账户的支出。如果在数据库层面实现这个一致性需求的话,每次发生交易时,都需要将所有的收入加起来减去所有的支出,再将所有的账户余额加起来,看看两个值相不相等。如果账单表里有几亿条记录,光是这个校验的过程可能就要跑好几个小时,这样的性能代价是完全承受不起的。
    现实生活中复杂的一致性需求比比皆是,而由于性能问题把一致性需求交给数据库去解决这是不现实的,所以这个锅就甩给了业务端程序员。比方说我们的account表,我们也可以不建立触发器,只要编写业务的程序员在自己的业务代码里判断一下,当某个操作会将balance列的值更新为小于0的值时,就不执行该操作就好了!

原子性隔离性都会对一致性产生影响,比如我们现实世界中转账操作完成后,有一个一致性需求就是参与转账的账户的总的余额是不变的。如果数据库不遵循原子性要求,也就是转了一半就不转了,那最后就是不符合一致性需求的;类似的,如果数据库不遵循隔离性要求,也就是说可能不符合一致性需求了。所以说,数据库某些操作的原子性和隔离性都是保证一致性的一种手段,在操作执行完成后保证符合所有既定的约束则是一种结果。那满足原子性隔离性的操作一定就满足一致性么?那倒也不一定,那不满足原子性隔离性的操作就一定不满足一致性么?这也不一定,只要最后的结果符合所有现实世界中的约束,那么就是符合一致性的。

4.持久性(Durability)

当现实世界的一个状态转换完成后,这个转换的结果将永久的保留,这个规则被MySQL称为持久性。当把现实世界的状态转换映射到数据库世界时,持久性意味着该转换对应的数据库操作所修改的数据都应该在磁盘上保留下来,不论之后发生了什么事故,本次转换造成的影响都不应该被丢失掉。

二,事务的概念

我们把原子性Atomicity)、隔离性Isolation)、一致性Consistency)和持久性Durability)这四个词对应的英文单词首字母提取出来就是AICD,稍微变换一下顺序可以组成一个完整的英文单词:ACID。MySQL叔为了方便起见,把需要保证原子性隔离性一致性持久性的一个或多个数据库操作称之为一个事务(英文名是:transaction)。

我们现在知道事务是一个抽象的概念,它其实对应着一个或多个数据库操作,MySQL根据这些操作所执行的不同阶段把事务大致上划分成了这么几个状态:

  • 活动的(active)
    事务对应的数据库操作正在执行过程中时,我们就说该事务处在活动的状态。
  • 部分提交的(partially committed)
    当事务中的最后一个操作执行完成,但由于操作都在内存中执行,所造成的影响并没有刷新到磁盘时,我们就说该事务处在部分提交的状态。
  • 失败的(failed)
    当事务处在活动的或者部分提交的状态时,可能遇到了某些错误(数据库自身的错误、操作系统错误或者直接断电等)而无法继续执行,或者人为的停止当前事务的执行,我们就说该事务处在失败的状态。
  • 中止的(aborted)
    如果事务执行了半截而变为失败的状态,那么就需要把已经修改的数据调整为未修改之前的数据,换句话说,就是要撤销失败事务对当前数据库造成的影响。书面一点的话,我们把这个撤销的过程称之为回滚。当回滚操作执行完毕时,也就是数据库恢复到了执行事务之前的状态,我们就说该事务处在了中止的状态。
  • 提交的(committed)
    当一个处在部分提交的状态的事务将修改过的数据都同步到磁盘上之后,我们就可以说该事务处在了提交的状态。

随着事务对应的数据库操作执行到不同阶段,事务的状态也在不断变化,一个基本的状态转换图如下所示:

事务流程.png

从图中大家也可以看出了,只有当事务处于提交的或者中止的状态时,一个事务的生命周期才算是结束了。对于已经提交的事务来说,该事务对数据库所做的修改将永久生效,对于处于中止状态的事务,该事务对数据库所做的所有修改都会被回滚到没执行该事务之前的状态。

三,MySQL中事务的语法

我们说事务的本质其实只是一系列数据库操作,只不过这些数据库操作符合ACID特性而已,那么MySQL中如何将某些操作放到一个事务里去执行的呢?

1.开启事务

我们可以使用下边两种语句之一来开启一个事务:

  • BEGIN [WORK];
    BEGIN语句代表开启一个事务,后边的单词WORK可有可无。开启事务后,就可以继续写若干条语句,这些语句都属于刚刚开启的这个事务。
    ``` mysql> BEGIN; Query OK, 0 rows affected (0.00 sec)

mysql> 加入事务的语句…

  1. - `START TRANSACTION;`<br />`START TRANSACTION`语句和`BEGIN`语句有着相同的功效,都标志着开启一个事务,比如这样: <br />不过比`BEGIN`语句牛逼一点儿的是,可以在`START TRANSACTION`语句后边跟随几个`修饰符`,就是它们几个:

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

mysql> 加入事务的语句…

  1. - `READ ONLY`:标识当前事务是一个只读事务,也就是属于该事务的数据库操作只能读取数据,而不能修改数据。
  2. > 其实只读事务中只是不允许修改那些其他事务也能访问到的表中的数据,对于临时表来说(我们使用CREATE TMEPORARY TABLE创建的表),由于它们只能在当前会话中可见,所以只读事务其实也是可以对临时表进行增、删、改操作的。
  3. - `READ WRITE`:标识当前事务是一个读写事务,也就是属于该事务的数据库操作既可以读取数据,也可以修改数据。
  4. - `WITH CONSISTENT SNAPSHOT`:启动一致性读。
  5. 比如我们想开启一个只读事务的话,直接把`READ ONLY`这个修饰符加在`START TRANSACTION`语句后边就好,比如这样:

START TRANSACTION READ ONLY;

  1. 如果我们想在`START TRANSACTION`后边跟随多个`修饰符`的话,可以使用逗号将`修饰符`分开,比如开启一个只读事务和一致性读,就可以这样写:

START TRANSACTION READ ONLY, WITH CONSISTENT SNAPSHOT;

  1. 或者开启一个读写事务和一致性读,就可以这样写:

START TRANSACTION READ WRITE, WITH CONSISTENT SNAPSHOT

  1. 不过这里需要注意的一点是,`READ ONLY``READ WRITE`是用来设置所谓的事务`访问模式`的,就是以只读还是读写的方式来访问数据库中的数据,一个事务的访问模式不能同时既设置为`只读`的也设置为`读写`的,所以我们不能同时把`READ ONLY``READ WRITE`放到`START TRANSACTION`语句后边。另外,如果我们不显式指定事务的访问模式,那么该事务的访问模式就是`读写`模式。
  2. <a name="2ef7c8ed"></a>
  3. ### 2.提交事务
  4. 开启事务之后就可以继续写需要放到该事务中的语句了,当最后一条语句写完了之后,我们就可以提交该事务了,提交的语句也很简单:

COMMIT [WORK]

  1. `COMMIT`语句就代表提交一个事务,后边的`WORK`可有可无。转账举例:

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

mysql> UPDATE account SET balance = balance - 10 WHERE id = 1; Query OK, 1 row affected (0.02 sec) Rows matched: 1 Changed: 1 Warnings: 0

mysql> UPDATE account SET balance = balance + 10 WHERE id = 2; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0

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

  1. <a name="ea04bb38"></a>
  2. ### 3.手动中止事务
  3. 如果我们写了几条语句之后发现上边的某条语句写错了,我们可以手动的使用下边这个语句来将数据库恢复到事务执行之前的样子:

ROLLBACK [WORK]

  1. `ROLLBACK`语句就代表中止并回滚一个事务,后边的`WORK`可有可无类似的。转账举例:

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

mysql> UPDATE account SET balance = balance - 10 WHERE id = 1; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0

mysql> UPDATE account SET balance = balance + 1 WHERE id = 2; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0

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

  1. 这里需要强调一下,`ROLLBACK`语句是我们程序员手动的去回滚事务时才去使用的,如果事务在执行过程中遇到了某些错误而无法继续执行的话,事务自身会自动的回滚。
  2. > 我们这里所说的开启、提交、中止事务的语法只是针对使用黑框框时通过mysql客户端程序与服务器进行交互时控制事务的语法,如果大家使用的是别的客户端程序,比如JDBC之类的,那需要参考相应的文档来看看如何控制事务。
  3. <a name="8097e3ab"></a>
  4. ### 4.支持事务的存储引擎
  5. `MySQL`中并不是所有存储引擎都支持事务的功能,目前只有`InnoDB``NDB`存储引擎支持(NDB存储引擎不是我们的重点),如果某个事务中包含了修改使用不支持事务的存储引擎的表,那么对该使用不支持事务的存储引擎的表所做的修改将无法进行回滚。比方说我们有两个表,`tbl1`使用支持事务的存储引擎`InnoDB``tbl2`使用不支持事务的存储引擎`MyISAM`,它们的建表语句如下所示:

CREATE TABLE tbl1 ( i int ) engine=InnoDB;

CREATE TABLE tbl2 ( i int ) ENGINE=MyISAM;

  1. 我们看看先开启一个事务,写一条插入语句后再回滚该事务,`tbl1``tbl2`的表现有什么不同:

mysql> SELECT * FROM tbl1; Empty set (0.00 sec)

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

mysql> INSERT INTO tbl1 VALUES(1); Query OK, 1 row affected (0.00 sec)

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

mysql> SELECT * FROM tbl1; Empty set (0.00 sec)

  1. 可以看到,对于使用支持事务的存储引擎的`tbl1`表来说,我们在插入一条记录再回滚后,`tbl1`就恢复到没有插入记录时的状态了。再看看`tbl2`表的表现:

mysql> SELECT * FROM tbl2; Empty set (0.00 sec)

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

mysql> INSERT INTO tbl2 VALUES(1); Query OK, 1 row affected (0.00 sec)

mysql> ROLLBACK; Query OK, 0 rows affected, 1 warning (0.01 sec)

mysql> SELECT * FROM tbl2; +———+ | i | +———+ | 1 | +———+ 1 row in set (0.00 sec)

  1. 可以看到,虽然我们使用了`ROLLBACK`语句来回滚事务,但是插入的那条记录还是留在了`tbl2`表中。
  2. <a name="115de33c"></a>
  3. ### 5.自动提交
  4. `MySQL`中有一个系统变量`autocommit`

mysql> SHOW VARIABLES LIKE ‘autocommit’; +———————-+———-+ | Variable_name | Value | +———————-+———-+ | autocommit | ON | +———————-+———-+ 1 row in set (0.01 sec)

  1. 可以看到它的默认值为`ON`,也就是说默认情况下,如果我们不显式的使用`START TRANSACTION`或者`BEGIN`语句开启一个事务,那么每一条语句都算是一个独立的事务,这种特性称之为事务的`自动提交`。假如我们在转账时不以`START TRANSACTION`或者`BEGIN`语句显式的开启一个事务,那么下边这两条语句就相当于放到两个独立的事务中去执行:

UPDATE account SET balance = balance - 10 WHERE id = 1; UPDATE account SET balance = balance + 10 WHERE id = 2;

  1. 当然,如果我们想关闭这种`自动提交`的功能,可以使用下边两种方法之一:
  2. - 显式的的使用`START TRANSACTION`或者`BEGIN`语句开启一个事务。<br />这样在本次事务提交或者回滚前会暂时关闭掉自动提交的功能。
  3. - 把系统变量`autocommit`的值设置为`OFF`,就像这样: <br />这样的话,我们写入的多条语句就算是属于同一个事务了,直到我们显式的写出`COMMIT`语句来把这个事务提交掉,或者显式的写出`ROLLBACK`语句来把这个事务回滚掉。

SET autocommit = OFF;

  1. <a name="98d320c7"></a>
  2. ### 6.隐式提交
  3. 当我们使用`START TRANSACTION`或者`BEGIN`语句开启了一个事务,或者把系统变量`autocommit`的值设置为`OFF`时,事务就不会进行`自动提交`,但是如果我们输入了某些语句之后就会`悄悄的`提交掉,就像我们输入了`COMMIT`语句了一样,这种因为某些特殊的语句而导致事务提交的情况称为`隐式提交`,这些会导致事务隐式提交的语句包括:
  4. - 定义或修改数据库对象的数据定义语言(Data definition language,缩写为:`DDL`)。<br />所谓的数据库对象,指的就是`数据库`、`表`、`视图`、`存储过程`等等这些东西。当我们使用`CREATE`、`ALTER`、`DROP`等语句去修改这些所谓的数据库对象时,就会隐式的提交前边语句所属于的事务,就像这样:

BEGIN;

SELECT … # 事务中的一条语句 UPDATE … # 事务中的一条语句 … # 事务中的其它语句

CREATE TABLE … # 此语句会隐式的提交前边语句所属于的事务

  1. - 隐式使用或修改`mysql`数据库中的表<br />当我们使用`ALTER USER``CREATE USER``DROP USER``GRANT``RENAME USER``REVOKE``SET PASSWORD`等语句时也会隐式的提交前边语句所属于的事务。
  2. - 事务控制或关于锁定的语句<br />当我们在一个事务还没提交或者回滚时就又使用`START TRANSACTION`或者`BEGIN`语句开启了另一个事务时,会隐式的提交上一个事务,比如这样: <br />或者当前的`autocommit`系统变量的值为`OFF`,我们手动把它调为`ON`时,也会隐式的提交前边语句所属的事务。<br />或者使用`LOCK TABLES``UNLOCK TABLES`等关于锁定的语句也会隐式的提交前边语句所属的事务。

BEGIN;

SELECT … # 事务中的一条语句 UPDATE … # 事务中的一条语句 … # 事务中的其它语句

BEGIN; # 此语句会隐式的提交前边语句所属于的事务

  1. - 加载数据的语句<br />比如我们使用`LOAD DATA`语句来批量往数据库中导入数据时,也会隐式的提交前边语句所属的事务。
  2. - 关于`MySQL`复制的一些语句<br />使用`START SLAVE``STOP SLAVE``RESET SLAVE``CHANGE MASTER TO`等语句时也会隐式的提交前边语句所属的事务。
  3. - 其它的一些语句<br />使用`ANALYZE TABLE``CACHE INDEX``CHECK TABLE``FLUSH` `LOAD INDEX INTO CACHE``OPTIMIZE TABLE``REPAIR TABLE``RESET`等语句也会隐式的提交前边语句所属的事务。
  4. <a name="98e82d96"></a>
  5. ### 7.保存点
  6. 如果你开启了一个事务,并且已经敲了很多语句,忽然发现上一条语句有点问题,你只好使用`ROLLBACK`语句来让数据库状态恢复到事务执行之前的样子,然后一切从头再来,总有一种一夜回到解放前的感觉。所以MySQL提出了一个`保存点`(英文:`savepoint`)的概念,就是在事务对应的数据库语句中打几个点,我们在调用`ROLLBACK`语句时可以指定会滚到哪个点,而不是回到最初的原点。定义保存点的语法如下:

SAVEPOINT 保存点名称;

  1. 当我们想回滚到某个保存点时,可以使用下边这个语句(下边语句中的单词`WORK``SAVEPOINT`是可有可无的):

ROLLBACK [WORK] TO [SAVEPOINT] 保存点名称;

  1. 不过如果`ROLLBACK`语句后边不跟随保存点名称的话,会直接回滚到事务执行之前的状态。
  2. 如果我们想删除某个保存点,可以使用这个语句:

RELEASE SAVEPOINT 保存点名称;

  1. 下边还是以转账的例子展示一下`保存点`的用法,在执行完扣除第一个账户的钱`10`元的语句之后打一个`保存点`

mysql> SELECT * FROM account; +——+————+————-+ | id | name | balance | +——+————+————-+ | 1 | 狗哥 | 11 | | 2 | 猫爷 | 2 | +——+————+————-+ 2 rows in set (0.00 sec)

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

mysql> UPDATE account SET balance = balance - 10 WHERE id = 1; Query OK, 1 row affected (0.01 sec) Rows matched: 1 Changed: 1 Warnings: 0

mysql> SAVEPOINT s1; # 一个保存点 Query OK, 0 rows affected (0.00 sec)

mysql> SELECT * FROM account; +——+————+————-+ | id | name | balance | +——+————+————-+ | 1 | 狗哥 | 1 | | 2 | 猫爷 | 2 | +——+————+————-+ 2 rows in set (0.00 sec)

mysql> UPDATE account SET balance = balance + 1 WHERE id = 2; # 更新错了 Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0

mysql> ROLLBACK TO s1; # 回滚到保存点s1处 Query OK, 0 rows affected (0.00 sec)

mysql> SELECT * FROM account; +——+————+————-+ | id | name | balance | +——+————+————-+ | 1 | 狗哥 | 1 | | 2 | 猫爷 | 2 | +——+————+————-+ 2 rows in set (0.00 sec) ```