08 | 事务到底是隔离的还是不隔离的?

发布时间 2023-06-27 23:19:23作者: 帅的雅痞

08 | 事务到底是隔离的还是不隔离的?

事务启动时机

事务启动时机: begin/start transaction 命令并不是一个事务的起点,在执行到它们之后的第一个操作 InnoDB 表的语句,事务才真正启动。如果想要马上启动,可以使用 start transaction with consistent snapshot 这个命令。

两者不同:

命令 一致性视图创建时机 事务ID分配时机
begin/start transaction 执行第一个快照读语句时创建的 (第一个 Select 语句) 第一条操作语句执行的时候启动事务(分配事务id)
start transaction with consistent snapshot 执行 start transaction with consistent snapshot 时创建的 事务启动的瞬间

也就是说,在第一种启动方式下,可能不会创建一致性视图,比如只有 update 语句的事务。

快照在 MVCC 里怎么工作的

InnoDB 在实现 MVCC 时用到了一致性读视图,即 consistent read view,用于支持 RC(Read Committed,读提交)和 RR(Repeatable Read,可重复读)隔离级别的实现。它没有物理结构,作用是事务执行期间用来定义“我能看到什么数据”。

在 RR 隔离级别下,事务在启动的时候就 “拍了个快照”。注意,这个快照是基于整库的

快照是怎么实现的呢?

InnoDB 中每个事务都有一个唯一的事务 ID,叫作 transaction id。 它是在事务开始时向 InnoDB 的事务系统申请的,是按申请顺序严格递增的。

而每行数据也都是有多个版本的。每次事务更新数据的时候,都会生成一个新的数据版本,并且把 transaction id 赋值给这个数据版本的事务 ID,记为 row trx_id。同时,旧的数据版本要保留,并且在新的数据版本中,能够有信息可以直接拿到它。

也就是说,数据表中的一行记录,其实可能有多个版本(row),每个版本都有自己的 row trx_id 。

img

前面说过,语句更新会生成 undo log (回滚日志),那么 undo log 在哪呢?

上图中的三个虚线箭头,就是 undo log; 而 V1、V2、V3 并不是物理上真实存在的,而是每次需要的时候根据当前版本和 undo log 计算出来的。

在快照实现上,InnoDB 为每个事务构造了一个数组,用来保存这个事务启动瞬间,当前正在 “活跃”的所有事务 ID。 “活跃”指的就是,启动了但还没提交

数组里面事务 ID 的最小值记为低水位,当前系统里面已经创建过的事务 ID 的最大值加 1 记为高水位

视图数组和高水位,就组成了当前事务的一致性视图(read-view)

而数据版本的可见性规则,就是基于数据的 row trx_id 和这个一致性视图的对比结果得到的。

这个视图数组把所有的 row trx_id 分成了几种不同的情况。

img

对于当前事务的启动瞬间来说,一个数据版本的 row trx_id,有以下几种可能:

1、如果落在绿色部分,表示这个版本是已提交的事务或者是当前事务自己生成的,这个数据是可见的;

2、如果落在红色部分,表示这个版本是由将来启动的事务生成的,是肯定不可见的;

3、如果落在黄色部分,那就包括两种情况

​ a. 若 row trx_id 在数组中,表示这个版本是由还没提交的事务生成的,不可见;

​ b. 若 row trx_id 不在数组中,表示这个版本是已经提交了的事务生成的,可见。

简单来说,一个数据版本,对于一个事务视图来说,除了自己的更新总是可见以外,有三种情况:

1、版本未提交,不可见;

2、版本已提交,但是是在视图创建后提交的,不可见;

3、版本已提交,而且是在视图创建前提交的,可见。

InnoDB 利用了“所有数据都有多个版本”的这个特性,实现了“秒级创建快照”的能力。

更新逻辑

更新数据都是先读后写的,而这个读,只能读当前的值,称为“当前读”(current read)。当前读,总是读取已经提交完成的最新版本。

除了 update 语句外,select 语句如果加锁,也是当前读。加锁方式: lock in share mode (读锁,S锁,共享锁) 或 for update (写锁、X 锁,排他锁)

事务的可重复读的能力是怎么实现的?

可重复读的核心就是一致性读(consistent read);而事务更新数据的时候,只能用当前读。如果当前的记录的行锁被其他事务占用的话,就需要进入锁等待。

而读提交的逻辑和可重复读的逻辑类似,它们最主要的区别是:

  • 在可重复读隔离级别下,只需要在事务开始的时候创建一致性视图,之后事务里的其他查询都共用这个一致性视图;对于可重复读,查询只承认在事务启动前就已经提交完成的数据;
  • 在读提交隔离级别下,每一个语句执行前都会重新算出一个新的视图。对于读提交,查询只承认在语句启动前就已经提交完成的数据;