场景引入

我们知道,在可重复读的隔离级别下,一个事务A启动的时候会创建一个read view,之后在这个事务A执行期间,即使其他事务修改数据,事务A看到的仍然和启动时相同。

考虑一个问题,假如该事务A想要对一行做更新,而此时这行的行锁被其他事务B持有,那么事务A会被锁住而等待行锁。当事务A获取到行锁想要查询更新时,它读到的值是启动时看到的旧值还是被事务B更新后的新值呢?

我们以一个有两行数据(id,k)=(1,1),(2,2)的表为例。假设现在有三个事务A,B,C,其语句时间顺序如下:

首先,需要注意事务启动时机:begin/start transaction并不会直接启动事务,而是执行到它们之后的第一个操作InnoDB表的语句,才会真正启动事务。如果想要马上启动一个事务,可以使用start transaction with consistent snapshot,就像事务A和事务B那样。而对于事务C没有显式使用语句,表示更新语句本身就是一个事务,会在语句完成时自动提交。

上面这个例子就是我们要考虑的问题的一个场景,事务B先启动了事务,而想要更新的行先被事务C更新,之后事务B自己更新并查询;事务A在事务B之后查询同一行。那么事务A和事务B查询结果是多少呢?

答案是:事务A得到的结果是k=1,事务B得到的结果是k=3

如果答案和你想的不一样,那么可以继续往下读,相信最后能解答疑惑。

快照在MVCC里如何工作

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

接下来首先来看快照如何实现:

InnoDB里每个事务都有一个唯一的事务ID称为transaction id,该ID在事务开始时向InnoDB的事务系统申请,按照申请顺序严格递增

而每行数据有多个版本,每次有事务更新数据,都会生成一个新的数据版本,并且把事务的transaction id赋值给这个数据版本,记为row trx_id。同时,旧的数据版本依然会被保留,且可以在新数据版本中通过一定方法获取到旧数据版本。下图表示了一个记录被多个事务更新的过程:

图中,下方的矩形就代表了不同的数据版本。而\(U_i\)实际就代表了undo log。只要获取了最新的数据版本和undo log,就能回滚出历史数据版本。

为了达到可重复读的定义,实际上是在一个事务启动的时候,允许其看见它自己创建的以及在它启动前已经生成的数据版本,而不允许看见在它启动时还未生成的数据版本

在实现上,InnoDB为每个事务构造一个数组,用来保存在这个事务启动瞬间,当前正在活跃的事务ID。这里,活跃指的是启动但未提交的事务。同时,还会记录数组里面事务ID的最小值,以及当前系统里已经创建过的事务ID的最大值+1。

数组+最小值+(最大值+1)+当前事务ID,实际上就组成了当前事务的一致性视图read view。

而数据版本是否可见,就是基于read view和数据版本的row trx_id。read view的数组和字段会把row trx_id分为几种情况:

对于当前启动的事务,一个数据版本的row trx_id,有如下可能:

  • 落在绿色部分,表示该版本在当前事务启动前已提交或是自己创建的,可见;

  • 落在红色部分,表示该版本不是由所有已创建出来的事务启动的,不可见;

  • 落在黄色部分

    • 若row trx_id在数组中,表示是活跃事务生成的,还未提交,不可见;

    • 若row trx_id不在数组中,表示是已经提交的事务生成的,可见。

所以,由于所有数据都有多个版本,每个创建的事务都有对应的快照。

接下来分析“场景引入”里事务A的查询结果为什么是k=1

这里先做几个假设。假设事务A开始前,系统里只有一个活跃事务ID为99,事务A,B,C的ID为100,101,102且当前系统只有四个事务;在三个事务启动前,(1,1)这一行的数据的row trx_id为90。

根据该假设,事务A的read view中的数组为[99,100],事务B的read view中的数组为[99,100,101],事务C的read view中的数组为[99,100,101,102]。

我们分析事务A相关的操作:

可以发现,尽管在事务A做查询时,数据已经改为了(1,3),但由于该版本的row trx_id=101,不存在于事务A的read view数组中,因此该版本对事务A不可见。事务A查询语句的流程应该是:

  • 找到(1,3),发现不可见;

  • 找到上一个版本(1,2),发现不可见;

  • 继续向前,找到(1,1),是一个可见的数据版本。

