【MySQL数据库原理 七】MySQL数据库事务及锁机制(下)

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
RDS MySQL Serverless 高可用系列,价值2615元额度,1个月
简介: 【MySQL数据库原理 七】MySQL数据库事务及锁机制(下)

提出问题

可重复读级别下,事务中读取的数据在整个事务过程中都是一致的,那么别的事务更新了数据,当前事务再去更新数据的时候,看到的是更新后的,还是更新前的?举个例子,初始插入值为insert into t(id, k) values(1,1),(2,2);

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

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

在这里,事务 B 查到的 k 的值是 3,而事务 A 查到的 k 的值是 1,我们需要知道为什么结果是这样

MVCC机制

这部分详细讲解下MVCC机制,包括快照和事务ID

快照

在 MySQL 里,有两个视图的概念,这两个视图分别用于不同的场景。

  • 一个是 view。它是一个用查询语句定义的虚拟表,在调用的时候执行查询语句并生成结果。创建视图的语法是 create view … ,而它的查询方法与表一样。
  • 一个是 InnoDB 在实现 MVCC 时用到的一致性读视图,即 consistent read view,用于支持 RC(Read Committed,读提交)和 RR(Repeatable Read,可重复读)隔离级别的实现。它没有物理结构,作用是事务执行期间用来定义“我能看到什么数据”

第二种视图更像是一种快照。在可重复读隔离级别下,事务在启动的时候就“拍了个快照”。注意,这个快照是基于整库的

事务ID

InnoDB 里面每个事务有一个唯一的事务 ID,叫作 transaction id。它是在事务开始的时候向 InnoDB 的事务系统申请的,是按申请顺序严格递增的。而每行数据也都是有多个版本的。每次事务更新数据的时候,都会生成一个新的数据版本,并且把 transaction id 赋值给这个数据版本的事务 ID,记为 row trx_id。同时,旧的数据版本要保留,并且在新的数据版本中,能够有信息可以直接拿到它。也就是说,数据表中的一行记录,其实可能有多个版本 (row),每个版本有自己的 row trx_id

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

undo log

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

回滚段是真实存在的,而视图也就是快照是一种逻辑形态,是计算出来的。

MVCC实现

按照可重复读的定义,一个事务启动的时候,能够看到所有已经提交的事务结果。但是之后,这个事务执行期间,其他事务的更新对它不可见。因此,一个事务只需要在启动的时候声明说,

  • “以我启动的时刻为准,如果一个数据版本是在我启动之前生成的,就认;
  • 如果是我启动以后才生成的,我就不认,我必须要找到它的上一个版本”。

当然,如果“上一个版本”也不可见,那就得继续往前找。还有,如果是这个事务自己更新的数据,它自己还是要认的

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

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

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

我们举个例子,假如事务ID从【90,100】,其中90,91,93,95,98,99,100已提交,则活跃事务ID数组为【92,96,97】,低水位为92,当前事务为94,高水位为101。则可以进行如下划分:

  • 绿色区域,低水位之前已提交的事务ID,90,91可见。包括当前事务虽然未提交刚开启,但是自身可见,94可见
  • 黄色区域,低水位到高水位之间,剩下的【92,100】,这一系列事务ID,分为两类
  • a. 若 row trx_id 在活跃数组中,【92,96,97】不可见。
  • b. 若 row trx_id 不在活跃数组中,93,95,98,99,100均可见
  • 红色区域,未开始的事务101及以后的事务ID都不可见

以上就是一致性视图的使用规范。对于上边的行数据版本链问题,低水位是18,则在所有版本的数据中,trx_id低于18的版本都可见,所以值为11肯定是可见的,有了这个声明后,系统里面随后发生的更新,是不是就跟这个事务看到的内容无关了呢?因为之后的更新,生成的版本一定属于上面的 3或者 2(a) 的情况,而对它来说,这些新的数据版本是不存在的,所以这个事务的快照,就是“静态”的了

解决问题

接下来,我们继续看一下开始提出问题的三个事务,分析下事务 A 的语句返回的结果,为什么是 k=1。这里,我们不妨做如下假设:

  • 事务 A 开始前,系统里面只有一个活跃事务 ID 是 99;
  • 事务 A、B、C 的版本号分别是 100、101、102,且当前系统里只有这四个事务;

三个事务开始前,(1,1)这一行数据的 row trx_id 是 90。这样,事务 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. 找到 (1,3) 的时候,判断出 row trx_id=101,比高水位大,处于红色区域,不可见;
  2. 接着,找到上一个历史版本,一看 row trx_id=102,比高水位大,处于红色区域,不可见;
  3. 再往前找,终于找到了(1,1),它的 row trx_id=90,比低水位小,处于绿色区域,可见。

