一文带你看懂MySQL事务的深入浅出

注:本文主要论述了MySQL事务的ACID特性及数据库并发场景下容易产生的脏读、不可重复读、幻读问题,四种隔离级别的详细介绍等。

创作不易,希望大家一键三连支持!!!♥♥♥

创作不易,希望大家一键三连支持!!!♥♥♥

创作不易,希望大家一键三连支持!!!♥♥♥

一. MySQL事务

1.1 定义

事务就是一组原子性的SQL查询(一个独立的工作单元),事务内的语句,要么全部执行成功,要么全部执行失败,没有介于两者的中间状态。

1.2 特性

事务有4大特性,称为ACID特性:原子性(atomicity)、一致性(consistency)、隔离性(isolation)、持久性(durability)。

原子性一个事务必须被视为一个不可分割最小工作单元,整个事务中的所有操作要么全部提交成功,要么全部提交失败,对于一个事务来说,不可能只执行其中的一部分操作。

注:
(1) MySQL的原子性是通过undo log来实现的。它是InnoDB存储引擎特有的,实现机制为:把所有对数据的修改(增/删/改)都写入日志(undo log)
(2)undo log逻辑日志,可以理解为:记录和事务操作相反的SQL语句,事务执行insert语句,undo log就记录delete语句。它以追加的方式写入日志,不会覆盖之前的日志。
(3)若一个事务中的部分操作成功,但部分操作由于断电、系统崩溃等原因导致无法成功执行,那么就能通过回溯日志,将已执行成功的操作撤销,从而达到全部操作失败的目的。

一致性事务应确保数据库的状态从一个一致状态转变为另一个一致状态。无论事务提交还是回滚,都不会破坏数据的完整性。如A给B充值50元,如果事务提交成功,A的账户必然少50元,而B的账户必然会多50元;而如果提交失败了,那么将会全部不执行,则A和B的账户余额均不会变。其中A和B的账户余额变动必然是一个完整的过程(不会出现A少30元,B多30元的中间状态),整个过程必须一致

隔离性:通常来说,一个事务所做的修改最终提交以前对其他事务不可见的(后续在考虑事务隔离级别时会遇到在提交前可见的情况!)

持久性一旦事务提交,则其所做的修改就会永久保存到数据库中。此时即使系统崩溃,修改的数据也不会丢失。

注:
(1)MySQL的持久性是通过redo log来实现的。它也是InnoDB存储引擎特有的,实现机制为:当发生数据修改(增/删/改)时,InnoDB会先把记录写到redo log中,并更新内存,同时InnoDB会在何时时机将记录刷写到磁盘中
redo log物理日志记录的是在某个数据页做了什么修改,而不是SQL语句的形式。它有固定大小,是循环写的方式写入日志空间用完后会覆盖之前的日志
undo logredo log并非直接写入磁盘,而是先写入log buffer,再等待由操作系统决定何时刷写到磁盘中,具体流程如下:
请添加图片描述
从上图我们不免能发现两种log文件都从log bufferOS buffer,再到磁盘。故其中间过程还是可能因各种原因导致日志丢失。为此MySQL提供了3种持久化的方式:涉及到一个参数innodb_flush_log_at_trx_commit,该参数控制InnoDB将logbuffer中的数据写入OS buffer,并刷写磁盘时间点,取值为0/1/2,默认是1,三个值分别代表如下:
请添加图片描述
MySQL默认设置的方式1,也就是每次提交后直接写入OS buffer,并且调用系统函数fsync()把日志写到磁盘上。就保证数据一致性的角度来说,这种方式是最安全的。但安全大多数时候意味着效率偏低。每次提交都直接写入OS buffer并且写到磁盘,无疑会导致单位时间内IO的次数过多而效率低下。除此之外,还有方式0方式2。基本上都是每秒写入磁盘一次,所以效率都比方式1更高。但是方式0是把数据先写入log buffer再写入OS buffer再写入磁盘,而方式2直接写入OS buffer,再写入磁盘,少了一次数据拷贝的过程(从log buffer到OS buffer),所以方式2比方式0更加高效

