事务的 ACID 特性

数据库的事务(Transaction):保证一系列操作是不可分割的,要么全部执行成功,要么全部失败,不允许出现中间状态的数据

事务的流程:

  1. 开启事务
  2. 执行一系列操作
  3. 过程中一切正常,则提交事务,使得对数据库所做的修改永久生效
  4. 如果中途发生中断或错误,则回滚事务,不对数据库产生任何修改

事务由存储引擎来实现:

  • InnoDB 支持事务
  • MyISAM 不支持事务

实现事务必须要遵守 4 个特性:

  • 原子性(Atomicity):一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚到事务开始前的状态,就像这个事务从来没有执行过一样。即:事务不可分割、不可约简
  • 一致性(Consistency):事务操作前和操作后,数据满足完整性约束,数据库保持一致性状态。这表示写入的数据必须完全符合所有的预设约束、触发器、级联回滚等
  • 隔离性(Isolation):数据库允许多个并发事务同时对其数据进行读写,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。多个事务同时使用相同的数据时,不会相互干扰,每个事务都有一个完整的数据空间,对其他并发事务是隔离的
  • 持久性(Durability):事务处理结束后,对数据的修改是永久的,即便系统故障也不会丢失

InnoDB 如何保证事务的 ACID?

  • 持久性是通过 redo log(重做日志)来保证的
  • 原子性是通过 undo log(回滚日志)来保证的
  • 隔离性是通过 MVCC(多版本并发控制)或锁机制来保证的
  • 一致性则是通过持久性 + 原子性 + 隔离性来保证

本文重点介绍事务的隔离性

并行事务引发的问题

  • 脏读:读到其他事务未提交的数据
  • 不可重复读:前后读取的数据不一致
  • 幻读:前后读取的记录数量不一致

三个现象的严重性排序:脏读 > 不可重复读 > 幻读

脏读

如果一个事务「读到」了另一个「未提交事务修改过的数据」

不可重复读

在一个事务内多次读取同一个数据,出现前后读到的数据不一致情况

幻读

在一个事务内多次查询某个符合查询条件的「记录数量」,出现前后两次查询到的记录数量不一致情况

事务的隔离级别

SQL 标准提出了四种隔离级别来规避脏读、不可重复读、幻读的现象,隔离级别越高,性能效率就越低,这四个隔离级别如下:

  • 读未提交(read uncommitted):指一个事务还没提交时,它做的变更就能被其他事务看到
  • 读已提交(read committed):指一个事务提交之后,它做的变更才能被其他事务看到
  • 可重复读(repeatable read)(InnoDB 默认隔离级别):指一个事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的
  • 串行化(serializable):会对记录加上读写锁,在多个事务对这条记录进行读写操作时,如果发生了读写冲突,后访问的事务必须等前一个事务执行完成,才能继续执行

按隔离水平高低排序:串行化 > 可重复读 > 读已提交 > 读未提交

针对不同的隔离级别,并发事务可能发生的现象:

  • 读未提交:可能发生脏读、不可重复读、幻读现象
  • 读已提交:可能发生不可重复读、幻读现象
  • 可重复读:可能发生幻读现象
  • 串行化:都不可能发生

不同的数据库厂商对 SQL 标准中规定的 4 种隔离级别的支持不一样,有的数据库只实现了其中几种隔离级别,MySQL 虽然支持 4 种隔离级别,但是与 SQL 标准中规定的各级隔离级别允许发生的现象有些出入

MySQL InnoDB 的默认隔离级别「可重复读」,可以很大程度上避免(不是彻底避免)幻读现象的发生(详见

举例说明这四种隔离级别:

  • 读未提交:B 修改余额后,虽然没有提交事务,但此时的余额已经可以被 A 看见了,V1、V2、V3 = 200
  • 读已提交:B 修改余额后,因为没有提交事务,所以 V1 = 100;等 B 提交完后,最新的余额数据才能被 A 看见,V2、V3 = 200
  • 可重复读:A 只能看见启动事务时的数据,所以 V1、V2 = 100,当 A 提交事务后,就能看见最新的数据,V3 = 200
  • 串行化:B 在执行修改时,由于此前 A 执行了读操作,这样就发生了读写冲突,于是 B 就会被锁住,直到 A 提交后,B 才可以继续执行,所以从 A 的角度看,V1、V2 = 100,V3 = 200

四种隔离级别实现方法:

  • 读未提交:因为可以读到未提交事务修改的数据,所以直接读取最新的数据就好了
  • 串行化:通过加读写锁的方式来避免并行访问
  • 读已提交和可重复读:通过 Read View 实现,区别在于创建 Read View 的时机不同。Read View 可以理解成一个数据快照
    • 读已提交:在「每个语句执行前」都会重新生成一个 Read View
    • 可重复读:「启动事务时」生成一个 Read View,然后整个事务期间都在用这个 Read View

注意,执行「开始事务」命令,并不意味着启动了事务:

  • begin / start transaction:执行该命令后,执行了第一条 select 语句,才是事务真正启动的时机
  • start transaction with consistent snapshot:执行该命令后,马上启动事务

MVCC(多版本并发控制)

读已提交和可重复读隔离级别才会用到

Read View:

  • creator_trx_id:创建该 Read View 的事务的事务 id
  • m_ids:创建 Read View 时,当前数据库中「活跃事务」的事务 id 列表。“活跃事务”指的是,启动了但还没提交的事务
  • min_trx_id:创建 Read View 时,当前数据库中「活跃事务」中事务 id 最小的事务。也就是 m_ids 的最小值。
  • max_trx_id:并不是 m_ids 的最大值,而是创建 Read View 时当前数据库中应该给下一个事务的 id 值。也就是全局事务中最大的事务 id 值 +1

InnoDB 数据表的聚簇索引记录中都包含下面两个隐藏列

  • trx_id:当一个事务对某条聚簇索引记录进行改动时,就会==把该事务的事务 id 记录在 trx_id 隐藏列里==
  • roll_pointer:每次对某条聚簇索引记录进行改动时,都会把旧版本的记录写入到 undo 日志中,然后这个隐藏列是个指针,指向每一个旧版本记录

一个事务访问记录时,除了自己的更新总是可见之外,还有这几种情况:

  • 如果记录的 trx_id 小于 Read View 的 min_trx_id,表示这个版本的记录是在创建 Read View 前已经提交的事务生成的,所以该版本的记录对当前事务可见
  • 如果记录的 trx_id 大于等于 Read View 的 max_trx_id,表示这个版本的记录是在创建 Read View 后才启动的事务生成的,所以该版本的记录对当前事务不可见
  • 如果记录的 trx_id 在 Read View 的 min_trx_id 和 max_trx_id 之间,需要判断 trx_id 是否在 m_ids 列表中:
    • 在:表示生成该版本记录的事务依然活跃着(还没提交),所以该版本的记录对当前事务不可见
    • 不在:表示生成该版本记录的活跃事务已经提交,所以该版本的记录对当前事务可见

版本链:

当还未提交事务修改记录时,并不是修改真正的数据记录,而是生成相应的 undo log,并以链表的方式串联起来,形成版本链

undo log 数据被放到回滚段空间

这种通过「版本链」来控制并发事务访问同一个记录时的行为就叫 MVCC(多版本并发控制)