这样执行下来,虽然期间这一行数据被修改过,但是事务 A 不论在什么时候查询,看到这行数据的结果都是一致的,所以我们称之为一致性读,一个数据版本,对于一个事务视图来说,除了自己的更新总是可见以外,有三种情况:

  • 版本未提交,不可见;
  • 版本已提交,但是是在视图创建后提交的,不可见;
  • 版本已提交,而且是在视图创建前提交的,可见。

现在,我们用这个规则来判断查询结果

  1. 事务 A 的查询语句的视图数组是在事务 A 启动的时候生成的,这时候:(1,3) 还没提交,属于情况 1,不可见;
  2. (1,2) 虽然提交了,但是是在视图数组创建之后提交的,属于情况 2,不可见
  3. (1,1) 是在视图数组创建之前提交的,可见。

去掉数字对比后,只用时间先后顺序来判断,分析起来是不是轻松多了。所以,后面我们就都用这个规则来分析

更新逻辑

事务 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。有点像Java的volatile机制

当前读

这里我们提到了一个概念,叫作当前读。其实,除了 update 语句外,select 语句如果加锁,也是当前读。所以,如果把事务 A 的查询语句 select * from t where id=1 修改一下,加上 lock in share modefor update,也都可以读到版本号是 101 的数据,返回的 k 的值是 3。下面这两个 select 语句,就是分别加了读锁(S 锁,共享锁)和写锁(X 锁,排他锁)

mysql> select k from t where id=1 lock in share mode;
mysql> select k from t where id=1 for update;

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

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

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

可重复读和读提交的实现

到这里,我们把一致性读、当前读和行锁就串起来了。那么事务的可重复读的能力是怎么实现的?

  • 可重复读的核心就是一致性读(consistent read);事务更新数据的时候,只能用当前读。如果当前的记录的行锁被其他事务占用的话,就需要进入锁等待
  • 读提交的逻辑和可重复读的逻辑类似,它们最主要的区别是:在可重复读隔离级别下,只需要在事务开始的时候创建一致性视图,之后事务里的其他查询都共用这个一致性视图;在读提交隔离级别下,每一个语句执行前都会重新算出一个新的视图。

那么,我们再看一下,在读提交隔离级别下,事务 A 和事务 B 的查询语句查到的 k,分别应该是多少呢?这里需要说明一下,“start transaction with consistent snapshot; ”的意思是从这个语句开始,创建一个持续整个事务的一致性快照。所以,在读提交隔离级别下,这个用法就没意义了,等效于普通的 start transaction。下面是读提交时的状态图,可以看到这两个查询语句的创建视图数组的时机发生了变化,就是图中的 read view 框。(注意:这里,我们用的还是事务 C 的逻辑直接提交,而不是事务 C’)

这时,事务 A 的查询语句的视图数组是在执行这个语句的时候创建的,时序上 (1,2)、(1,3) 的生成时间都在创建这个视图数组的时刻之前。但是,在这个时刻:

  • (1,3) 事务B还没提交,属于情况 1,不可见;
  • (1,2) 事务C提交了,属于情况 3,可见。

所以,这时候事务 A 查询语句返回的是 k=2。显然地,事务 B 查询结果 k=3,因为B的视图里,C已经提交了。自己状态又可见,直接加2次。

隔离级别与行锁

Innodb对于行级锁,行文至此,我们最终探讨一次四种隔离级别是如何产生效果的呢?是依据MVCC机制和行级别的锁来实现的,针对每种隔离级别分别介绍一下:

  • 读未提交不创建视图,所有语句不加任何锁,有脏读问题。解决办法就是下面的读已提交
  • 读已提交执行sql时创建一致性视图,SELECT语句是一致性读,一致性读会根据 row trx_id 和一致性视图确定数据版本的可见性,且SELECT不加锁,更新语句使用当前读机制+两阶段行锁(Record Lock排它锁)机制,UPDATE加排他锁,存在的问题不可重复读。即在一次事务之间,进行了两次读取,但是结果不一样,不可重复读问题
  • 可重复读事务开始时创建一致性视图,查询语句是一致性读,一致性读会根据 row trx_id 和一致性视图确定数据版本的可见性,且SELECT不加锁,更新语句使用当前读机制+两阶段行锁(Record Lock排它锁)机制,UPDATE加排他锁,可重复读阻止的写事务包括update(只给存在的数据行加上了锁),但是不包括insert、delete(新行不存在,所以没有办法加锁)
  • 串行化不创建视图,读加读锁,写加跨行级别Next-key Lock排他锁阻止其它读写事务 ,基本上就是一个个执行事务,所以叫串行化。

整体的隔离机制介绍如上,可算是对事务和锁的实现有了一个全盘的掌握了。