了解了undo log和redo log的作用和实现机制之后,那么这两个日志具体是怎么让数据库从异常的状态恢复到正常状态的呢?(回答持久性中即使系统崩溃,修改的数据也不会丢失的原因?)
数据库系统崩溃后重启,此时数据库处于不一致的状态,必须先执行一个crash recovery的过程:首先读取redo log,把成功提交但是还没来得及写入磁盘的数据重新写入磁盘,保证了持久性。再读取undo log将还没有成功提交的事务进行回滚,保证了原子性。crash recovery结束后,数据库恢复到一致性状态,可以继续被使用

一个实现了ACID特性DB,相比没有实现ACID的DB,通常会需要更强的CPU处理能力更大内存更多磁盘空间

1.3 隔离级别

SQL定义了4种隔离级别,每种级别都规定了一个事务中所做的修改,哪些在事务内事务间可见的,哪些是不可见的。较低级别的隔离通常可以执行更高的并发,系统的开销也更低

如下介绍了4大隔离级别:

READ UNCOMMITTED(未提交读):事务中的修改,即使未提交对其他事务也都是可见的,常常会引发脏读(一个事务读取到另一个事务还未提交的数据)。

READ COMMITTED(提交读):一个事务从开始到提交之前,做的任何修改对其他事务都是不可见的。也叫不可重复读,因为两次执行同样的查询,得到的结果可能不一样。

REPEATABLE READ(可重复读):保证在同一个事务中多次读取同样的记录的结果是一致的。该级别解决了脏读的问题,但无法解决幻读(某个事务在读取某个范围内的记录时,另一事务又在该范围内插入了新的记录,当之前的事务再次读取该范围内的记录时,会产生幻行)。该级别同时也是MySQL的默认事务隔离级别

SERIALIZABLE(可串行化):最高的隔离级别。通过强制事务串行执行避免了幻读问题。该级别会在读取的每一行数据上都加锁,故可能导致大量超时锁争用。实际场景只有在非常需要确保数据一致性且可以接受没有并发的情况下才去使用。

RC级别解决了脏读问题,RR级别解决了不可重复读问题,SERIALIZABLE级别可以解决幻读问题。

下面将依次介绍脏读、不可重复读、幻读

脏读(一个事务读取到了另一个事务还未提交的数据):

请添加图片描述

上图可以看到,开启A、B两个事务,其中B事务在A事务还未提交之前进行了查询,得到了A事务还未提交的更新。

不可重复读(一个事务读到了另一个事务已提交的更新数据):
请添加图片描述

如上图,事务B在T2和T5都执行了查询工资的操作,但是两次得到的结果不一样,同一事务种的同样多次查询,每次都返回不一样的结果,这在开发中绝不允许。

幻读(一个事务读到了另一个事务已提交的新增数据):
请添加图片描述

如上图,事务B在同一个事务中执行两次统计操作之间,A事务insert了一条记录,导致得到的结果不一样,好像发生了幻觉。还有一种情况是事务B更新了表中所有记录的某一字段,之后事务A又插入了一条记录,事务B再去查询发现有一条记录没有被更新,这也是幻读。

脏读、不可重复读的区别?
脏读:指读到其他事务未提交数据.
不可重复读: 读到其他事务已提交数据(update).

不可重复读和幻读的区别?
不可重复读与幻读都是读到其他事务已提交数据,但是它们针对点不同.
不可重复读: update(更新)
幻读: delete,insert(插入/删除)

1.4 MySQL的默认RR隔离级别

为了证明RR级别并不能解决幻读问题,我们如下演示一个实例:

首先创建一张简单表test1,包含两个字段:id和salary,均为int类型。

create table test1(id int,salary int);

之后向表中插入3条记录:

insert into test1(id,salary) values(1,8800);
insert into test1(id,salary) values(2,9500);
insert into test1(id,salary) values(3,6600);