通过以上分析,相信你已经理解为什么事务A的查询结果是k=1了。但若每次分析都像这样,未免有些麻烦,因此我们给出总结:一个数据版本,对于一个事务视图来说,除了自己的更新总是可见,有三种情况:

  • 版本未提交,不可见;

  • 版本已提交,但是是在read view创建后提交的,不可见;

  • 版本已提交,而且是在read view创建前提交的,可见。

以上总结对比前面与row trx_id比较分析的方法,其实就是去掉了数字的对比,只用时间先后顺序判断。

更新逻辑

分析事务B相关的操作:

可以发现,如果我们像分析事务A那样去分析事务B,会认为事务B看不到(1,2)这个数据版本。

这个问题出在混淆了“快照读”“当前读”。当前读指的是读最新版本的数据。由于更新数据都是先读后写,它用到的是当前读而不再是快照读。

知道了这个规则后,就比较好理解答案了,事务B在更新时能拿到数据(1,2),从而更新后生成了一个新的数据版本(1,3),且该版本的row trx_id为事务B的ID 101。那么之后事务B在查询时,能查到由自己更新的数据版本,得到结果为k=3

当前读除了在update语句上会生效,如果使用select … lock in share mode / for update,也是当前读。因此,如果对事务A的查询语句加锁,它也能查询出k=3

假设事务C不是马上提交的,而是变成了下面这样:

此时,就需要考虑上一篇文章介绍的“两阶段锁协议”。由于事务C’ 没有提交,其在id=1这一行上加的写锁并不会释放。而事务B是当前读,必须加锁读最新版本,因此会被锁住,直到事务C’ 释放这个行锁。

从可重复读到读已提交

到这里,我们可以归纳事务的可重复读的能力是如何实现的:核心是read view,而事务更新数据的时候,只能用当前读,如果读取行的行锁被其他事务占用,就需要进入锁等待。

可重复读和读已提交的区别:

  • 读已提交隔离级别下,每个语句执行前都会生成一个read view。

  • 可重复读隔离级别下,只在事务开始时创建read view。

最后我们再来分析一下在读已提交的隔离级别下,一开始的场景中事务A和事务B的读取结果。画出状态图:

对于事务B,答案依然是k=3

对于事务A,其创建read view时已经能看到(1,2)的版本,但由于事务B还未提交,事务A并不能看到(1,3)的版本,因此事务A查询结果为k=2

MySQL 08 详解read view:事务到底是隔离的还是不隔离的?的更多相关文章

  1. MySQL状态变量详解

    MySQL状态变量详解 mysql的状态变量(status variables)记录的mysql服务器的运行状态信息.查看语法如下: SHOW [GLOBAL | SESSION] STATUS; S ...

  2. MySQL 数据类型 详解

    MySQL 数据类型 详解 MySQL 的数值数据类型可以大致划分为两个类别,一个是整数,另一个是浮点数或小数.许多不同的子类型对这些类别中的每一个都是可用的,每个子类型支持不同大小的数据,并且 My ...

  3. MySQL配置文件详解

    MYSQL 配置文件详解 “全局缓存”.“线程缓存”,全局缓存是所有线程共享,线程缓存是每个线程连接上数据时创建一个线程(如果没有设置线程池),假如有200连接.那就是200个线程,如果参数设定值是1 ...

  4. MySQL权限详解

    MySQL权限级别介绍 MySQL权限级别 全局性的管理权限,作用于整个MySQL实例级别 数据库级别的权限,作用于某个指定的数据库上或者所有的数据库上 数据库对象级别的权限,作用于指定的数据库对象上 ...

  5. mysql存储过程详解

    mysql存储过程详解 1.      存储过程简介   我们常用的操作数据库语言SQL语句在执行的时候需要要先编译,然后执行,而存储过程(Stored Procedure)是一组为了完成特定功能的S ...

  6. mysql 存储过程详解 存储过程

    mysql存储过程详解 1.      存储过程简介         我们常用的操作数据库语言SQL语句在执行的时候需要要先编译,然后执行,而存储过程(Stored Procedure)是一组为了完成 ...

  7. MySQL存储过程详解 mysql 存储过程

    原文地址:MySQL存储过程详解  mysql 存储过程作者:王者佳暮 mysql存储过程详解 1.     存储过程简介 我们常用的操作数据库语言SQL语句在执行的时候需要要先编译,然后执行,而存储 ...

  8. Mysql Explain 详解

    Mysql Explain 详解[强烈推荐] Mysql Explain 详解一.语法explain < table_name >例如: explain select * from t3 ...

  9. MySQL存储过程详解 mysql 存储过程(二)

    mysql存储过程详解 1.      存储过程简介 我们常用的操作数据库语言SQL语句在执行的时候需要要先编译,然后执行,而存储过程(Stored Procedure)是一组为了完成特定功能的SQL ...

  10. MySQL 操作详解

    MySQL 操作详解 一.实验简介 本节实验中学习并实践 MySQL 上创建数据库.创建表.查找信息等详细的语法及参数使用方法. 二.创建并使用数据库 1. 创建并选择数据库 使用SHOW语句找出服务 ...

