结合图文一起搞懂MySQL事务、MVCC、ReadView!

前言

上次讲完MySQL的三大日志 undolog、redolog、binlog后,有必要把关于MySQL事务分析的文章马上给续上,我们知道在多并发事务处理的MVCC【多版本并发控制】中是有涉及到undo log日志的。

不过我们要明确一点MySQL的InnoDB存储引擎支持事务, MyISAM 存储引擎是不支持事务。

📚 全文字数 : 9k+

⏳ 阅读时长 : 13min

📢 关键词 : 事务、事务隔离级别、MVCC、ReadView

阅读之前,提前了解下全文大纲,对阅读内容有个先前了解,这样对事务这块了解程度不同的朋友就可以选择不同的章节去读了。

什么是mysql事务?

Mysql事务(Transaction)用于保证数据的一致性,事务是在数据库管理系统中执行的一个逻辑操作单元,它是由一组列数据库操作组成的逻辑工作单元。

这一组操作要么全部成功,要么全部失败,不存在部分成功部分失败的情况,所有的操作共进退,因此事务是一个不可分割的逻辑单元,举个案例(来自)。

sql 复制代码
   现在A、B发生了转账行为(假设都有10w元,小明给小红转账10w元)
    -- 从A账户减去5w元
    UPDATE account SET money = money - 50000 WHERE name = "A";
    -- 小红对应的账户增加100元
    UPDATE account SET money = money + 50000  WHERE name = "B";
    -- 说明:
    上面的2条SQL可以被认为是一个事务
   其中有任何一条SQL语句执行错误或系统宕机都会导致数据恢复到最初执行这两条语句前(ROLLBACK)
    但是这两条SQL都执行成功的话则会提交事务(COMMIT)

事务解决了什么问题?

在原来没有事务的情况下,当多个用户同时执行对同一条数据的操作时,就会涉及到冲突问题。

比如,如果用户A在进行修改,而此时用户B也要进行修改,那么就可能会导致数据混乱或者损坏

通过使用事务,可以确保数据的准确性和完整性,还可以减少数据库故障对业务系统的影响,提高了系统的可用性和稳定性。

事务特性

事务有ACID四个特性:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability),实现事务须遵守这四个特性。

ACID

原子性(Atomicity):事务是一个不可分割的操作单元,要么完全执行,要么完全不执行。如果事务中的某个操作失败,那么整个事务都将被撤销,回滚到事务开始前的状态。

一致性(Consistency):事务在执行之前和执行之后,数据库的状态必须保持一致。这意味着事务执行过程中的任何变化都必须满足预定的规则和约束。

隔离性(Isolation):事务的执行应该与其他事务的执行相互隔离,即每个事务的操作独立于其他事务的操作。这确保了事务在并发执行时,不会相互干扰导致数据不一致或异常结果。

持久性(Durability):一旦事务被提交,其所做的更改将永久保存在数据库中,并且在系统故障或重启后仍然保持有效。

这么看ACID的概念可能不怎么好理解,这里举个栗子,加深下

ACID是如何保证的

MySQL事务的特性也是基于某些底层的功能来实现的,这些特性的实现如下:

  • 【持久性】通过 redo log (重做日志)来保证的
  • 【原子性】通过 undo log(回滚日志) 来保证的
  • 【隔离性】通过 MVCC(多版本并发控制+读写锁)来保证的
  • 【一致性】则是通过持久性+原子性+隔离性来保证

基本使用

日常开发中我们可能更多的基于ORM来进行事务的操作,各有各的用法,我们来看看MySQL是如何使用事务的。

默认情况下,MySQL处于自动提交模式,即每个语句都被视为一个事务,并自动提交到数据库

ini 复制代码
START TRANSACTION; //或者 BEGIN 来开启一个新的事务

DML语句1;
DML语句2;

ROLLBACK; //事务回滚

COMMIT; //事务提交

也就是说在你执行了这个命令后,MySQL会将接下来的所有语句视为事务的一部分,直到你提交或者回滚事务。

而ROLLBACK将撤销事务中的所有更改,并且回滚到事务开始之前的状态。

COMMIT使事务的更改永久生效,并将它们保存到数据库中。

DML(Data Manipulation Language)语句:数据操纵语句,用于添加、删除、更新和查询数据库记录

事务类型

MYSQL事务分为【隐式事务和显示事务】

