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

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
RDS MySQL DuckDB 分析主实例,集群系列 8核16GB
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排他锁阻止其它读写事务 ,基本上就是一个个执行事务,所以叫串行化。

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

相关实践学习
每个IT人都想学的“Web应用上云经典架构”实战
本实验从Web应用上云这个最基本的、最普遍的需求出发,帮助IT从业者们通过“阿里云Web应用上云解决方案”,了解一个企业级Web应用上云的常见架构,了解如何构建一个高可用、可扩展的企业级应用架构。
MySQL数据库入门学习
本课程通过最流行的开源数据库MySQL带你了解数据库的世界。   相关的阿里云产品:云数据库RDS MySQL 版 阿里云关系型数据库RDS(Relational Database Service)是一种稳定可靠、可弹性伸缩的在线数据库服务,提供容灾、备份、恢复、迁移等方面的全套解决方案,彻底解决数据库运维的烦恼。 了解产品详情: https://www.aliyun.com/product/rds/mysql 
相关文章
|
6月前
|
人工智能 运维 关系型数据库
数据库运维:mysql 数据库迁移方法-mysqldump
本文介绍了MySQL数据库迁移的方法与技巧,重点探讨了数据量大小对迁移方式的影响。对于10GB以下的小型数据库,推荐使用mysqldump进行逻辑导出和source导入;10GB以上可考虑mydumper与myloader工具;100GB以上则建议物理迁移。文中还提供了统计数据库及表空间大小的SQL语句,并讲解了如何使用mysqldump导出存储过程、函数和数据结构。通过结合实际应用场景选择合适的工具与方法,可实现高效的数据迁移。
1095 1
|
7月前
|
负载均衡 算法 关系型数据库
大数据大厂之MySQL数据库课程设计:揭秘MySQL集群架构负载均衡核心算法:从理论到Java代码实战,让你的数据库性能飙升!
本文聚焦 MySQL 集群架构中的负载均衡算法,阐述其重要性。详细介绍轮询、加权轮询、最少连接、加权最少连接、随机、源地址哈希等常用算法,分析各自优缺点及适用场景。并提供 Java 语言代码实现示例,助力直观理解。文章结构清晰,语言通俗易懂,对理解和应用负载均衡算法具有实用价值和参考价值。
大数据大厂之MySQL数据库课程设计:揭秘MySQL集群架构负载均衡核心算法:从理论到Java代码实战,让你的数据库性能飙升!
|
3月前
|
关系型数据库 MySQL 数据库
阿里云数据库RDS费用价格:MySQL、SQL Server、PostgreSQL和MariaDB引擎收费标准
阿里云RDS数据库支持MySQL、SQL Server、PostgreSQL、MariaDB,多种引擎优惠上线!MySQL倚天版88元/年,SQL Server 2核4G仅299元/年,PostgreSQL 227元/年起。高可用、可弹性伸缩,安全稳定。详情见官网活动页。
772 152
|
3月前
|
关系型数据库 分布式数据库 数据库
阿里云数据库收费价格:MySQL、PostgreSQL、SQL Server和MariaDB引擎费用整理
阿里云数据库提供多种类型,包括关系型与NoSQL,主流如PolarDB、RDS MySQL/PostgreSQL、Redis等。价格低至21元/月起,支持按需付费与优惠套餐,适用于各类应用场景。
|
3月前
|
SQL 关系型数据库 MySQL
Mysql数据恢复—Mysql数据库delete删除后数据恢复案例
本地服务器,操作系统为windows server。服务器上部署mysql单实例,innodb引擎,独立表空间。未进行数据库备份,未开启binlog。 人为误操作使用Delete命令删除数据时未添加where子句,导致全表数据被删除。删除后未对该表进行任何操作。需要恢复误删除的数据。 在本案例中的mysql数据库未进行备份,也未开启binlog日志,无法直接还原数据库。
|
3月前
|
关系型数据库 MySQL 数据库
阿里云数据库RDS支持MySQL、SQL Server、PostgreSQL和MariaDB引擎
阿里云数据库RDS支持MySQL、SQL Server、PostgreSQL和MariaDB引擎,提供高性价比、稳定安全的云数据库服务,适用于多种行业与业务场景。
|
6月前
|
中间件 关系型数据库 Go
Go语言数据库编程:数据迁移与事务控制
本文介绍了《Go语言实战指南》中关于数据库编程的核心内容,涵盖使用 GORM 进行数据迁移与事务控制。主要内容包括:AutoMigrate 方法自动创建或更新表结构;事务控制的自动与手动实现方式;事务隔离级别的设置;以及在 Gin 框架中统一管理事务的实践建议。适合开发阶段的数据库结构管理和事务性操作需求。
|
8月前
|
负载均衡 算法 关系型数据库
大数据新视界--大数据大厂之MySQL数据库课程设计:MySQL集群架构负载均衡故障排除与解决方案
本文深入探讨 MySQL 集群架构负载均衡的常见故障及排除方法。涵盖请求分配不均、节点无法响应、负载均衡器故障等现象,介绍多种负载均衡算法及故障排除步骤,包括检查负载均衡器状态、调整算法、诊断修复节点故障等。还阐述了预防措施与确保系统稳定性的方法,如定期监控维护、备份恢复策略、团队协作与知识管理等。为确保 MySQL 数据库系统高可用性提供全面指导。
|
8月前
|
SQL 关系型数据库 MySQL
大数据新视界--大数据大厂之MySQL数据库课程设计:MySQL 数据库 SQL 语句调优方法详解(2-1)
本文深入介绍 MySQL 数据库 SQL 语句调优方法。涵盖分析查询执行计划,如使用 EXPLAIN 命令及理解关键指标;优化查询语句结构,包括避免子查询、减少函数使用、合理用索引列及避免 “OR”。还介绍了索引类型知识,如 B 树索引、哈希索引等。结合与 MySQL 数据库课程设计相关文章,强调 SQL 语句调优重要性。为提升数据库性能提供实用方法,适合数据库管理员和开发人员。
|
8月前
|
关系型数据库 MySQL 大数据
大数据新视界--大数据大厂之MySQL 数据库课程设计:MySQL 数据库 SQL 语句调优的进阶策略与实际案例(2-2)
本文延续前篇,深入探讨 MySQL 数据库 SQL 语句调优进阶策略。包括优化索引使用,介绍多种索引类型及避免索引失效等;调整数据库参数,如缓冲池、连接数和日志参数;还有分区表、垂直拆分等其他优化方法。通过实际案例分析展示调优效果。回顾与数据库课程设计相关文章,强调全面认识 MySQL 数据库重要性。为读者提供综合调优指导,确保数据库高效运行。

热门文章

最新文章

推荐镜像

更多