查看表中所有记录:

select * from test1;

请添加图片描述

之后我们开启两个事务A和B,具体事务流程如下:

请添加图片描述

按照之前的结论,会出现幻读现象,也就是事务AT5时间段的查询select看到事务B提交的新增数据,而查询结果却如下图所示:

请添加图片描述

和预期结果并不一致,没有出现幻读?

事实上虽然MySQL在RR隔离级别下虽然没有解决幻读问题,但是它却采用了另外一种机制即MVCC(Multiversion Concurrency Control,多版本并发控制)去解决了select普通查询下的幻读现象

实现方式为:事务开始时,第一条select语句查询结果集生成一个快照,并且这个事务结束前同样的select语句返回的都是这个快照的结果,而不是最新的查询结果,这就是MySQL在RR隔离级别对普通select语句使用的快照读

快照读和MVCC有什么关系?
MVCC是多版本并发控制,快照就是其中的一个版本。所以可以说MVCC实现了快照读,具体的实现方式涉及到MySQL的隐藏列。MySQL会给每个表自动创建三个隐藏列如下:
DB_TRX_ID事务ID,记录操作(增、删、改)该数据事务事务ID
DB_ROLL_PTR回滚指针,记录上一个版本的数据在undo log中的位置
DB_ROW_ID隐藏ID ,创建表没有合适的索引作为聚簇索引时,会用该隐藏ID创建聚簇索引
由于undo log中记录了各个版本的数据,并且通过DB_ROLL_PTR(回滚指针采用了类似链表的数据结构,即表头是当前最新的历史记录,表尾是当前最旧的历史记录)可以找到各个历史版本,并且由DB_TRX_ID决定使用哪个版本(快照)。所以相当于undo log实现了MVCC,MVCC实现了快照读。

这么看来,MySQL在RR隔离级别下利用快照读,解决了幻读的问题,事实是如此么?

接下来我们再来看一个例子:

请添加图片描述

事务AT2的时候生成快照事务BT3的时候插入一条id为5的数据,然后提交。在T5的时候事务A把id为5的salary更新成5678,按照上例,此时事务A是看不到id为5的这条数据的,所以更新也不会成功,并且在T6的时候查询,和T2时候一样,只有id=1~4的共4条数据。

请添加图片描述

但是,执行的结果却是上图所示不仅看到了id为5的记录,而且其salary还被成功的改成了5678。即使事务A成功commit之后,再次查询还是这样。

这其实是MySQLinsert、update和delete语句所使用的当前读(current read)。因为涉及到数据的修改,所以MySQL必须拿到最新的数据才能修改,所以涉及到数据的修改会用当前读。由于事务A读到了事务B已提交的新增数据,所以就产生了前文所说的幻读

那么在RR隔离级别是怎么解决幻读的呢?

是通过间隙锁(Gap Lock)来解决的。InnoDB支持行锁,并且行锁是锁住索引。而间隙锁用来锁定索引记录间隙,确保索引记录的间隙不变间隙锁是针对事务隔离级别为RR或以上级别而设的,间隙锁和行锁一起组成了Next-Key Lock。当InnoDB扫描索引记录的时候,会首先对索引记录加上行锁,再对索引记录两边的间隙加上间隙锁(Gap Lock)。加上间隙锁之后,其他事务就不能在这个间隙插入记录。这样就有效地防止了幻读的发生

默认情况下,InnoDB工作在RR的隔离级别下,并且以Next-Key Lock的方式对索引行进行加锁。当查询的索引具有唯一性(主键、唯一索引)时,Innodb存储引擎会对Next-Key Lock进行优化,将其降为行锁,仅仅锁住索引本身,而不是范围(除非锁定不存在的值)。若是普通索引,则会使用Next-Key Lock将记录和间隙一起锁定。

使用快照读的查询语句:

 select * from ...

使用当前读的语句:

select * from ... lock in share mode
select * from ... for update
insert into table ...
update table set ...
delete table where ...
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

丷江南南

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值