相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助     相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
24天前
|
存储 关系型数据库 MySQL
MySQL主从复制原理和使用
本文介绍了MySQL主从复制的基本概念、原理及其实现方法,详细讲解了一主两从的架构设计,以及三种常见的复制模式(全同步、异步、半同步)的特点与适用场景。此外,文章还提供了Spring Boot环境下配置主从复制的具体代码示例,包括数据源配置、上下文切换、路由实现及切面编程等内容,帮助读者理解如何在实际项目中实现数据库的读写分离。
MySQL主从复制原理和使用
|
24天前
|
SQL 关系型数据库 MySQL
Mysql中搭建主从复制原理和配置
主从复制在数据库管理中广泛应用,主要优点包括提高性能、实现高可用性、数据备份及灾难恢复。通过读写分离、从服务器接管、实时备份和地理分布等机制,有效增强系统的稳定性和数据安全性。主从复制涉及I/O线程和SQL线程,前者负责日志传输,后者负责日志应用,确保数据同步。配置过程中需开启二进制日志、设置唯一服务器ID,并创建复制用户,通过CHANGE MASTER TO命令配置从服务器连接主服务器,实现数据同步。实验部分展示了如何在两台CentOS 7服务器上配置MySQL 5.7主从复制,包括关闭防火墙、配置静态IP、设置域名解析、配置主从服务器、启动复制及验证同步效果。
Mysql中搭建主从复制原理和配置
|
28天前
|
存储 关系型数据库 MySQL
优化 MySQL 的锁机制以提高并发性能
【10月更文挑战第16天】优化 MySQL 锁机制需要综合考虑多个因素,根据具体的应用场景和需求进行针对性的调整。通过不断地优化和改进,可以提高数据库的并发性能,提升系统的整体效率。
49 1
|
1月前
|
SQL 关系型数据库 MySQL
阿里面试:MYSQL 事务ACID,底层原理是什么? 具体是如何实现的?
尼恩,一位40岁的资深架构师,通过其丰富的经验和深厚的技術功底,为众多读者提供了宝贵的面试指导和技术分享。在他的读者交流群中,许多小伙伴获得了来自一线互联网企业的面试机会,并成功应对了诸如事务ACID特性实现、MVCC等相关面试题。尼恩特别整理了这些常见面试题的系统化解答,形成了《MVCC 学习圣经:一次穿透MYSQL MVCC》PDF文档,旨在帮助大家在面试中展示出扎实的技术功底,提高面试成功率。此外,他还编写了《尼恩Java面试宝典》等资料,涵盖了大量面试题和答案,帮助读者全面提升技术面试的表现。这些资料不仅内容详实,而且持续更新,是求职者备战技术面试的宝贵资源。
阿里面试:MYSQL 事务ACID,底层原理是什么? 具体是如何实现的?
|
28天前
|
存储 关系型数据库 MySQL
MySQL锁,锁的到底是什么?
【10月更文挑战第16天】MySQL 锁锁定的是与数据和资源相关的对象,其目的是为了保证数据的一致性、避免冲突,并在并发环境下合理协调事务或操作的执行。理解锁的对象和意义对于优化数据库性能、处理并发问题至关重要。
54 0
|
30天前
|
关系型数据库 MySQL 数据库
mysql锁详解
通过理解并合理运用MySQL中的锁机制,开发者可以有效管理数据库并发访问,平衡性能与数据一致性需求。更多关于MySQL锁的深入探讨和最佳实践,请参考专业的数据库管理资源[[深入MySQL锁机制详解
32 0
|
8天前
|
SQL 关系型数据库 MySQL
12 PHP配置数据库MySQL
路老师分享了PHP操作MySQL数据库的方法,包括安装并连接MySQL服务器、选择数据库、执行SQL语句(如插入、更新、删除和查询),以及将结果集返回到数组。通过具体示例代码,详细介绍了每一步的操作流程,帮助读者快速入门PHP与MySQL的交互。
23 1
|
10天前
|
SQL 关系型数据库 MySQL
go语言数据库中mysql驱动安装
【11月更文挑战第2天】
26 4
|
1月前
|
存储 关系型数据库 MySQL
Mysql(4)—数据库索引
数据库索引是用于提高数据检索效率的数据结构,类似于书籍中的索引。它允许用户快速找到数据,而无需扫描整个表。MySQL中的索引可以显著提升查询速度,使数据库操作更加高效。索引的发展经历了从无索引、简单索引到B-树、哈希索引、位图索引、全文索引等多个阶段。
61 3
Mysql(4)—数据库索引
|
17天前
|
监控 关系型数据库 MySQL
数据库优化:MySQL索引策略与查询性能调优实战
【10月更文挑战第27天】本文深入探讨了MySQL的索引策略和查询性能调优技巧。通过介绍B-Tree索引、哈希索引和全文索引等不同类型,以及如何创建和维护索引,结合实战案例分析查询执行计划,帮助读者掌握提升查询性能的方法。定期优化索引和调整查询语句是提高数据库性能的关键。
85 1