MySQL中对于事务完整的超详细介绍

一、事务基本介绍

1.什么是事务

事务本质上就是一组DML(数据操纵语言,即增删查改)语句组成,这一组DML语句要么一次性成功,要么全部失败,是一个整体。事务:sql语句=1:n

其实从语义角度出发,事务并不属于MySQL,应该属于数据上层业务。比如我们购买了两张火车票,在数据库内表中的数据我们需要减2,在我们手机的客户端上,便需要产生两张火车票,此时我们可以把减2和加2两个操作总和称为事务,但在MySQL的语法角度来说,这实际上只是两句DML语句

又或者说事务就是要做的或所做的事情,主要用于处理操作量大,复杂度高的数据。假设一种场景:你毕业了,学校的教务系统后台 MySQL 中,不在需要你的数据,要删除你的所有信息(一般不会:) ), 那么要删除你的基本信息(姓名,电话,籍贯等)的同时,也删除和你有关的其他信息,比如:你的各科成绩,你在校表现,甚至你在论坛发过的文章等。这样,就需要多条 MySQL 语句构成,那么所有这些操作合起来,就构成了一个事务。

对于一个数据库来说,肯定会有很多用户同时对他进行操作,此时会出现多个用户对同一个数据同时进行操作,甚至,因为事务由多条 SQL 构成,那么,也会存在执行到一半出错或者不想再执行的情况,那么已经执行的怎么办呢?

2.为什么出现事务

事务被 MySQL 编写者设计出来,本质是为了当应用程序访问数据库的时候,事务能够简化我们的编程模型,不需要我们去考虑各种各样的潜在错误和并发问题.可以想一下当我们使用事务时,要么提交,要么回滚,我们不会去考虑网络异常了,服务器宕机了,同时更改一个数据怎么办对吧?因此事务本质上是为了应用层服务的.而不是伴随着数据库系统天生就有的

3.事务四大特性

为了保护用户使用数据库时,事务的完整性,所以事务需要具备四大特性

原子性,隔离性,持久性->一致性(同时也需要程序员一起保证),三大特性一起为了保证一致性

原子性

一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。换种说法,我们需要保证事务操作的完整

一致性

在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。

隔离性

数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交( Read uncommitted )、读提交( read committed )、可重复读( repeatable read )和串行化( Serializable )
我们在下面对隔离性进行详细解释

持久性

事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。数据能够永久保存

上面四个属性,可以简称为 ACID

4.事务的隔离级别

四种级别

事务的隔离级别分为四种:
1.读未提交: 在该隔离级别,所有的事务都可以看到其他事务没有提交的执行结果。(实际生产中不可能使用这种隔离级别的),但是相当于没有任何隔离性,也会有很多并发问题,如脏读,幻读,不可重复读等,我们上面为了做实验方便,用的就是这个隔离性。简单理解,我们可以理解为,只要其他的事务修改了表中数据,无论提交与否,当前操作的事务就能够查询的数据的改变
2.读提交:该隔离级别是大多数数据库的默认的隔离级别(不是 MySQL 默认的)。它满足了隔离的简单定义:一个事务只能看到其他的已经提交的事务所做的改变。这种隔离级别会引起不可重复读,即一个事务执行时,如果多次 select, 可能得到不同的结果。简单理解,我们可以理解为,只要其他的事务修改了表中数据并且提交后,当前操作的事务再次使用select时,就能够查询的数据的改变
3.可重复读:这是 MySQL 默认的隔离级别,它确保同一个事务,在执行中,多次读取操作数据时,会看到同样的数据行。但是会有幻读问题。可以理解为在同一事务当中,多次是读取时,表内数据也不会改变
4.串行化:这是事务的最高隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决了幻读的问题。它在每个读的数据行上面加上共享锁,但是可能会导致超时和锁竞争(这种隔离级别太极端,
实际生产基本不使用)
其实对于上面的四种级别,我们可以进行高度概括,mysql需要对数据很强的安全性和运行时的高效率

⭐上述的隔离级别当中,我们可能会产生疑惑,如果我们想实时看到新数据,或者想看见的是老版本的表内容,,又或者说我们能否按照时间顺序对表内容进行查看呢?
这些问题我们等下解答

mysql在事务当中,默认的隔离级别是可重复读

不同隔离级别所具有的问题

