深入解读 MySQL 的 MVCC 与事务隔离级别

数据库 MySQL
本文我们将一同踏上这场解析 MySQL MVCC 和事务隔离级别的精彩之旅,逐步揭开它们神秘的面纱,探寻其背后蕴含的深刻原理和实际应用价值。

在当今的数据库世界中,MySQL 以其强大的功能和广泛的应用备受瞩目。而其中的 MVCC(多版本并发控制)和事务隔离级别更是关键且核心的概念,它们犹如数据库运行的精密齿轮,协同作用确保着数据的完整性、一致性和高效的并发处理。

当我们深入探究 MySQL 的内部机制时,MVCC 展现出其独特的魅力,它巧妙地解决了并发操作中可能产生的诸多问题。与此同时,事务隔离级别则为不同场景下的数据处理提供了灵活而精准的规则框架。理解这两者,不仅是对 MySQL 技术精髓的把握,更是开启高效数据库应用和系统开发的关键钥匙。在接下来的篇章中,我们将一同踏上这场解析 MySQL MVCC 和事务隔离级别的精彩之旅,逐步揭开它们神秘的面纱,探寻其背后蕴含的深刻原理和实际应用价值。

一、详解事务的基本概念

1. 什么是事务

现在我们开发的一个功能需要进行操作多张表,假如我们遇到以下几种情况:

  • 某个逻辑报错
  • 数据库连接中断
  • 某台服务器突然宕机
  • .......

这时候我们数据库执行的操作可能才到一半,所以为了避免这种一半一半的情况,我们就需要事务来保证数据一致性。 所以事务就是当作一个原子的逻辑组操作,要么全都成功执行,要么全部都失败。事务有分分布式事务和数据库事务,如果没有特指,我们平时所说的事务都是数据库事务,也就是本文探讨的话题。

2. 事务的四大特性

  • 原子性(Atomicity):一组操作要构成一个原子,原子可以看作事务的最小单位,不可在进行分割了,要么都执行,要么都不执行。
  • 一致性(Consistency):经过一个事务的操作后,前后要保持数据一致性,例如我们要用数据库记录一次转账操作,那么两个数据经过转账逻辑之后总额还是保持不变。
  • 隔离性(Isolation):在并发场景下,每个事务之间的操作互不干扰。
  • 持久性(Durability):存储到数据库中的数据永不丢失,及时数据库发生故障,当然机器被破坏了那就另说了。

3. 并发事务带来那些问题

这里笔者先说一个概念,具体会在后文示例中详尽介绍。

脏读:我们举个例子:

  • 我们开启一个事务A,准备读取user表的数据。
  • 此时,事务B将事务A要读取的数据修改了,但事务还没提交.
  • A却能看到这个未提交的结果即sex为1(而且这个结果后续还不一定提交)。

这种其他事务还没提交的结果能被另一个事务看到的情况就属于脏读。

幻读:我们再举个例子:

  • 事务A查询user表,此时表中有10条数据。
  • 在此期间,事务B插入5条数据。
  • 事务A再次查发现有15条事务。

这种同一次事务两次查询结果不一致的情况是幻读:

不可重复读,仍然举一个例子:

  • 事务A读取id为1的数据,name为xiaoming。
  • 事务B在此期间更新id为1的数据并提交这个事务
  • 结果事务A再次读取时发现name变了。 这就是不可重复读。

你可能会问了,这和幻读听起来是一个概念啊,他俩有什么区别? 幻读说是针对插入或者删除操作后导致数据前后不一致的情况,而不可重复读是针对两次相同查询操作出现数据不一致。

数据丢失:这个就很好理解了,高并发场景下,事务A修改id为1的money+100,事务B修改id为1的money+200,他们统一时间读取,先后写入,这就导致如果事务A后写入,那么money最后只加了100,如果事务B后写入,那么money就少了100。

二、详解事务的隔离级别

1. 读未提交(READ UNCOMMITTED)

在这个级别下,任何事务的修改操作即使没有提交,其他事务也能看到,造成我们上述所说的脏读,对此我们不妨用下面这段SQL来验证一下:

首先我们先建个测试表:

create table test2 (id int,name varchar(10),money int); 
insert into test2 values(1,'xiaoming',100);
insert into test2 values(2,'xiaowang',100);

事务A开启事务,进行test2  的更新操作,不提交

start transaction;
-- 小明+100元
update test2   set money = money +100 where name ='xiaoming';
-- 小王减100元
update test2   set money =money -100 where name ='xiaowang';

事务B设置为读未提交的隔离级别:

SET SESSION TRANSACTION ISOLATION LEVEL READ committed;
select * from test2 t ;

查询结果是事务B看到了事务A的更新操作,造成脏读。

对应结果如下:

id|name    |money|
--+--------+-----+
 1|xiaoming|  200|
 2|xiaowang|    0|

