1、事务简介

数据库将现实世界里的对数据的操作(比如取钱、存钱等)映射到数据库的事务中,事务(transcation)就是一个或者多个数据库的操作,这些操作满足事务的四大特性。

1.1 事务的状态

事务的状态有以下5种:

  • 活动的(active)
  • 部分提交的(partially committed)
  • 失败的(failed)
  • 中止的(aborted)
  • 提交的(committed)

下面具体介绍事务的5种状态。
(1)活动的(active)
事务对应的数据库操作正在执行过程中,此时事务处于活动的状态。
(2)部分提交的(partially committed)
事务对应的一些列数据库的操作已经执行完毕,还没有将执行结果从数据库刷新到磁盘中,此时事务处于部分提交的状态。
(3)失败的(failed)
当事务处在活动的或者部分提交的状态时,可能遇到了某些错误(数据库自身错误、操作系统错误或者节点下电了)而无法继续执行,或者人为停止当前事务的执行,此时事务处于失败的状态。
(4)中止的(aborted)
数据库状态变为失败的状态时,会进行回滚操作,即将数据库恢复到事务执行前的状态,回滚后事务处于中止的状态。
(5)提交的(committed)
当一个处在部分提交的状态的事务将事务执行的结果从内存刷新到磁盘后,此时事务处于提交的状态。

状态转移图如下:
事务 - 图1

1.2 事务的四大特性(ACID)

什么是事务的特性?上面讲到数据库将现实世界里涉及数据的处理操作映射到数据库中就是事务,现实世界里操作数据肯定要遵循一些状态转换的规则,这些规则映射到数据库的世界中就是事务的特性(即事务要遵循的状态转换规则)。事务的四大特性如下:

  • 原子性(Atomicity)
  • 隔离性(Isolation)
  • 一致性(Consistency)
  • 持久性(Durability)

下面具体介绍一下这4大特性。
(1)原子性(Atomicity)
原子性是针对于一组操作说的,这组操作有原子性,是说这组操作,要么都发生,要么都不发生。
举例:A账户向B账户转100元,A账户金额减少100元、B账户金额增加100元这两个操作要么都发生,要么都不发生,这两个操作具备原子性。
(2)隔离性(Isolation)
事务之间的运行是互不影响的,事务的隔离性很重要,后面介绍的脏读幻读等异常场景,以及MySQL中MVCC机制和锁机制都跟事务的隔离性有关。
举例:事务A写入账户的金额,事务B读取账户的金额,在事物A没有提交前,事务B读取的a账户的金额依然是改变前的金额。
(3)一致性(Consistency)
事务的运行不会改变数据库的一致性约束。
举例:现有完整性约束a+b=10,事务的发生改变了a的值,就一定要改变b的值,使事务结束后依然满足a+b=10的约束。
需要注意的是:事务的原子性和隔离性都是保证事务一致性的一种手段。
(4)持久性(Durability)
事务一旦提交了,它所作出的修改会永久地保存在数据库上,即使数据库宕机也不会丢失。MySQL的redo日志就是为了满足持久性提出的。

1.3 事务相关的基本sql语句

1.3.1 BEGIN

  1. # 开启事务
  2. BEGIN
  3. ... 这里执行事务包含的数据库操作的sql语句
  4. # 提交事务
  5. COMMIT;

举个例子:

  1. mysql> BEGIN;
  2. Query OK, 0 rows affected (0.00 sec)
  3. mysql> UPDATE account SET balance = balance - 10 WHERE id = 1;
  4. Query OK, 1 row affected (0.02 sec)
  5. Rows matched: 1 Changed: 1 Warnings: 0
  6. mysql> UPDATE account SET balance = balance + 10 WHERE id = 2;
  7. Query OK, 1 row affected (0.00 sec)
  8. Rows matched: 1 Changed: 1 Warnings: 0
  9. mysql> COMMIT;
  10. Query OK, 0 rows affected (0.00 sec)

BEGINCOMMIT语句也是MySQL中事务语句使用最多的。

1.3.2 START TRANSCATION

  1. # 开启事务
  2. START TRANSCATION [修饰符]
  3. ... 这里执行事务包含的数据库操作的sql语句
  4. # 提交事务
  5. COMMIT;

修饰符是可选的,有以下几种:

  • READ ONLY:标识当前事务是一个只读事务,也就是属于该事务的数据库操作只能读取数据,而不能修改数据;
  • READ WRITE:标识当前事务是一个读写事务,也就是属于该事务的数据库操作既可以读取数据,也可以修改数据;
  • WITH CONSISTENT SNAPSHOT:启动一致性读。

修饰符可以跟多个,不同修饰符之间用逗号分隔开,比如:

  1. START TRANSACTION READ ONLY, WITH CONSISTENT SNAPSHOT;
  2. START TRANSACTION READ WRITE, WITH CONSISTENT SNAPSHOT

一个事务的访问模式不能同时既设置为只读的也设置为读写的,所以我们不能同时把READ ONLYREAD WRITE放到START TRANSACTION语句后边。另外,如果我们不显式指定事务的访问模式,那么该事务的访问模式就是读写模式。

1.4 MySql支持事务的存储引擎

MySQL常见的存储引擎有InnoDBMyISAMMemory等,目前只有InnoDBNDB存储引擎支持事务,且对事务支持的程度不同。本文介绍的事务都是InnoDB存储引擎支持的事务。

2、事务并发产生的问题