随机推荐

  1. Web前端入门第 24 问:CSS 单位

    单位就是那个形容长度大小的东西.比如身高180cm(厘米),cm就是单位. css 也不例外,要描述一个盒子的大小,就必须要用到单位. css 单位根据其作用分为几大类:绝对单位.相对单位.视口单位. ...

  2. Python 潮流周刊#96:MCP 到底是什么?(摘要)

    本周刊由 Python猫 出品,精心筛选国内外的 250+ 信息源,为你挑选最值得分享的文章.教程.开源项目.软件工具.播客和视频.热门话题等内容.愿景:帮助所有读者精进 Python 技术,并增长职 ...

  3. java基础之函数式接口

    一.函数式接口在Java中是指:有且仅有一个抽象方法的接口,所以函数式接口就是可以适用于Lambda使用的接口 二.自定义函数式接口 格式: @FunctionalInterface //该注解可省, ...

  4. Traefik,想说爱你不容易:一场动态反向代理的心累之旅

    前言:技术选型的初心 在微服务盛行.容器部署逐渐常态化的今天,"动态反向代理"显得尤为重要. Traefik 凭借其原生支持 Docker.自动生成路由.集成 Let's Encr ...

  5. 适用于LixtBox的,开启UI虚拟化时,某些时候需要定位到还没加载的项,比如自动选中某项,视图自动移过去等等

    1 /// <summary> 2 /// 将指定父级的下级索引元素,显示在视野下,使其可见 3 /// </summary> 4 /// <param name=&qu ...

  6. AI Agent离我们有多远?认知革命的开始(上篇)

    认知是成本最低的对冲. --张三思维进化论 深夜3点,我与AI Agent的惊人对话 2025年的一个深夜,我习惯性地打开电脑处理一些工作.身为一个从大厂转型的自由职业者,夜晚往往是我效率最高的时段. ...

  7. 支持国产,为deepin添把柴,全面切换到deepin;

    虽然不是技术型,但是对deepin的支持必须有的. 只希望国产系统越来越好.国产软件越来越好. 软件生态也越来越好! 等搞完高精密仪器问题,cpu自己造了,下来估计就要整顿软件行业. 我这里要时刻准备 ...

  8. 【SQL周周练】给你无酸纸、变色油墨,你能伪造多少美金?

    大家好,我是"蒋点数分",多年以来一直从事数据分析工作.从今天开始,与大家持续分享关于数据分析的学习内容. 本文是第 2 篇,也是[SQL 周周练]系列的第 2 篇.该系列是挑选或 ...

  9. C++基础——引用和指针篇

    一.指针(Pointer) 定义: 指针是一个变量,用于存储另一个变量的地址. 基本用法: #include <iostream> using namespace std; int mai ...

  10. OpenPPL的执行流程与类间关系UML表达

    上一讲,对OpenPPL进行了介绍,以及通过官方文档,学习了它的python与C++的操作流程,以及如添加新的引擎与Op算子. 本节,将通过阅读代码通过UML梳理操作流程以及类之间的相互关系 src地 ...