请添加图片描述
脏读:脏读就是指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据。

幻读:是指当事务不是独立执行时发生的一种现象,例如第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象发生了幻觉一样。

不可重复读:是指在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的,因此称为是不可重复读。

我们可以通过下面的展示隔离级别的操作进行理解

二、操作演示

1.自动提交和手动提交

在MySQL中,事务的提交方式默认是自动提交,即每一条语句都被认为是一个事务且自动提交
而手动提交,则需要输入 start transaction命令(或者begin),表示开始事务操作,commit命令则表示事务提交
可以看见mysql当中是默认打开的

我们发现自动提交是默认打开的
其实对于begin,commit来说,自动提交是否打开都不影响,只要使用了begin语句,只有使用commit提交后才成为一个事务。但如果自动提交开着,单独一条dml语句在MySQL中就是一个事务

请添加图片描述

此时我们便可以证明,在MySQL中,一条单独的DML语句便是一个事务

2.回滚操作

首先我们先将
请添加图片描述
然后下面是进行回滚操作
请添加图片描述

从上面的操作中,我们可以发现,我们可以使用回滚操作,使数据可以回到某一操作点,这便体现了一致性和原子性
输入rollback命令,可以回到begin,开始的状态
最后输入commit命令后,便说明当前事务结束

下面演示,当未commit,客户端崩溃,MySQL自动会回滚(隔离级别设置为读未提交)
这里体现原子性

请添加图片描述
同样的道理,如果我们commit提交后,即使客户端崩溃,MySQL数据不会在受影响,已经持久化,这里体现一致性

3.查看与设置隔离性

– 查看
mysql> SELECT @@global.tx_isolation; --查看全局隔级别
±----------------------+
| @@global.tx_isolation |
±----------------------+
| REPEATABLE-READ |
±----------------------+
1 row in set, 1 warning (0.00 sec)
mysql> SELECT @@session.tx_isolation; --查看会话(当前)全局隔级别
±-----------------------+
| @@session.tx_isolation |
±-----------------------+
| REPEATABLE-READ |
±-----------------------+
1 row in set, 1 warning (0.00 sec)
mysql> SELECT @@tx_isolation; --默认同上
±----------------+
| @@tx_isolation |
±----------------+
| REPEATABLE-READ |
±----------------+
1 row in set, 1 warning (0.00 sec)

设置当前会话隔离性,另起一个会话,看不多,只影响当前会话
set session transaction isolation level serializable(串行化)设置全局隔离性,另起一个会话,会被影响set global transaction isolation level READ UNCOMMITTED(读未提交)

4.读未提交

我们设置为读未提交状态
在这里插入图片描述

请添加图片描述
一个事务在执行中,读到另一个执行中事务的更新(或其他操作)但是未commit的数据,这种现象叫做脏读(dirty read)

5.读提交

同样,先设置未读提交状态
请添加图片描述
此时还在当前事务中,并未commit,那么就造成了,同一个事务内,同样的读取,在不同的时间段(依旧还在事务操作中!),读取到了不同的值,这种现象叫做不可重复读(non reapeatable read)
不过,这个是问题吗??我们平常查询数据时,我们自身没对数据进行操作时,数据会更改吗

6.可重复读

同样,先设置可重复读状态

请添加图片描述
在终端B中,事务无论什么时候进行查找,看到的结果都是一致的,这叫做可重复读

但是!!
多次查看,发现终端A在对应事务中insert的数据,在终端B的事务周期中,也没有什么影响,也符合可重复的特点。但是,一般的数据库在可重复读情况的时候,无法屏蔽其他事务insert的数据(为什么?因为隔离性实现是对数据加锁完成的,而insert待插入的数据因为并不存在,那么一般加锁无法屏蔽这类问题),会造成虽然大部分内容是可重复读的,但是insert的数据在可重复读情况被读取出来,导致多次查找时,会多查找出来新的记录,就如同产生了幻觉。这种现象,叫做幻读(phantom read)。很明显,MySQL在RR级别的时候,是解决了幻读问题

7.串行化

对所有操作进行加锁,但效率过低,几乎完全不会被使用

请添加图片描述
我们可以发现,两个读取不会串行化,共享锁,但修改不可以
只有当终端Bcommit后,修改才能成功

三、对隔离性的详细解读