隐式事务:

比如insert、update、delete语句,事务的开启、提交或回滚由mysql内部自动控制的,事务自动开启、提交或回滚。

我们可以通过 show variables like 'autocommit'查看是否开启了自动提交,autocommit为ON表示开启了自动提交

显示事务:

显式事务是指在应用程序中明确指定事务的开始和结束,使用BEGIN、COMMIT和ROLLBACK语句来控制事务的执行,语法如下:

sql 复制代码
BEGIN;
-- SQL statements
COMMIT;

多事务并发问题

我们知道在并发情况下和单线程处理问题的方式是不一样的,MySQL服务器支持多个Client进行连接,意味着存在多事务并发情况,同样在多事务并发情况下是存在脏读(dirty read)、不可重复读(non-repeatable read)、幻读(phantom read)的问题。

这几个问题我们一个个看,到底是什么现象

脏读

现象:在数据库访问中,一个事务读取了另一个事务未提交的数据,导致读取到的数据是不一致或者无效的,脏读一般是针对于update操作的。

不可重读

现象:不可重复读是指在一个事务内,多次读取同一数据,但是数据的值发生了改变

幻读

现象:幻读是一种数据库事务的并发问题,指的是在一个事务中,多次查询同一符合条件的的数据,出现了前后两次记录数量不一致的情况。

小结一下:

  • 脏读:读到其他事务未提交的数据
  • 不可重复读:前后两次读取的数据不一致
  • 幻读:前后读取的记录数量不一致

大家弄懂了几个的区别吗?如果不太清楚的,可以结合事务隔离级别的案例流程来了解

事务隔离级别

既然多事务并发情况下会出现脏读、不可重复读、幻读的情况,那么这些该如何避免呢?

如果数据库进行隔离操作,就能减少问题的发生了,事务的隔离级别是指在并发事务中,一个事务对数据的修改是否对其他事务可见,

以及其他事务对数据的修改是否对当前事务可见。但是不同的隔离级别还是会导致不同的并发问题发生,但是能对这些现象进行规避。

不同的事务隔离级别对应的情况如下:

隔离水平高低排序如下,但是隔离级别越高,性能效率就越低

不同的事务隔离级别在并发事务下也会产生不同的问题,如下图(脏读、不可重复读、幻读)

总结起来就是说

  • 【读未提交】隔离级别:可能发生脏读、不可重复读和幻读现象;
  • 【读提交】隔离级别:可能发生不可重复读和幻读现象,但是不可能发生脏读现象;
  • 【可重复读】隔离级别:可能发生幻读现象,但是不可能脏读和不可重复读现象;
  • 【串行化】隔离级别: 不会发生脏读、不可重复读和幻读现象

如何设置隔离级别

我们来验证MySQL默认的隔离级别是不是可重复读,可通过 show variables like 'transaction_isolation' 命令查。

sql 复制代码
// mysql 5.7之后查看隔离级别
show variables like 'transaction_isolation'; 

+---------------+-----------------+
| Variable_name | Value           |
+---------------+-----------------+
| tx_isolation  | REPEATABLE-READ |
+---------------+-----------------+

从上面的value值可以看出确实是可重复读,我们看下如何去修改隔离级别,两种方式:

1:可以在 MySQL 的配置文件 my.cnf、my.ini 中设置

比如设置为:transaction-isolation=REPEATABLE-READ # 可重复读

2:使用 SET TRANSACTION 命令改变单个或者所有新连接的事务隔离级别

css 复制代码
SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE}

比如:
set session transaction isolation level read committed;
  • 不带 GLOBAL 或 SESSION 关键字表示设置下一个事务的隔离级别;
  • 使用 GLOBAL 关键字表示对全局设置事务隔离级别,设置后的事务隔离级别对所有新的数据库连接生效;
  • 使用 SESSION 关键字表示对当前的数据库连接设置事务隔离级别,只对当前连接生效;
  • 任何客户端都可以自由改变当前会话的事务隔离级别,可以在事务中间改变,也可以改变下一个事务的隔离级别

分析不同的隔离级别

在数据库中新建测试表user,对于隔离级别的分析都基于user表进行,在分析不同的事务隔离级别额时候都会把问题先下结论,再进行场景分析。