本节介绍一下事务并发(没有保证隔离性)会导致异常问题的4种场景,按照问题严重性从大到小排序,分别是脏写(Dirty Write)、脏读(Dirty Read)、不可重复读(Non-Repeatable Read)和幻读(Phantom)。

2.1 脏写(Dirty Write)

如果一个事务修改了另一个未提交的事务修改过的数据,就造成了脏写。
事务 - 图2
如上图,Session B rollback后,把Session A的修改也给回滚了,Session A发现自己明明做了修改并提交成功,但实际上记录并没有更新,这就是脏写。

2.2 脏读(Dirty Read)

如果一个事务读到了另一个未提交事务修改过的数据,就造成了脏读。
事务 - 图3
如上图,Session A读到了Session B更新的数据“关羽”,然后Session A COMMIT,之后Session B ROLLBACK,那么Session A相当于读取到了一个不存在的数据“关羽”,这就是脏读。

2.3 不可重复读(Non-Repeatable Read)

事务A对一条记录不断地读,事务B对这条记录不断的写,导致每次事务A查询时得到的记录都是不一样的,就造成了不可重复读。
事务 - 图4
如上图,Session A这边没有做UPDATE,仅是做SELECT,站在Session A的角度每次SELECT时数据应该都是不变的,但由于Session B一直在做UPDATE,Session A发现我明明没有做UPDATE,但每次SELECT的结果就是不一样,这就是不可重复读。

2.4 幻读(Phantom)

事务A对一条记录不断的查询,事务B同时向这张表里INSERT符合事务A查询条件的记录,导致每次事务A按照相同的条件查询时得到的记录数目都是不一样的,就造成了幻读。新增的记录又叫幻影记录。
事务 - 图5
关于幻读需要注意两点:

  • 上图中,如果Session B不是INSERT而是DELETE,删除记录使按统一条件查询时得到的记录数减小,这样算幻读么?不算!幻读强调后读取到了之前没有读到的记录,所以仅对INSERT成立;
  • 关于幻读和不可重复读的区别:不可重复读是另一个写事务同时在UPDATE,而幻读是另一个写事务同时在INSERT,因此不可重复读的结果强调每次读取时的记录内容不同,幻读的结果强调每次读取记录时都读取到了之前没读取到的记录,即记录数在递增。

    3、事务的隔离级别

    数据库的服务端肯定存在同一时刻接收到来自不同客户端的访问请求,这就是所谓的事务的并发,跟Java多线程概念上有相同的地方。与Java里锁的机制类似,当有多个事务同时访问(尤其是并发写的情况)同一份记录时,应该通过某些机制让这些事务串行执行以此保证事务的隔离性,但服务器又想同一时刻尽可能多地处理访问请求,因此数据库设立了不同的隔离级别,选择不同的数据库隔离级别,可以达到牺牲部分隔离性而获得性能上提升的效果。

    3.1 SQL标准的四种隔离级别

    关于隔离级别,SQL标准制定了四种隔离级别,如下:

  • **READ UNCOMMITTED**:未提交读;

  • **READ COMMITTED**:已提交读;
  • **REPEATABLE READ**:可重复读;
  • **SERIALIZABLE**:可串行化。

前面提到过为了追求性能,SQL会牺牲一部分并发安全性,SQL标准中规定,针对不同的隔离级别,并发事务可以发生不同严重程度的问题,严重程度从大到小依次为:脏写、脏读、不可重复读和幻读,具体如下:

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

此外,由于脏写问题太严重,无论哪种隔离级别都不允许脏写的情况发生,事务的隔离级别和异常场景的对应关系用文字表述一下:

  • READ UNCOMMITTED隔离级别下,可能发⽣脏读、不可重复读和幻读问题;
  • READ COMMITTED隔离级别下,不可能发生脏读,但可能发⽣不可重复读和幻读;
  • REPEATABLE READ隔离级别下,不可能发生脏读和不可重复读,但可能发生幻读;
  • SERIALIZABLE隔离级别下,各种问题都不可能发⽣。

    3.2 MySql支持的事务隔离级别

    不同数据库对SQL标准规定的四种事务隔离级别支持不一样,Oracle就只⽀持READ COMMITTEDSERIALIZABLE隔离级别,MySQL四种隔离级别均支持,但针对REPEATABLE READ隔离级别,MySQL是不允许幻读产生的。
    MySQL默认的事务隔离级别为REPEATABLE READ,可以通过SQL语句修改数据库支持的事务隔离级别:
    1. SET [GLOBAL|SESSION] TRANSACTION ISOLATION LEVEL level;
    level的可选值有4个,如下:
    1. level: {
    2. REPEATABLE READ
    3. | READ COMMITTED
    4. | READ UNCOMMITTED
    5. | SERIALIZABLE
    6. }
    实际开发中,一般会在SpringBoot项目里的@Transcation注解里设置事务的隔离级别和事务 传播性,普通业务这两个参数也不用设置,用默认的就可以。

    3.3 事务的传播机制

    这一部分不是MySQL的事务特性,而是Spring中事务的特性,因为面试中聊事务肯定会问Spring中的事务传播机制,这里不要混淆了,Spring中的事务传播机制可以参考我这篇文章:
    https://www.yuque.com/docs/share/c4b76815-ac10-4e9d-a651-819fe2bb2e67?# 《SpringBoot 事务》

    参考

    掘金小册
    Spring 事务传播行为
    2020年前必须掌握的数据库面试问题