我们要深入了解隔离性,即MVCC(多版本并发控制),首先我们需要格外知道三种知识,3个记录隐藏字段,undo 日志,Read View

为事务分配单向增长的事务ID,为每个修改保存一个版本,版本与事务ID关联,读操作只读该事务开始前的数据库的快照。 所以 MVCC 可以为数据库解决以下问题在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读
写的性能同时还可以解决脏读,幻读,不可重复读等事务隔离问题,但不能解决更新丢失问题

1.3个记录隐藏字段

DB_TRX_ID :6 byte,最近修改( 修改/插入 )事务ID,记录创建这条记录/最后一次修改该记录的事务ID
DB_ROLL_PTR : 7 byte,回滚指针,指向这条记录的上一个版本(简单理解成,指向历史版本就行,这些数据一般在 undo log 中)
DB_ROW_ID : 6 byte,隐含的自增ID(隐藏主键),如果数据表没有主键, InnoDB 会自动以 DB_ROW_ID 产生一个聚簇索引
无法理解的小伙伴可以往下看到MVCC^ _ ^

假如我们向表中存入一个叫张三,28岁的数据,则会创建一个表(包括隐藏字段)
请添加图片描述

2.undo 日志

MySQL 将来是以服务进程的方式,在内存中运行。我们之前所讲的所有机制:索引,事务,隔离性,日志等,都是在内存中完成的,即在 MySQL 内部的相关缓冲区中,保存相关数据,完成各种判断操作。然后在合适的时候,将相关数据刷新到磁盘当中的。

这里我们可以简单理解为我们进行的各种操作(包括数据),都会存储在MySQL的缓冲区中,在一定时间存入到磁盘内

模拟MVCC

现在有一个事务10(仅仅为了好区分),对student表中记录进行修改(update):将name(张三)改成name(李四)

事务10,因为要修改,所以要先给该记录加行锁。
修改前,现将改行记录拷贝到undo log中,所以,undo log中就有了一行副本数据。(原理就是写时拷贝)所以现在 MySQL 中有两行同样的记录。现在修改原始记录中的name,改成 ‘李四’。并且修改原始记录的隐藏字段 DB_TRX_ID 为当前 事务10 的ID, 我们默认从 10 开始,之后递增。而原始记录的回滚指针 DB_ROLL_PTR 列,里面写入undo log中副本数据的地址,从而指向副本记录,既表示我的上一个版本就是它。
事务10提交,释放锁。
请添加图片描述
这时我们有事务11,要将表内的age(28)改成age(38)
修改前,现将改行记录拷贝到undo log中,所以,undo log中就又有了一行副本数据。此时,新的副本,我们采用头插方式,插入undo log。现在修改原始记录中的age,改成 38。并且修改原始记录的隐藏字段 DB_TRX_ID 为当前 事务11 的ID。而原始记
录的回滚指针 DB_ROLL_PTR 列,里面写入undo log中副本数据的地址,从而指向副本记录,既表示我的上一个版本就是它。
事务11提交,释放锁。

请添加图片描述
上述三个表格,就可以称为版本,在不同的时间段的操作,会产生多版本
当有新操作产生时,当前的表会进行复制产生副本,然后对副本进行修改,再把表存入undo日志中,并把副本成为新表。

这样,我们就有了一个基于链表记录的历史版本链。所谓的回滚,无非就是用历史数据,覆盖当前数据。
上面的一个一个版本,我们可以称之为一个一个的快照。

此时我们可以更容易的理解那三个隐藏字段
DB_TRX_ID :因为我们每个事务都有一个自动增加的ID,这个便是存放事务ID的
DB_ROLL_PTR : 指向前一个版本的内存,即前一个表
DB_ROW_ID :当前表的主键

我们是insert呢?因为insert是插入,也就是之前没有数据,那么insert也就没有历史版本。但是一般为了回滚操作,insert的数据也是要被放入undo log中,如果当前事务commit了,那么这个undo log 的历史insert记录就可以被清空了。
总结一下,也就是我们可以理解成,updatedelete可以形成版本链,

3.Read View

