vlambda博客
学习文章列表

十分钟了解MySQL事务机制


读书百遍其义自现。

MySQL数据库在我们平时工作学习中的使用频率是相当之高,彻底掌握MySQL的事务机制对我们平时工作会有非常大的帮助,仔细回忆一下,你是否对MySQL事务相关的知识是否完全掌握?是否感觉有的地方有些模糊?通过这篇文章让你彻底理顺逻辑,下面就开始吧!

01
事务的特性

事务的特点

提到数据库的事务,我们肯定脱口而出的就是ACID,但是有的时候并不清楚ACID到底是什么,掌握事务的特性对于我们接下来学习是相当重要的。


首先举个例子,老王去银行转账的例子将农业银行的100元转到老刘农业银行卡,主要有以下三步

1.首先判断老王的农业银行的账户金额是否大于100元

2.老王的农业银行的账户余额减去100元

3.老刘的农业银行的账户余额加100元

试想一下如果其中某一步失败就会导致非常严重的问题,我们需要把操作放到一个事务中去执行,如果其中有一步执行失败,那么所有操作都将回滚,通过这种方式来保证数据一致性。

 start transaction;
 select balance from account where uid = 60610;
 update account set balance = balance - 100 where uid = 60610;
 update account set balance = balance + 100 where uid = 60611;
 commit;

原子性(atomicity)

原子在化学中表示不可再进行分割的基本微粒,在事务表示的是这是一个不能再被分割的最小工作单元,整个事务的所有操作,要么全部成功,要么全部失败回滚,而不可能仅仅执行某一部分。


一致性(consistency)

数据库总是从一个一致性的状态转化到另一个一致性的状态,老王转账即使第二步和第三步之间系统崩溃了,也不会出现问题,因为数据并没有提交,没有提交事务,所做的修改也并不会保存到数据库中。


隔离性(isolation)

通常来说,一个事务所做的修改在最终提交前,对其他的事务是不可见的,这里为什么说“通常是不可见的”,因为这和具体的事务隔离级别有关,后面我们会进行分析。


持久性(durability)

一旦事务提交,则其他的修改就会永远保存到数据库中。即使系统崩溃,修改的数据也不会丢失。

02
事务隔离级别

事务隔离级别

所谓事务隔离级别,就是用来定义一个事务所做的修改,哪些在事务内和事务间是可见的,哪些是不可见的。较低级别的隔离通常可以执行更高的并发,系统开销也更低 。

在MySQL中一共有四种事务隔离级别:读未提交、读提交、可重复读、可串行化,下面我们将分别简单介绍四种隔离级别。


读未提交 read uncommitted

在此事务隔离级别下,修改数据库中的数据,即使没有提交,对其他事务也是可见的。事务可以读到未被提交的数据,这种情况也被称为脏读,这种隔离级别下会出现很多问题,但性能也不会比read committed好太多。


读提交 read committed

大多的数据库的默认事务隔离都是读提交,但是MySQL的默认事务隔离基本是可重复读,在这种隔离级别下,只有当事务提交后,事务中的修改才会对其他事务可见,但是会出现在同一个事务中多次查询数据结果可能不同,因为中间可能有修改数据的事务提交了,这种现象叫不可重复读


可重复读 repeatable read

该级别不仅解决了脏读,还可以在一个事务中,同一个事务多次读取同样的记录结果是一样,但是理论上,还是无法解决幻读现象,所谓幻读,就是在一个事务执行中,其他事务插入了新的行并且提交,就会产生幻行。但实际上MySQL通过MVCC(多版本并发控制)解决了幻读的问题。


可串行化 serializable

该级别是最高的隔离级别,避免了前面说的幻读的问题,顾名思义是对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。实际中很少使用这个隔离级别,因为使用这个隔离级别,几乎没有并发可言。


各个事务级别对应的能力

十分钟了解MySQL事务机制

03
验证事务隔离级别

MySQL基础设置

首先登录MySQL。

十分钟了解MySQL事务机制

查看当前数据库

十分钟了解MySQL事务机制

创建测试数据库

十分钟了解MySQL事务机制

可以看到刚刚新建的数据库

十分钟了解MySQL事务机制

使用刚刚创建的数据库

十分钟了解MySQL事务机制

创建测试表

十分钟了解MySQL事务机制

 create table tb_user (
     id bigint not null auto_increment comment "用户id",
     name varchar(50) not null comment "用户姓名",
    age int not null comment "用户年龄",
    balance int not null comment "用户余额",
 PRIMARY KEY (`id`)
 )ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8 COMMENT='用户表';


初始化数据

十分钟了解MySQL事务机制

SQL数据

 insert into tb_user(name, age, balance) values('老王', 24, 100);
 insert into tb_user(name, age, balance) values('老刘', 25, 500);

测试之前做的一些准备

MySQL中默认的是事务自动提交的,在测试我们需要控制事务提交的时机,所以我们将MySQL改为非自动提交事务,这种方式仅仅是当前session关闭自动提交。

十分钟了解MySQL事务机制

设置全局关闭自动提交

十分钟了解MySQL事务机制

查看自动提交事务状态,可以看到当前session和全局的自动提交都已经关闭。

十分钟了解MySQL事务机制

验证读未提交 read uncommitted

这里我们约定,每次测试完成后都将余额还原成初始值,即老王余额100元,老刘余额500元。

因为MySQL的默认事务隔离级别是可重复读,我们先将当前session的事务隔离级别设置为读未提交。

十分钟了解MySQL事务机制

修改之后的事务隔离级别

十分钟了解MySQL事务机制


下面启动两个事务A、B并设置它们的事务隔离级别为读未提交。