sql 复制代码
// 表结构
CREATE TABLE `user` (
  `id` int NOT NULL AUTO_INCREMENT COMMENT '用户id',
  `name` varchar(32) COLLATE utf8_bin DEFAULT NULL COMMENT '姓名',
  `point` int DEFAULT '0' COMMENT '积分',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin

//插入一条记录
INSERT INTO user (name, point) VALUES('xiaoxu', 100)

读未提交(read uncommitted):问题(可能发生脏读、不可重复读和幻读现象)

arduino 复制代码
// 先将事务隔离级别设置为读未提交 read uncommitted
set session transaction isolation level read uncommitted;

现象解读

启动两个事务A、B,事务A查询到的值是100,然后进行Update将point设置为150,此时事务A未提交,而事务B查询的时候值是150。而此时事务B已经读取到150进行后续业务了,但是事务A混滚,事务B的数据就是脏数据了(脏读)

而如果一开始事务A查询的值是100,事务B进行Update了Point的值,改成200,事务A再查询的值就是200,事务B回滚后,造成了事务A两次读取结果不一致(不可重复度),这里就不再画图了。

而幻读这里同样解决不了

读已提交(read committed):可能发生不可重复读和幻读现象

arduino 复制代码
// 同样先将事务隔离级别设置为读未提交 read committed
set session transaction isolation level read committed;

现象解读

启动两个事务A、B,事务A查询到的值是100,此时事务B将point的值改为150(事务未提交),事务A查询到的值还是100,而事务B提交后,事务A查询到值是150。事务A相同条件下两次查询的结果不一致,虽然解决了脏读,但是没做到可重复读,同样没法解决幻读。

可重复读(repeatable read):可能发生幻读现象

arduino 复制代码
// 同样先将事务隔离级别设置为读未提交 repeatable read
set session transaction isolation level repeatable read;

现象解读

可重复读不会发生脏读和不可重复读问题,但是可能会发生幻读问题.

启动两个事务A、B,事务A查询到point > 50的记录得到一条记录,事务B查询到point > 50的记录也是一条记录,此时事务A插入一条point=150的记录,并提交事务,此时事务B再次查询point>50的记录,同样的条件,出现了两条记录。

这种和前一次读到的记录数量不一样了,就感觉发生了幻觉一样,这种现象就被称为幻读

串行化(serializable):脏读、不可重复读和幻读现象都【不可能会发生】

串行化是4种事务隔离级别中隔离效果最好的,解决了脏读、可重复读、幻读的问题,但是效果最差,它将事务的执行变为顺序执行。

通过举例不同隔离级别发生的情况,想必大家对多事务并发问题和事务的隔离级别会比较清楚了哈,换个思路:不同的事务隔离级别是为了解决多事务并发可能发生的不同问题,这样理解起来更顺!

需要注意的是,InnoDB默认的事务隔离级别是【可重复读】,但是InnoDB在这一级别,在部分场景下规避了幻读的问题,接下来看一下两种解决方式。 快照读 普通select 语句:是基于MVCC 多版本并发控制方式解决了幻读 当前读(select ... for update 等语句),是通过 next-key lock(记录锁+间隙锁)方式解决了幻读

快照读和当前读

MySQL读取数据实际上有两种模式,分别是当前读和快照读。

快照读:普通的select语句(不包括 select ... lock in share mode; select ... for update;),也就是 不加锁的select操作 都是采用 快照读的模式。MySQL使用MVCC (Multiversion Concurrency Control)机制来保证被读取到数据的一致性,读数据不需要进行加锁,不会被其他事务阻塞。

注:即使某个数据正在被修改或插入新数据的时候,也可以进行读取该数据,因为快照已生成,保证了读写不冲突。 而不同隔离级别下,创建快照的时机也不同:

  • read committed (读已提交):事务每次select时创建ReadView
  • repeatable read (可重复读):事务第一次select时创建ReadView,后续一直使用

当前读:数据修改的操作(update、insert、delete) 都是采用 当前读的模式,过对读取到的数据(索引记录)【加锁】来保证数据一致性,比如:

sql 复制代码
select ... lock in share mode; 
select ... for update; 
insert; update;
delete;

当前读,快照读和MVCC之间是什么关系呢?

  • MVCC 多版本并发控制是只是一个概念,并非具体实现
  • MySQL实现MVCC概念中,快照读就是MySQL为我们实现MVCC理想模型的其中一个具体非阻塞读功能。而相对而言,当前读就是悲观锁的具体功能实现
  • MVCC模型在MySQL中的具体实现则是由 三个隐式字段,undo日志 ,Read View 等去完成的

ok、明白了吗?小许这下也补缺查漏的明白了

MVCC多版本并发控制

解决了什么问题

多版本并发控制(MVCC)是一种用来解决读-写冲突的无锁并发控制,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能 同时还可以解决脏读,幻读,不可重复读等事务隔离问题,但不能解决更新丢失问题。

如何实现的

实现原理主要是依赖记录中的 三个隐式字段,undo日志 ,Read View 来实现的。

隐藏字段

InnoDB在每行数据都增加三个隐藏字段,一个唯一行号,一个记录创建的版本号,一个记录回滚的版本号,如下:

这里用之前的文章老图来看,有兴趣的同学可以回过去看看【传送门:MySQL InnoDB 行记录存储结构

db_row_id:6byte,隐含的自增ID(隐藏主键),如果数据表没有主键,InnoDB会自动以DB_ROW_ID生成一个聚簇索引。

db_trx_id:6byte,最近修改(修改、插入)事务ID:记录创建这条记录以及最后一次修改该记录的事务的ID,是一个指针。

db_roll_ptr:7byte,回滚指针,指向这条记录的上一个版本(上一个版本存储于,rollback segment里)。

Undo日志

undo log是为回滚而用,用于记录数据修改前的信息,需要注意的一点是,由于查询操作(SELECT)并不会修改任何用户记录,因此不需要记录相应的undo log。

不同事务或者相同事务的对同一记录的修改,会导致该记录的undo log成为一条记录版本线性表,即版本链表

因为undo log 记录事务修改之前版本的数据信息,因此假如由于系统错误或者rollback操作而回滚的话可以根据undo log的信息来进行回滚到没被修改前的状态。

有兴趣的同学可以回过去看看之前关于MySQL日志的文章【传送门:结合MySQL更新流程看 undolog、redolog、binlog

Read View - 读视图

什么是Read View?Read View就是事务进行快照读操作的时候生产的读视图(Read View),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID(当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以最新的事务,ID值越大)。

ReadView中主要包含4个比较重要的内容,分别是:

creator_trx_id ,创建这个Read View 的事务ID,即创建者的事务ID,而不是记录中的trx_id哦!

说明:只有在对表中的记录做改动时(执行INSERT、DELETE、UPDATE这些语句时)才会为事务分配事务id,否则在一个只读事务中的事务id值都 默认为0。

m_ids :表示创建ReadView时当前系统中活跃的事务的ID集合 ("活跃"指的就是,启动了但还没提交)。

min_trx_id :表示创建ReadView时活跃的事务中最小的事务 ID

max_trx_id:表示创建ReadView时系统中应该分配给下一个事务的id值,当前最大事务ID+1

而判断数据记录可见性的逻辑就是通过readview和【行记录的隐藏字段trx_id】做对比的

一个事务去访问记录的时候,怎么判断记录的可见性呢?

Read View决定当前事务能读到哪个版本的数据,从表记录到Undo Log历史数据的版本链,依次匹配,满足哪个版本的匹配规则,就能读到哪个版本的数据,一旦匹配成功就不再往下匹配。

遵循了以下可见性匹配规则:

规则说明:

  • trx_id = creator_trx_id:如果 trx_id 值等于创建Read View的事务Id,那么数据记录的最后一次操作的事务就是当前事务,该版本的记录对当前事务可见

  • trx_id < min_trx_id:如果 trx_id 值小于 Read View 中的 min_trx_id ,表示这个版本的记录是在创建 Read View 前已经提交的事务生成的,所以该版本的记录对当前事务可见

  • trx_id >= max_trx_id:如果trx_id 值小于 Read View 中的 min_trx_id ,表示这个版本的记录是在创建 Read View 后才启动的事务生成的,所以该版本的记录对当前事务不可见

  • min_trx_id <= trx_id < max_trx*id:判断 *trx_id 是不是在当前事务ID集合(m_ids)里面

  • 如果在m_ids中,则代表Read View生成时刻,这个事务还在活跃,还没有Commit,版本记录在前事务不可见

  • 如果不在m_ids中,则说明,这个事务在Read View生成之前就已经Commit了,版本记录在前事务可见

好了,关于MVCC的介绍就讲完了,小伙伴们花点时间结合图多分析分析!

MVCC隔离级别分析

从前面我们也总结了,在不同的隔离级别下快照读生成的ReadView规则不同,区别如下:

read committed (读已提交):事务每次select时创建ReadView,每个ReadView中四个字段的值都是不同的

repeatable read (可重复读):事务第一次select时创建ReadView,后面都是复用这个ReadView

对这两者的分析继续使用文章开头的user表作为基础,就不多建其他表进行案例分析了

读已提交分析

来看示例流程,事务A,B几乎同事查询一条记录,因为是read committed (读已提交) 隔离级别,所以每次select都会生成不同的ReadView

事务A、B查询流程图如下:

我们在来看事务ID分别为27和28的A、B读取trx_id 为 26 的记录,事务A进行了两次次查询,而第二次是在事务B提交之后,我们来两次次查询生成的ReadView的区别:

此时事务A能查询到point值为100, 符合db_trx_id < min_trx_id规则,所以能查询到当前版本数据记录, 但是第而次读之前事务B对进行了修改,并提交了事务,此时可见的版本链数据如下图:

此时表记录中的隐藏记录db_trx_id的值是28,符合规则 min_trx_id <= db_trx_id < maxtrxid(27<=28<29),并且当前数据版本的事务ID不在当前系统中活跃的事务m_ids集合,可以看到当前版本的数据,也就是能查询到 point的值是150。

因此整个过程中,同一个事务A内,相同的查询条件,查询到的数据不一致,也就是出现了不可重复读的情况。

可重复读分析

可重复读在事务第一次select时创建ReadView,后面都是复用这个ReadView,这个和读已提交的区别所在。

事务A、B的执行情况和读已提交的流程一样,都是针对同一条记录修改前后事务提交的两次查询,但是两次查询出来的都是一样的,值都是100。

但是两次查询的ReadView共用一个,结果如下:

可以看出符合规则规则 min_trx_id <= db_trx_id < max_trx_id(27<=27<29),并且当前数据版本的事务ID不在当前系统中活跃的事务m_ids集合,所以是不可以看到当前版本的数据,也就是为什么事务B提交了,但是第二次查询出来的point的值还是100。

所以通过这样的方式就实现了,就是通过复用原有ReadView的方式解决了重复读问题。

总结

看会文章之后的你可以摊牌了,关于MySQL事务你基本都会了,难不到你了,你已经又打败了一个知识点,你又可以昂首向前了!(O(∩_∩)O哈哈~)

对写作的理解:其实写文章之前自己对不少内容要么忘记了,要么也是不够清楚,有了前文没下文,最主要的还是写之前梳理要写的内容太重要了,列个提纲,不然会无厘头,思路会断!

👨👩 朋友,希望本文对你有帮助~🌐

欢迎点赞 👍、收藏 💙、关注 💡 三连支持一下~🎈

我是小许,下期见~🙇💻

参考文章:

MySQL事务详解_mysql 事务_树窗的博客-CSDN博客

图文结合带你搞懂InnoDB MVCC-腾讯云开发者社区-腾讯云

硬核解析MySQL的MVCC实现原理,面试官看了都直呼内行 | HeapDump性能社区

相关推荐
i道i6 小时前
MySQL win安装 和 pymysql使用示例
数据库·mysql
qq_17448285756 小时前
springboot基于微信小程序的旧衣回收系统的设计与实现
spring boot·后端·微信小程序
锅包肉的九珍7 小时前
Scala的Array数组
开发语言·后端·scala
心仪悦悦7 小时前
Scala的Array(2)
开发语言·后端·scala
Oak Zhang7 小时前
sharding-jdbc自定义分片算法,表对应关系存储在mysql中,缓存到redis或者本地
redis·mysql·缓存
2401_882727578 小时前
BY组态-低代码web可视化组件
前端·后端·物联网·低代码·数学建模·前端框架
心仪悦悦8 小时前
Scala中的集合复习(1)
开发语言·后端·scala
代码小鑫8 小时前
A043-基于Spring Boot的秒杀系统设计与实现
java·开发语言·数据库·spring boot·后端·spring·毕业设计
真心喜欢你吖9 小时前
SpringBoot与MongoDB深度整合及应用案例
java·spring boot·后端·mongodb·spring
久醉不在酒9 小时前
MySQL数据库运维及集群搭建
运维·数据库·mysql