[关闭]
@adamhand 2019-03-15T09:47:40.000000Z 字数 3273 阅读 1026

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


如果是可重复读隔离级别,事务 T 启动的时候会创建一个视图 read-view,之后事务 T 执行期间,即使有其他事务修改了数据,事务 T 看到的仍然跟在启动时看到的一样。

但是,一个事务要更新一行,如果刚好有另外一个事务拥有这一行的行锁,它又不能这么超然了,会被锁住,进入等待状态。问题是,既然进入了等待状态,那么等到这个事务自己获取到行锁要更新数据的时候,它读到的值又是什么呢?

下面先看一个问题。

  1. mysql> CREATE TABLE `t` (
  2. `id` int(11) NOT NULL,
  3. `k` int(11) DEFAULT NULL,
  4. PRIMARY KEY (`id`)
  5. ) ENGINE=InnoDB;
  6. insert into t(id, k) values(1,1),(2,2);



需要注意的是事务的启动时机。begin/start transaction 命令并不是一个事务的起点,在执行到它们之后的第一个操作 InnoDB 表的语句(第一个快照读语句),事务才真正启动。如果想要马上启动一个事务,可以使用 start transaction with consistent snapshot 这个命令。如果设置autocommit=1,上面的图在可重复读的隔离级别下,事务A和事务B最后得到的数据是什么呢?

先说答案,A读到的数据是1B读到的数据是3

要分析上面的结果,需要先看一下几个概念。

事务ID

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

行中隐藏的列

参考MySQL官方手册InnoDB为每一行数据都添加了三个隐藏字段:

“快照”在 MVCC 里是怎么工作的

MVCC是根据DB_ROLL_PTRDB_TX_ID这两个字段(还有一个在“特殊位置”的删除标记)来构建事务可视版本(即快照,read-view)的。

下图中的V1、V2、V3、V4一个记录被多个事务连续更新后的状态。图中的三个虚线箭头,就是 undo log;而 V1、V2、V3 并不是物理上真实存在的,而是每次需要的时候根据当前版本和 undo log 计算出来的。



快照是基于整个库的,但是为什么即使库很大,“拍快照”的时间却很短?其实InnoDB 利用了“所有数据都有多个版本”的这个特性,实现了“秒级创建快照”的能力。下面从read view的源码看一下

  1. struct read_view_t{
  2. // 由于是逆序排列,所以low/up有所颠倒
  3. trx_id_t low_limit_id; // 能看到当前行版本的高水位标识,> low_limit_id皆不能看见
  4. trx_id_t up_limit_id; // 能看到当前行版本的低水位标识,< up_limit_id皆能看见
  5. ulint n_trx_ids; // 当前活跃事务(即未提交的事务)的数量
  6. trx_id_t* trx_ids; // 以逆序排列的当前获取活跃事务id的数组,其up_limit_id<tx_id<low_limit_id
  7. trx_id_t creator_trx_id; // 创建当前视图的事务id
  8. UT_LIST_NODE_T(read_view_t) view_list; // 事务系统中的一致性视图链表
  9. };

文章一开始的哪个问题其实就是行记录的可见性问题。上述源码中与可见性相关的两个参数为:low_limit_idup_limit_id

InnoDB 为每个事务构造了一个数组,用来保存这个事务启动瞬间,当前正在“活跃”的所有事务 ID“活跃”指的就是,启动了但还没提交。数组里面事务 ID 的最小值记为低水位(up_limit_id当前系统里面已经创建过的事务 ID 的最大值加 1 记为高水位(low_limit_id这个视图数组和高水位,就组成了当前事务的一致性视图(read-view)

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



有点晕,用白话总结一下,一个数据版本,对于一个事务视图来说,除了自己的更新总是可见以外可见性规则有四个,可以按照以下四个规则来判断可见性(可重复读版本):

用这四条规则分析刚开始的问题,就能通了。

快照读和当前读

上面提到一个“当前读”的概念,当前读就是读取当前最新的数据,当前读需要加锁,除了updata语句,select语句也可以当前读:

  1. mysql> select k from t where id=1 lock in share mode; //加读锁(S锁,共享锁)
  2. mysql> select k from t where id=1 for update; //加写锁(X锁,排他锁)

而普通的select语句都是快照读。

进一步

假设事务 C 不是马上提交的,而是变成了下面的事务 C’,会怎么样呢?



和前面的不同主要在B和C上,C开启事务之后。执行update语句会拿到行的锁,根据两阶段锁协议,在commit之前它是不会释放锁的,所以,当B要执行update时就需要等待C释放锁,之后才能拿到锁,执行当前读。

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

读提交和可重复读

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

所以开始的问题如果是在读提交的隔离级别下,A在select时会创建一个新视图,这时C已经提交完成,所以此时C更新的数据对A是可见的,所以A会返回2。而B的返回值不变。

需要注意的是,“start transaction with consistent snapshot; ”的意思是从这个语句开始,创建一个持续整个事务的一致性快照。所以,在读提交隔离级别下,这个用法就没意义了,等效于普通的 start transaction

总结一下,“读提交”和“可重复读”的情况如下:

参考

InnoDB存储引擎MVCC的工作原理
InnoDB多版本并发控制机制-MVCC底层实现
MySQL · 源码分析 · InnoDB的read view,回滚段和purge过程简介

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注