十分钟了解MySQL事务机制

开启事务A和事务B

十分钟了解MySQL事务机制

十分钟了解MySQL事务机制

事务A查询老王的余额(下图应是查询结果)

十分钟了解MySQL事务机制

老王充值50元

十分钟了解MySQL事务机制

事务B读到了未提交的数据,出现了脏读。

十分钟了解MySQL事务机制

事务A回滚

十分钟了解MySQL事务机制

老王账户减少150元,并提交事务B

十分钟了解MySQL事务机制

此时查看老王的账户余额可以发现为-50,可以看出由脏读引发的问题。

十分钟了解MySQL事务机制


验证读已提交 read committed

首先我们需要把数据进行初始化,即老王余额100元,老刘余额500元。将两个session的事务隔离级别设置为读已提交。

十分钟了解MySQL事务机制


让事务A、B依次执行测试读未提交时的逻辑,同样的条件下,在读已提交的情况下,事务B并没有读到脏数据。


十分钟了解MySQL事务机制


接下来,我们做下一组测试,事务A和事务B按照下面的顺序进行执行。


十分钟了解MySQL事务机制

事务B查询老刘账户余额为500元。

十分钟了解MySQL事务机制

事务A向老刘账户充值500元并提交事务。

十分钟了解MySQL事务机制

事务B再次查询老刘的余额,为1000元,在同一个事务中,重复进行查询返回的结果不同,这就是不可重复读。

十分钟了解MySQL事务机制

可重复读 repeatable read

首先我们需要把数据进行初始化,即老王余额100元,老刘余额500元。将两个session的事务隔离级别设置为可重复读。

十分钟了解MySQL事务机制


下面我们启动事务A、B,重新按顺序执行读已提交的测试逻辑,通过测试我们发现,在可重复读级别下,在同一个事务中多次查询结果是一致的,解决了不可重复读。


十分钟了解MySQL事务机制


接下来,我们来看另一组测试,把数据进行初始化,即老王余额100元,老刘余额500元。


十分钟了解MySQL事务机制

查询当前用户表中有几个用户,可以看到查询到了两个用户。

十分钟了解MySQL事务机制

在事务B中插入用户老赵,并提交事务,此时数据中应该有三条记录。

十分钟了解MySQL事务机制

在事务A中查询数据库记录条数,我们发现记录数还是2,说明MySQL在repeatable read级别成功避免了幻读,那么MySQL是如何在RR级别避免幻读的呢?下文会进行分析。

十分钟了解MySQL事务机制

验证可串行化 serializable

这里没有对可串行化 serializable隔离级别进行验证,因为在此隔离级别会给所有的读取行加锁,所有事务只能串行执行。

04
MVCC如何解决幻读问题

MVCC(Multiversion Concurrency Control)即多版本并发控制,大多数的数据库都实现了MVCC,但是实现机制各不相同,并且没有统一的标准。

以InnoDB引擎为例,对MVCC的工作流程进行讲解。

在使用InnoDB引擎的表中是通过在每条记录后面保存两个隐藏的列来实现的。这两个列,一个保存了行的创建的时间,另一个保存行的删除时间。

这里其实保存的并不是创建时间,而是系统的版本号(system version number)。每开始一个新事务,系统版本号都会自动递增,事务开始时刻的系统版本号会作为事务的版本号,用来和查询到的每行记录版本号进行比较。

下面通过在repeatable read隔离级别下,对于增删改查MVCC是如何操作的。

SELECT查询

InnoDB会根据两个条件去检查每行记录。

1、InnoDB只查版本早于当前事务的数据行(也就是,行的创建版本号小于或者等于事务的系统版本号),这样做是为了保证,事务读取的行,要么是事务开始前已经存在了,要么是事务自身插入或者修改过的。

2、关于行的删除版本,我们读取的行,要么没有定义,要么大于当前事务的系统版本号(这说明是事务开启之后修改的),这样就可以确保我们读取到的数据,都是在事务开启之前未删除。

这也就解释了为什么在可重复级别可以避免幻读,等同于给数据库的表和数据做了一个快照,相当于在一个视图上进行操作,无论其他事务怎么修改数据,在当前事务提交之前,多次查询的结果都是相同的,除此之外我们可以发现,在数据库中同一条记录可能存在多个版本。

INSERT插入

InnoDB为新插入的每一行保存当前事务的系统版本号,作为行的创建版本号。


DELETE删除

InnoDB为删除的每一行保存当前事务的系统版本号,作为行删除标记。


UPDATE更新

InnoDB为插入一行新记录,保存当前事务系统版本号作为行的创建版本号,同时保存当前事务的系统版本号作为行删除标记。


关于MVCC

通过上述介绍的机制,通过两个额外的版本号字段,使得大多数的读操作不需要加锁,这样设计使得读数据操作很简单,性能很好,但是需要额外的存储空间,并且需要更多的检查和维护工作。

此外,MVCC只在REPEATABLE READ和READ COMMITTED两个隔离级别下工作。其他两个隔离级别和MVCC都不兼容,因为读未提交总是读取最新的数据行,而可串行化则会对所有行都加锁。

05
总结

本文主要介绍了,MySQL的事务的特性ACID,除此还介绍了事务的各个隔离级别的特征,不同的隔离级别会出现什么样的问题,理论上,事务的隔离级别越高消耗的资源越多,在平时的业务开发中,需要根据业务特点去选择不同的事务隔离级别,最后我们介绍了MVCC,并说明它如何解决幻读问题的,以及MVCC在可重复读下,增删改查的操作,希望读者能够掌握本文的内容,相信对理解MySQL的事务会有一定的帮助。