同理这个读未提交,也会造成:

  • 幻读(同一个事务同一次查询记录数不一样)
  • 不可重复读(同一个事务下查询记录的值不一样)

2. 读已提交(READ COMMITTED)

这个概念也很好理解,每个事务只能看到其他事务提交后的数据。避免了脏读,但是无法避免幻读和不可重复读。 我们就以幻读为例,如下图,事务B首先查询到数据表中没有id为1的用户,在这个查询结束后,事务A进行一次插入操作但是事务还未提交。

然后事务A将数据提交,事务B再次查询就发现了数据,出现幻读:

了解流程之后,我们拿SQL印证一下,首先创建数据表:

drop table if exists account1;


CREATE TABLE `account1` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(50) DEFAULT NULL,
  `balance` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `account1_un` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=UTF8MB4;

事务B查询,没数据:

SET SESSION TRANSACTION ISOLATION LEVEL READ committed;
START TRANSACTION;

-- 查询表,此时没有数据
SELECT * from account1;

事务A在此期间插入,事务不提交:

SET SESSION TRANSACTION ISOLATION LEVEL READ committed;
START TRANSACTION;
-- 在上一个事务查询后,插入一条事务但是不提交
insert into account1(id,name,balance) values(1,'zhangsan',1000);

此时事务B还是没看到数据,然后我们将上述的事务A数据commit,事务B看到这条数据出现幻读:

3. 可重复读(REPEATABLE READ)

这个隔离级别,也很好理解,同一个事务内,多次查询的数据都是一样的。我们不妨基于上面的例子实验一下

首先事务B查询,没有任何数据:

SET SESSION TRANSACTION ISOLATION LEVEL  REPEATABLE READ;
START TRANSACTION;
select * from account1 a  where id=3;

此时xiaoming的数据为300:

id|name    |balance|
--+--------+-------+
 3|xiaoming|    100|

事务A执行更新并提交:

SET SESSION TRANSACTION ISOLATION LEVEL  REPEATABLE READ;
START TRANSACTION;
update account1 set balance=0 where id=3;
commit;

事务B再查数据还是不变,还是300:

id|name    |balance|
--+--------+-------+
 3|xiaoming|    100|

总的来说可重复读避免了脏读和不可重复读,但是幻读还是无法避免:

4. 串行化(SERIALIZABLE)

事务隔离最高级别,通过锁的方式控制并发流程,解决上述一切问题。

三、详解多版本并发控制MVCC

1. 当前读和快照读

快照读:即读取数据是从快照中获取的,事务在进行事务读取时不上锁,这就是mysql并发读写性能高的原因之一。 而当前读反之,读取数据时会上锁,这也就意味着即使你的隔离级别是可重复读,你用当前读也能读取到其他事务的最新结果,造成不可重复读。

我们举个例子,首先事务A读取数据,假设数据值是100:

begin;
-- 读取到a的money为100
select * from account1 a ;

事务B更新事务并提交:

update account1 set money=1000 where id=1;

事务A使用快照读,数据还是100:

select * from account1 a ; --快照读 旧数据

一旦使用当前读,就是其他事务提交的新数据了:

--两个都是当前读,得到最新结果
select * from account1 a for update; 
select * from account1 a lock in share mode;

2. undo.log概念扫盲

首先说说undo log,在innoDB的聚簇索引中,每一条记录除了我们表中的数据以外,还会额外记录名为事务id(transaction id)的隐藏列。每当用户对当前数据进行修改操作后,新值的数据的事务id就会递增。 同时每行数据还有一个回滚指针(roll_pointer),如下图所示,每当用户对索引进行更新之后,旧的数据就会被存放到undo log中,新的数据的回滚指针指向这条最新的旧数据(就是刚刚存到undo log中的数据,通俗的说是最新的垃圾),用于后续可能需要的回滚操作:

3. readView概念扫盲

接下来就说说readView,readView就是真正用到undo log的东西,如下图所示,它由三个部分组成,分别是:

  • 已提交事务:已提交事务中记录的则是已经被提交的事务id集合。
  • 活跃事务:这个则记录那些还能活动且还没被提交的事务,其中min_trx_id指向活跃事务的最小值。
  • 未开始事务:这里面则是存放待使用的事务id值,其中max_trx_id就是记录这一块的最小值。

4. 基于可重复读版本理解SQL的MVCC工作机制

了解了undo.log和readView,我们就可以了解mvcc的工作机制了。就先以可重复读RR为例,我们来了解一下如何结合undo.log和readView实现可重复读的。

可重复读这个级别的readView只会在事务刚刚开始时创建,这也就意味着后续数据无论怎么变化,readView都以第一次创建的为主:

假设我们现在account表数据存在一条id为1的数据xiaoming,然后事务trx_id为100的事务基于RR级别将name先更新为xiaoming_50然后再更新为xiaoming_100,但是事务还没提交,此时对应的版本链如下所示:

需要注意的是,只有进行SQL修改操作即insert、update、delete才会分配一个事务id,所以我们本在进行查询之前执行一些无关紧要的update操作,生成一个事务200开始查询执行下面这条sql查询,即查询id为1的数据:

-- 执行一些无关紧要的update
select * from account1 a where id=1;

然后事务启动创建readView,结合版本链记录来看,活跃但是未提交事务值为100,即min_trx_id为100,而我们的事务为200,这也就意味着max_trx_id为201,由此可得活跃的读写事务m_ids列表有100、200之间。

所以事务200生成readView如下,然后顺着版本链开始获取数据首先看到xiaoming_100事务id为100处于活跃事务列表不符合要求继续顺着指针往下走,看到xiaoming_50也不符合要求,继续顺着指针往下走,看到xiaoming事务id值为80小于min_trx_id即已提交的事务中的值,所以我们事务200查询结果就是xiaoming:

此时事务100将更新结果提交,因为可重复读生成readView永远是以第一次创建时候为主,这也就意味着查询的思路还是和上述步骤一样,查询结果仍然是xiaoming,这里就不多做赘述了。

5. 基于读已提交版本readView理解SQL的MVCC工作机制

读已提交版本会在每次执行查询时生成一个readView,我们还是以上面的例子进行演示,还是事务100触发修改但是还没提交,对应生成的版本链如下:

还是同理,执行一些无关紧要的修改操作生成本次的事务id为200然后开始查询,因为事务100没有提交,所以活跃的事务列表数据为100、200生成readView如下:

所以顺着版本链查询到结果也是小于min_trx_id最大值为80,最终查询结果为xiaoming。

然后事务100将结果提交,此时我们的事务200再次进行查询,由读已提交生成readView为每次查询时可得,事务100已提交所以该事务处于已提交事务范围,然后我们的事务200还未提交,所以处于活跃事务列表中,所以活跃事务列表只有我们的事务200:

由此顺着版本链定位到小于min_trx_id的最大值为100,顺着版本链定位到的第一个trx_id为100的结果是xiaoming_100,所以事务200查询结果就是xiaoming_100。

6. MySQL 的隔离级别是基于锁实现的吗

是基于锁和mvcc共同实现的,SERIALIZABLE 这个隔离级别就是基于锁实现的,其他隔离级别都是基于mvcc,需要补充的是REPEATABLE-READ 如果使用当前读也是基于锁实现。

7. MySQL 的默认隔离级别是什么

以笔者使用的MySQL8来说使用如下命令可以看到默认级别为可重复读:

select @@transaction_isolation;

对应输出结果如下:

@@transaction_isolation|
-----------------------+
REPEATABLE-READ        |

四、小结

MySQL 的 MVCC(多版本并发控制)是其实现高效并发处理的关键机制。

通过 MVCC,在并发读写操作时,读操作不会阻塞写操作,写操作也不会阻塞读操作,极大地提高了数据库的并发性和性能。

它允许事务读取到特定版本的数据,实现了事务隔离级别的灵活控制。使得不同的事务可以看到符合其隔离级别要求的数据视图。

在 MVCC 中,每行数据都有多个版本,记录了不同事务对其的修改历史。这种方式有效地避免了锁竞争带来的性能开销和潜在的死锁问题。

对于理解和优化数据库的并发操作,MVCC 是一个至关重要的概念。深入研究和掌握它,有助于更好地设计和管理数据库系统,确保数据的一致性和高效性。

责任编辑:赵宁宁 来源: 写代码的SharkChili
相关推荐

2020-10-13 10:32:24

MySQL事务MVCC

2018-12-19 16:46:38

MySQL事务隔离数据库

2021-07-26 10:28:13

MySQL事务隔离

2024-04-26 09:17:20

MySQL事务隔离

2021-08-04 13:19:42

MySQL 事务隔离

2024-12-02 08:37:04

2010-11-19 16:13:06

oracle事务隔离级

2021-10-19 10:10:51

MySQL事务隔离级别数据库

2009-06-29 17:54:47

Spring事务隔离

2023-10-13 07:54:49

数据库MySQL

2022-06-10 11:51:49

MySQL事务隔离

2020-09-21 18:44:35

MySQL

2019-10-15 10:23:13

服务器MySQL 数据

2021-12-27 09:20:13

事务模式隔离

2017-08-09 14:34:12

MysqlJavaPython

2021-08-30 20:12:11

MySQL事务隔离

2023-10-11 08:09:53

事务隔离级别

2020-03-05 09:33:15

数据库事务隔离事务

2021-01-18 11:49:26

面试事务隔离

2023-09-22 08:27:39

点赞
收藏

51CTO技术栈公众号