MySQL 事务隔离

提到事务,我们一定都不陌生,最常用的例子就是银行转账,你的银行卡有 1000 元,要给小明转 1000 元,转账过程具体到程序⾥会有⼀系列的操作,⽐如查询余额、做加减法、更新余额等,这些操作必须保证是⼀体的,不然等程序查完之后,还没做减法之前,你这 1000 块钱,完全可以借着这个时间差再查⼀次,然后再给另外⼀个朋友转账,这种情况对于银行来说是不可接受的,这个时候就需要用到事务这个概念了。

在维基百科中,对事务的定义是:事务是数据库管理系统 (DBMS) 执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成。简单来说,事务就是要保证⼀组数据库操作,要么全部成功,要么全部失败。在 MySQL 中,事务⽀持是在引擎层实现的。所以并不是所有引擎都支持事务的,MyISAM 引擎被取代的重要原因就是他不支持事务。

事务的四大特性

事务包含四大特性,即原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability)(ACID)。

  1. 原子性(Atomicity)
    原子性是指对数据库的一系列操作,要么全部成功,要么全部失败,不可能出现部分成功的情况。以转账场景为例,一个账户的余额减少,另一个账户的余额增加,这两个操作一定是同时成功或者同时失败的。
  2. 一致性(Consistency)
    一致性是指数据库的完整性约束没有被破坏,在事务执行前后都是合法的数据状态。这里的一致可以表示数据库自身的约束没有被破坏,比如某些字段的唯一性约束、字段长度约束等等;还可以表示各种实际场景下的业务约束,比如上面转账操作,一个账户减少的金额和另一个账户增加的金额一定是一样的。
  3. 隔离性(Isolation)
    隔离性指的是多个事务彼此之间是完全隔离、互不干扰的。隔离性的最终目的也是为了保证一致性。
  4. 持久性(Durability)
    持久性是指只要事务提交成功,那么对数据库做的修改就被永久保存下来了,不可能因为任何原因再回到原来的状态。

事务的状态

根据事务所处的不同阶段,事务大致可以分为以下 5 个状态:

  1. 活动的(active)
    当事务对应的数据库操作正在执行过程中,则该事务处于活动状态。
  2. 部分提交的(partially committed)
    当事务中的最后一个操作执行完成,但还未将变更刷新到磁盘时,则该事务处于部分提交状态。
  3. 失败的(failed)
    当事务处于活动或者部分提交状态时,由于某些错误导致事务无法继续执行,则事务处于失败状态。
  4. 中止的(aborted)
    当事务处于失败状态,且回滚操作执行完毕,数据恢复到事务执行之前的状态时,则该事务处于中止状态。
  5. 提交的(committed)
    当事务处于部分提交状态,并且将修改过的数据都同步到磁盘之后,此时该事务处于提交状态。

这几种状态的状态转移如下:

事务隔离级别

事务的四大特性之一是必须具有隔离性。实现隔离性最简单的方式就是不允许事务并发,每个事务都排队执行,但是这样的性能会很差。所以为了兼顾事务的隔离性和性能,事务支持不同的隔离级别。

为了方便表述后续的内容,我们先建一张示例表。

1
2
mysql> create table T(c int) engine=InnoDB;
insert into T(c) values(1);

事务并发执行遇到的问题

在事务并发执行时,如果不进行任何控制,可能会出现脏读(dirty read)、不可重复读(non-repeatable read)、幻读 (phantom read)的问题

脏读(Dirty Read)

脏读是指一个事务读到了其它事务未提交的数据。

序号 事务 A 事务 B
启动事务
启动事务
查询得到值 1
将 1 改为 2
查询得到值 V1
提交事务 B
提交事务 A

如上表,事务 A 和事务 B 分别开启了一个事务,事务 B 中的事务先将 c 列为 1 的记录更新为 2,然后事务 A 中的事务再去查询 c 列 的记录,读到列 c 的 V1 值为 2,这时候事务 B 虽然还没有提交,但是结果已经被 A 看到了,这种现象就称之为脏读

不可重复读(non-repeatable read)

不可重复读指的是在一个事务执行过程中,读取到其它事务已提交的数据,导致两次读取的结果不一致。

序号 事务 A 事务 B
启动事务
启动事务
查询得到值 1
将 1 改为 2
查询得到值 V1
提交事务 B
查询得到值 V2
提交事务 A

如上表,事务 A 和事务 B 分别开启了一个事务,事务 B 中的事务先将 c 列为 1 的记录更新为 2,在事务 B 提交之前,事务 A 读取到的值 V1 是 1,事务 B 提交后,事务 A 读取到的值 V2 是 2。不管事务 A 有没有提交,事务 B 的更新在提交后就能被 A 看到,这种现象就称之为不可重复读

幻读 (phantom read)

幻读是指的是在一个事务执行过程中,读取到了其他事务新插入数据,导致两次读取的结果不一致。

序号 事务 A 事务 B
启动事务
查询 c < 10 启动事务
插入新行 c=2
提交事务 B
查询 c < 10
提交事务 A

