4.事务
ACID
-
原子性(Atomicity): 一个事务中的所有操作,要么全部完成,要么全部不完成。在执行过程中发生错误,会被回滚到事务开始前的状态。--- 通过 undo log(回滚日志)保证。
-
一致性(Consistency): 事务操作前后,数据满足完整性约束,数据库保持一致性状态。--- 通过持久性+原子性+隔离性保证。
-
隔离性(Isolation): 数据库允许多个并发事务同时对数据进行读写操作,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。--- 通过 MVCC(多版本并发控制)或锁机制来保证。
-
持久性(Durability): 事务处理结束后,对数据的修改是永久的,即便系统故障也不会丢失。--- 通过 redo log(重做日志)来保证。
隔离级别
在同时处理多个事务的时候,就可能出现脏读(dirty read)、不可重复读(non-repeatable read)、幻读(phantom read)的问题:
-
脏读: 如果事务 A「读到」了事务 B「未提交事务修改过的数据」,就意味着发生了「脏读」现象,因为 B 可以回滚,A 读到的数据就是过期数据。
-
**不可重复读:**在一个事务内多次读取同一个数据,如果出现前后两次读到的数据不一样的情况,就意味着发生了「不可重复读」现象。A 事务读取了余额,继续执行,B 事务更新并提交了余额,A 事务再次读取,两次结果不一致。
-
幻读: 在一个事务内多次查询某个符合查询条件的**「记录数量」**,如果出现前后两次查询到的记录数量不一样的情况,就意味着发生了「幻读」现象。与不可重复读类似,也是一个事务两次读取中间另一个事务进行了修改。
事务的隔离级别:
-
**读未提交(read uncommitted):**一个事务还没有提交时,他做的变更能被其他事务看到。(会出现 脏读 - 不可重复读 - 幻读)
-
**读已提交(read committed):**一个事务提交后,他做的变更才能被其他事务看到。(会出现 不可重复读 - 幻读)
-
**可重复读(repeatable read):**一个事务执行过程中看到的数据,与事务启动时看到的一致。(会出现 幻读)
-
**串行化(serializable):**对记录加上读写锁,在多个事务对这条记录进行读写操作时,如果发生了读写冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。
**读未提交:**可以读到未提交事务修改的数据,所以直接读取最新的数据。
**串行化:**通过加读写锁的方式来避免并行访问。
读提交 和 可重复读: 是通过Read View 来实现的,区别在于创建 Read View(数据快照) 的时机不同。
读提交: 在「**每个语句执行前」**都会重新生成一个 Read View。
可重复读: 是**「启动事务时」**生成一个 Read View,然后整个事务期间都在用这个 Read View。
执行「开始事务」命令,并不意味着启动了事务:
-
第一种:begin/start transaction 命令。并不代表事务启动了。只有在执行这个命令后,执行了第一条 select 语句,才是事务真正启动的时机;
-
第二种:start transaction with consistent snapshot 命令。就会马上启动事务。
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。
对于使用 InnoDB 存储引擎的数据库表,每行数据包含两个隐藏字段:
-
**trx_id:**当一个事务对某条聚簇索引记录进行改动时,就会把该事务的事务 id 记录在 trx_id 隐藏列里;
-
**roll_pointer:**每次对某条聚簇索引记录进行改动时,都会把旧版本的记录写入到 undo 日志中,然后这个隐藏列是个指针,指向每一个旧版本记录,于是就可以通过它找到修改前的记录。

一个事务去访问记录的时候,除了自己的更新记录总是可见之外,会根据 Read View 存储的事务 id 信息以及行数据的 trx_id 判断该行数据自己是否可见,不可见则通过 roll_pointer 走 undo_log 链找到可见的旧数据。具体为这几种情况:
-
如果记录的 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 列表中:-
如果记录的 trx_id 在
m_ids列表中,表示生成该版本记录的活跃事务依然活跃着(还没提交事务),所以该版本的记录对当前事务不可见。 -
如果记录的 trx_id 不在
m_ids列表中,表示生成该版本记录的活跃事务已经被提交,所以该版本的记录对当前事务可见。
-
这种通过「版本链」来控制并发事务访问同一个记录时的行为就叫 MVCC(多版本并发控制)。
我的理解:
MVCC 本质是通过创建 Read View + undo log 链以及行的隐藏字段来完成的,Read View 保证自己读的合法性,而 undo log 为 Read View 提供合法的可读数据,隐藏字段记录 undo log 链以及最新修改数据的事务 id,与 Read View 中的数据进行对比判断该行数据是否可读,不可读就去 undo log 中找。
读已提交: 每次读都创建 Read View,意味着他每次读到的都是最新提交的事务操作后的结果,故没有解决可重复读问题。
**可重复读:**事务开启时创建 Read View,意味着每次读都是事务开启时的最新可读数据,期间其他事务修改数据后同步修改隐藏字段 trx_id,读时通过该字段与 Read View 字段对比发现不合法就去 undo log 中找到第一条合法的记录进行读取。
各事务修改数据期间不会动 Read View 中的字段,而是更新行数据的隐藏字段,让其他事务知道是我修改的这行数据,其他事务执行时会看修改数据的事务是不是在自己 Read View 的合法范围内(是在我开启之前就有,还是我开启后新来的),决定是直接读还是去 undo log 链中找旧数据。
可重复读下的幻读
可重复读隔离级别下是否真正解决了幻读?
MySQL InnoDB 引擎的默认隔离级别虽然是「可重复读」,但是它很大程度上避免幻读现象,解决的方案有两种:
-
针对快照读(普通 select 语句),是通过 MVCC 方式解决了幻读,因为可重复读隔离级别下,事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,即使中途有其他事务插入了一条数据,是查询不出来这条数据的,所以就很好了避免幻读问题。
-
针对当前读(select ... for update、update、insert 等语句),是通过 next-key lock(记录锁+间隙锁)方式解决了幻读,因为当执行 select ... for update 语句的时候,会加上 next-key lock,如果有其他事务在 next-key lock 锁范围内插入了一条记录,那么这个插入语句就会被阻塞,无法成功插入,直到事务提交,所以就很好了避免幻读问题。
但并没有真正解决幻读:两次读类型不同
情况 1:先是普通快照读,然后其他事务修改数据,再次快照读是不会幻读的,如果是当前读的话就会读新数据,就出现了幻读。
情况 2:A 查询一条数据没有,B 插入了这条数据,A 再查询的话是没有的,但 A 直接选择更新(这也是当前读,跟 1 类似),再次读就看到了。