3.1 事务的特性

  • 原子性:一个事务中的操作要么全部完成,要么全部不完成。
  • 一致性:事务操作前和操作后,数据满足完整性约束,数据库保持一致性状态。
  • 隔离性:数据库允许多个并发事务的同时,防止多个事务并发时由于交叉执行而导致数据不一致。
  • 持久性:事务处理结束后,对数据的修改是永久的。

InnoDB通过redo log保证持久性,通过undo log保证原子性,隔离性通过MVCC机制或锁机制保证,一致性则通过持久性+原子性+隔离性来保证。

3.2 并发事务会引发什么问题

脏读

如果一个事务读到了另一个事务还没有提交的修改数据,就意味着发生了脏读。

不可重复度

在一次事务内多次读取同一个数据,如果出现两次读到的数据不一致的情况,就意味着发生了不可重复读现象。

幻读

在一个事务内多次查询某个符合查询条件的记录数量,如果出现前后两次查询到的记录数量不一致的情况,就意味着发生了幻读现象。

3.3 事务的隔离级别

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

MySQL InnoDB 引擎的默认隔离级别虽然是「可重复读」,但是它很大程度上避免幻读现象(并不是完全解决了

  • 针对快照读(普通 select 语句),是通过 MVCC 方式解决了幻读,因为可重复读隔离级别下,事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,即使中途有其他事务插入了一条数据,是查询不出来这条数据的,所以就很好了避免幻读问题。
  • 针对当前读(select … for update 等语句),是通过 next-key lock(记录锁+间隙锁)方式解决了幻读,因为当执行 select … for update 语句的时候,会加上 next-key lock,如果有其他事务在 next-key lock 锁范围内插入了一条记录,那么这个插入语句就会被阻塞,无法成功插入,所以就很好了避免幻读问题。

3.4 Read View在MVCC中如何工作?

Read View中的四个字段

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

聚簇索引记录的两个隐藏列

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

一个事务去访问记录时,除了自己更新的记录总是可见的之外:

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

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

3.5 可重复读是如何工作的

可重复读隔离级别是启动事务时生成一个 Read View,然后整个事务期间都在用这个 Read View

3.6 读已提交是如何工作的

读提交隔离级别是在每次读取数据时,都会生成一个新的 Read View

3.7 幻读现象

案例1

在可重复读隔离级别下,事务 A 第一次执行普通的 select 语句时生成了一个 ReadView,之后事务 B 向表中新插入了一条 id = 5 的记录并提交。接着,事务 A 对 id = 5 这条记录进行了更新操作,在这个时刻,这条新记录的 trx_id 隐藏列的值就变成了事务 A 的事务 id,之后事务 A 再使用普通 select 语句去查询这条记录时就可以看到这条记录了,于是就发生了幻读。

因为这种特殊现象的存在,所以我们认为 MySQL Innodb 中的 MVCC 并不能完全避免幻读现象

案例2

  • T1 时刻:事务 A 先执行「快照读语句」:select * from t_test where id > 100 得到了 3 条记录。
  • T2 时刻:事务 B 往插入一个 id= 200 的记录并提交;
  • T3 时刻:事务 A 再执行「当前读语句」 select * from t_test where id > 100 for update 就会得到 4 条记录,此时也发生了幻读现象。

要避免这类特殊场景下发生幻读的现象的话,就是尽量在开启事务之后,马上执行 select … for update 这类当前读的语句,因为它会对记录加 next-key lock,从而避免其他事务插入一条新记录。

MySQL 可重复读隔离级别并没有彻底解决幻读,只是很大程度上避免了幻读现象的发生。