如上表,事务 A 和事务 B 分别开启了一个事务,事务 A 根据条件 c<10 进行查询,得到记录 c=1,事务 B 中的事务 插入一条 c=2 的新记录,提交事务,事务 A 再次根据条件 c<10 进行查询,得到记录 c=1 和 c=2,这种现象也被称之为幻读

四种隔离级别

在谈隔离级别之前,⾸先要知道,隔离级别越高,一致性越强,但是效率就会越低。为了解决上面提到的读一致性问题,设立了四种隔离级别。

SQL标准的事务隔离级别包括:读未提交(read uncommitted)、读提交(read committed)、可重复读(repeatable read) 和串⾏化(serializable )。

  • 读未提交,指⼀个事务还没提交时,它做的变更就能被别的事务看到。
  • 读提交,指⼀个事务提交之后,它做的变更才会被其他事务看到。
  • 可重复读,指⼀个事务执⾏过程中看到的数据,总是跟这个事务在启动时看到的数据是⼀致的。当然在可重复读隔离级别下,未提交变更对其他事务也是不可⻅的。
  • 串⾏化,顾名思义是对于同⼀⾏记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前⼀个事务执⾏完成,才能继续执⾏,整个执行流程是串行的。

还是以之前的表为例:

序号 事务 A 事务 B
启动事务
启动事务
查询得到值 1
将 1 改为 2
查询得到值 V1
提交事务 B
查询得到值 V2
提交事务 A
查询得到值 V3

不同的隔离级别下,事务 A 会有不同的返回结果,也就是表中 V1、V2、V3 的返回值会不同。

  • 若隔离级别是“读未提交”,则 V1 的值就是 2。这时候事务 B 虽然还没有提交,但是结果已经被 A 看到了。因此,V2、V3 也都 是 2。
  • 若隔离级别是“读提交”,则 V1 是 1,V2 的值是 2。事务 B 的更新在提交后才能被 A 看到。所以, V3 的值也是 2。
  • 若隔离级别是“可重复读”,则 V1、V2 是 1,V3 是 2。之所以 V2 还是 1,遵循的就是这个要求:事务在执⾏期间看到的数据前 后必须是⼀致的。
  • 若隔离级别是“串⾏化”,则在事务 B 执⾏“将 1 改成 2”的时候,会被锁住。直到事务 A 提交后,事务 B 才可以继续执⾏。所以从 A 的⻆度看, V1、V2 值是 1,V3 的值是 2。

在实现上,数据库⾥⾯会创建⼀个视图,访问的时候以视图的逻辑结果为准。在“可重复读”隔离级别下,这个视图是在事务启动时创建的,整个事务存在期间都⽤这个视图。在“读提交”隔离级别下,这个视图是在每个 SQL 语句开始执⾏的时候创建的。 这⾥需要注意的是,“读未提交”隔离级别下直接返回记录上的最新值,没有视图概念;⽽“串⾏化”隔离级别下直接⽤加锁的⽅式来避免并⾏访问。

各个隔离级别下可能出现的读一致性问题如下:

隔离级别 脏读 不可重复读 幻读
未提交读(READ UNCOMMITTED) 可能 可能 可能
已提交读(READ COMMITTED) 不可能 可能 可能
可重复读(REPEATABLE READ) 不可能 不可能 可能(对 InnoDB 不可能)
串行化(SERIALIZABLE) 不可能 不可能 不可能

事务隔离的实现

理解了事务的隔离级别,我们再来看看事务隔离具体是怎么实现的,这里我们重点关注“可重复读”。

从上一篇我们知道在 MySQL 中,每条记录在更新的时候都会同时记录⼀条回滚操作 (undo log)。记录上的最新值,通过回滚操作,都可以得到前⼀个状态的值。

假设⼀个值从 1 被按顺序改成了 2、3、4,在回滚⽇志⾥⾯就会有类似下⾯的记录。

当前值是 4,但是在查询这条记录的时候,不同时刻启动的事务会有不同的 read-view。如图中看到的,在视图 A、B、C⾥⾯, 这⼀个记录的值分别是 1、2、4,同⼀条记录在系统中可以存在多个版本,就是数据库的多版本并发控制(MVCC)。对于 read-view A,要得到 1,就必须将当前值依次执⾏图中所有的回滚操作得到。

并且,即使现在有另外⼀个事务正在将 4 改成 5,这个事务跟 read-view A、B、C 对应的事务是不会冲突的。

对于 undo log 来说,当系统⾥没有⽐这个 undo log 更早的 read-view 的时候 undo log 就会被删除。

总结

在本篇中,我们了解了什么是事务,事务的四大特性 (ACID),事务的状态和事务的隔离级别及其实现。

提几个问题检验一下阅读效果:

  1. 事务的概念是什么,有哪些特性?
  2. MySQL 事务的状态有哪些,他们直接如何状态转移?
  3. MySQL 事务并发时可能遇到哪些问题?请举例说明
  4. MySQL 的事务隔离级别都有哪些?请举例说明
  5. 读已提交,可重复读是如何构建视图的?
  6. 事务隔离是如何实现的?