【MySQL实战45讲8】事务到底是隔离还是不隔离

文章目录

前言

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

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

案例

下面是一个只有两行的表的初始化语句

sql 复制代码
mysql> CREATE TABLE `t` (
  `id` int(11) NOT NULL,
  `k` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB;
insert into t(id, k) values(1,1),(2,2);
事务A 事务B 事务C
start transaction with consistent snapshot;
start transaction with consistent snapshot;
update t set k=k+1 where id=1;
update t set k=k+1 where id=1;
select k from t where id=1; commit;
commit;

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

begin/start transaction :一致性视图是在执行第一个快照读语句时创建。

start transaction with consistent snapshot:一致性视图是在执行start transaction with consistent snapshot时创建

事务 C 没有显式地使用 begin/commit,表示这个 update 语句本身就是一个事务,语句完成的时候会自动提交。事务 B 在更新了行之后查询 ; 事务 A 在一个只读事务中查询,并且时间顺序上是在事务 B 的查询之后。

和第一感觉相反的是,事务B查到的k的值是3,事务A查到的k的值是1

"快照"在MVCC里是怎么工作的

在可重复读隔离级别下,事务在启动的时候就"拍了个快照"。这个快照是基于整库的。

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

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

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

图中虚线框里是同一行数据的 4 个版本,当前最新版本是 V4,k 的值是 22,它是被 transaction id 为 25 的事务更新的,因此它的 row trx_id 也是 25。

图中的3个虚线箭头,就是undo log;而V1,V2,V3并不是物理上真实存在的,而是每次需要的时候根据当前版本和undo log计算出来的。比如,需要V2的时候,就是通过V4依次执行U3,U2算出来。

InnoDB为每个事务构造了一个数组,用来保存这个事务启动瞬间,当前正在"活跃"的所有事务ID。"活跃"指的就是,启动了但还没提交。数组里面事务ID的最小值记为低水位 ,当前系统里面已经创建过的事务ID的最大值加1记为高水位

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

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

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

  1. 如果落在绿色部分,表示这个版本是已经提交的事务,或者是当前事务自己生产的,这个数据是可见的
  2. 如果落在红色部分,表示这个版本是由将来启动的事务生成的,是肯定不可见的
  3. 如果落在黄色 部分,包括两种情况:
    • 若row trx_id在数组中,表示这个版本是由还没提交的事务生成的,不可见
    • 若row trx_id不在数组中,表示这个版本是已经提交了的事务生成的,可见

比如,如果有一个事务,它的低水位是 18,那么当它访问这一行数据时,就会从 V4 通过 U3 计算出 V3,所以在它看来,这一行的值是 11。

有了这个声明后,系统里面随后发生的更新,就和这个事务看到的内容无关了,之后的更新,新的数据版本对于当前的事务来说是不存在的,所以这个事务的快照,是"静态"的。

接下来,看看第一个表格中的三个事务,分析下事务A的语句返回的结果,为什么是k=1

假设:

  1. 事务A开始前,系统里面只有一个活跃事务ID是99
  2. 事务A、B、C的版本号分别是100、101、102,且当前系统里只有这四个事务
  3. 三个事务开始前,(1,1)这个一行数据的row trx_id

事务A的视图数组是[99, 100],事务B的视图数组是[99,100,101],事务C的视图数组是[99,100,101,102]

为了简化分析,先把其他干扰语句去掉,只画出跟事务A查询逻辑有关的操作:

从图中可以看到,第一个有效更新是事务 C,把数据从 (1,1) 改成了 (1,2)。这时候,这个数据的最新版本的 row trx_id 是 102,而 90 这个版本已经成为了历史版本。

第二个有效更新是事务 B,把数据从 (1,2) 改成了 (1,3)。这时候,这个数据的最新版本(即 row trx_id)是 101,而 102 又成为了历史版本。

在事务 A 查询的时候,其实事务 B 还没有提交,但是它生成的 (1,3) 这个版本已经变成当前版本了。但这个版本对事务 A 必须是不可见的,否则就变成脏读了。

现在事务 A 要来读数据了,它的视图数组是[99,100]。当然了,读数据都是从当前版本读起的。所以,事务 A 查询语句的读数据流程是这样的:

  • 找到 (1,3) 的时候,判断出 row trx_id=101,比高水位大,处于红色区域,不可见;
  • 接着,找到上一个历史版本,一看 row trx_id=102,比高水位大,处于红色区域,不可见;
  • 再往前找,终于找到了(1,1),它的 row trx_id=90,比低水位小,处于绿色区域,可见。

这样执行下来,虽然期间这一行数据被修改过,但是事务 A 不论在什么时候查询,看到这行数据的结果都是一致的,所以我们称之为一致性读。

更新逻辑

对于上述的逻辑图,其实有一个疑问没有解答:事务B的update语句,如果按照一致性读,好像结果是不对的?

事务B的视图数组是先生成的,之后事务C才提交,不是应该看不见(1,2)吗,怎么能算出(1,3)来?

是的,如果事务B在更新之前查询一次数据,这个查询返回的k的值确实是1

但是,当它要去更新数据的时候,就不能再在历史版本上更新了,否则事务C的更新就丢失了。因此,事务B此时的set k = k + 1是在(1,2)的基础上进行的操作。

所以,这里就用到了这样一条规则,更新数据都是先读后写,而这个读,只能读当前的值,称为"当前读"(current read)

因此,在更新的时候,当前读拿到的数据是 (1,2),更新后生成了新版本的数据 (1,3),这个新版本的 row trx_id 是 101。

所以,在执行事务 B 查询语句的时候,一看自己的版本号是 101,最新数据的版本号也是 101,是自己的更新,可以直接使用,所以查询得到的 k 的值是 3。

这里我们提到了一个概念,叫作当前读 。其实,除了 update 语句外,select 语句如果加锁,也是当前读。

sql 复制代码
mysql> select k from t where id=1 lock in share mode;
mysql> select k from t where id=1 for update;

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

事务A 事务B 事务C'
start transaction with consistent snapshot;
start transaction with consistent snapshot;
start transaction with consistent snapshot; update t set k=k+1 where id=1;
update t set k=k+1 where id=1; select k from t where id=1;
select k from t where id=1; commit;
commit; commit;

事务 C'的不同是,更新后并没有马上提交,在它提交前,事务 B 的更新语句先发起了。前面说过了,虽然事务 C'还没提交,但是 (1,2) 这个版本也已经生成了,并且是当前的最新版本。那么,事务 B 的更新语句会怎么处理呢?

这时候,"两阶段锁协议"就要上场了。事务 C'没提交,也就是说 (1,2) 这个版本上的写锁还没释放。而事务 B 是当前读,必须要读最新版本,而且必须加锁,因此就被锁住了,必须等到事务 C'释放这个锁,才能继续它的当前读。

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

  • 在可重复读隔离级别下,只需要再事务开始的时候创建一致性视图,之后事务里的其他查询都共用这个一致性视图
  • 在读提交隔离级别下,每一个语句执行前都会重新算出一个新的视图
相关推荐
做梦敲代码10 分钟前
达梦数据库-读写分离集群部署
数据库·达梦数据库
hanbarger29 分钟前
mybatis框架——缓存,分页
java·spring·mybatis
苹果醋31 小时前
2020重新出发,MySql基础,MySql表数据操作
java·运维·spring boot·mysql·nginx
小蜗牛慢慢爬行1 小时前
如何在 Spring Boot 微服务中设置和管理多个数据库
java·数据库·spring boot·后端·微服务·架构·hibernate
hanbarger1 小时前
nosql,Redis,minio,elasticsearch
数据库·redis·nosql
微服务 spring cloud1 小时前
配置PostgreSQL用于集成测试的步骤
数据库·postgresql·集成测试
先睡1 小时前
MySQL的架构设计和设计模式
数据库·mysql·设计模式
弗罗里达老大爷1 小时前
Redis
数据库·redis·缓存
仰望大佬0072 小时前
Avalonia实例实战五:Carousel自动轮播图
数据库·microsoft·c#