Read View就是事务进行 快照读 操作的时候生产的 读视图 (Read View),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID(当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以最新的事务,ID值越大)
Read View 在 MySQL 源码中,就是一个类,本质是用来进行可见性判断的。 即当我们某个事务执行快照读的时候,对该记录创建一个 Read View 读视图,把它比作条件,用来判断当前事务能够看到哪个版本的数据,既可能是当前最新的数据,也有可能是该行记录的 undo log 里面的某个版本的数据。

这时,我们知道了每一个快照读都会产生一个视图,那么此时MySQL是不是就需要对齐进行管理,那么此时需要先描述,再组织,此时是有一个struct结构体

class ReadView {
// 省略...
private:
/** 高水位,大于等于这个ID的事务均不可见*/
trx_id_t m_low_limit_id
/** 低水位:小于这个ID的事务均可见 */
trx_id_t m_up_limit_id;
/** 创建该 Read View 的事务ID*/
trx_id_t m_creator_trx_id;
/** 创建视图时的活跃事务id列表*/
ids_t m_ids;
/** 配合purge,标识该视图不需要小于m_low_limit_no的UNDO LOG,
* 如果其他视图也不需要,则可以删除小于m_low_limit_no的UNDO LOG*/
trx_id_t m_low_limit_no;
/** 标记视图是否被关闭*/
bool m_closed;
// 省略...
};

m_ids:一张列表,用来维护Read View生成时刻,系统正活跃的事务ID
up_limit_id:记录m_ids列表中事务ID最小的ID(没有写错)
low_limit_id:ReadView生成时刻系统尚未分配的下一个事务ID,也就是目前已出现过的事务ID的最大值+1(也没有写错)
creator_trx_id:创建该ReadView的事务ID

此时,我们就有了一个基于链表记录的历史版本链,即链表

⭐⭐重点!!
此时我们明白了,我们在每一次产生快照读后产生了一个读视图,这里我们可以通过m_ids,up_limit_id,low_limit_id,通过和DB_TRX_ID比较的方式,比如产生了一个事务6,视图内容为up_limit_id为5,而DB_TRX_ID为4,且m_ids没有4,这里DB_TRX_ID小于up_limit_id,说明事务4已经完成,当前事务6可以看到事务4完成后的内容

同理当DB_TRX_ID大于low_limit_id,则说明当前事务一定看不见DB_TRX_ID事务的操作,需要通过当前表中的回滚指针找到自己对应的那个版本

如果DB_TRX_ID存在于m_ids中时,说明当前的操作是自己改变的,那自己的操作自己当然能够看见,不然的话,m_ids中的事务和当前事务同时都在活跃,当前事务当然看不见另外在活跃的事务

值得注意的是:快照表在进行对比时,是先从当前最新的版本开始对比,如果不匹配则通过回滚指针寻找之前的版本,直到符合为止

由于快照表的产生,使得我们每一个用户都可以不相关的对表进行操作,这里便是可以让人明确的感受到隔离性的存在(主要是读和写,读写同步是一个数据库中存在最多的现象),同样这些也是为了我们事务的一致性

4.RR 与 RC的本质区别

正是Read View生成时机的不同,从而造成RC,RR级别下快照读的结果的不同

1.在RR级别下的某个事务的对某条记录的第一次快照读会创建一个快照及Read View, 将当前系统活跃的其他事务记录起来

2.此后在调用快照读的时候,还是使用的是同一个Read View,所以只要当前事务在其他事务提交更新之前使用过快照读,那么之后的快照读使用的都是同一个Read View,所以对之后的修改不可见;

3.即RR级别下,快照读生成Read View时,Read View会记录此时所有其他活动事务的快照,这些事务的修改对于当前事务都是不可见的。而早于Read View创建的事务所做的修改均是可见

4.而在RC级别下的,事务中,每次快照读都会新生成一个快照和Read View, 这就是我们在RC级别下的事务中可以看到别的事务提交的更新的原因

5.总之在RC隔离级别下,是每个快照读都会生成并获取最新的Read View;而在RR隔离级别下,则是同一个事务中的第一个快照读才会创建Read View, 之后的快照读获取的都是同一个Read View。

正是RC每次快照读,都会形成Read View,所以,RC才会有不可重复读问题

事务中快照读的结果是非常依赖该事务首次出现快照读的地方,即某个事务中首次出现快照读,决定该事务后续快照读结果的能力

感谢各位看到这里,希望大家收获满满噢^ _ ^

  • 10
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 7
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值