MySql 事务

一.定义和特性

事务就是一组DML语句组成,这些语句在逻辑上存在相关性,这一组DML语句要么全部成功,要么全部失败,是一个整体。MySQL提供一种机制,保证我们达到这样的效果。事务还规定不同的客户端看到的数据是不相同的。

要了解事务最重要的是了解他的四个特性:

  • 原子性:一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
  • 一致性:在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。
  • 隔离性 :数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务 并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交( Read uncommitted )、读提交( read committed )、可重复读( repeatable read )和串行化 ( Serializable )
  • 持久性:事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失

在四个特性中,隔离性是最难理解的我们之后重点讲解。

为什么会出现事务 ?

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

在 MySQL 中只有使用了 Innodb 数据库引擎的数据库或表才支持事务, MyISAM 不支持。

二.事务提交方式

事务的提交方式常见的有两种: 自动提交,手动提交。

查看事务提交方式:

这里表示自动提交打开,当我们在命令行每输入一行命令都会被当成一个事务提交。

用 SET 来改变 MySQL 的自动提交模式

使用set autocommit=0; 即可让自动提交关闭,事务需要手动提交。

三.事务常见操作方式

1.开启事务

start transaction或者begin。

2.创建保存点

savepoint save1;创建保存后可以之后使事务会滚到对应的保存点,就像游戏里的存档一样。

3.回滚到保存点

rollback to save1;rollback回滚到最开始。

4.提交事务

commit。

5.例子如下:

先创建一个简单表格

对事务进行操作,我们先开启事务,建立第一个保存点,再插入一条记录,建立第二个保存点,再插入事务,查看数据显示俩条记录,再不断回滚,查看记录。

上面就是手动提交事务的基本流程。

四.事务隔离级别

表中的数据可能会被多个事务并发访问,隔离级别就是允许事务受到其他事务不同程度的干扰。分别有四个隔离级别:

  • 读未提交【Read Uncommitted】: 在该隔离级别,所有的事务都可以看到其他事务没有提交的执行结果。(实际生产中不可能使用这种隔离级别的),但是相当于没有任何隔离性,也会有很多 并发问题,如脏读,幻读,不可重复读等
  • 读提交【Read Committed】 :该隔离级别是大多数数据库的默认的隔离级别(不是 MySQL 默 认的)。它满足了隔离的简单定义:一个事务只能看到其他的已经提交的事务所做的改变。这种隔离 级别会引起不可重复读,即一个事务执行时,如果多次 select, 可能得到不同的结果。
  • 可重复读【Repeatable Read】: 这是 MySQL 默认的隔离级别,它确保同一个事务,在执行 中,多次读取操作数据时,会看到同样的数据行。但是会有幻读问题。
  • 串行化【Serializable】: 这是事务的最高隔离级别,它通过强制事务排序,使之不可能相互冲突, 从而解决了幻读的问题。它在每个读的数据行上面加上共享锁,但是可能会导致超时和锁竞争 (这种隔离级别太极端,实际生产基本不使用)

光看定义比较难理解,我们结合具体的操作理解。

1.查看与设置隔离性

隔离级别在作用范围可以可以分成:全局隔离级别和当前(会话)隔离级别,一个是对全部会话生效,一个是当前会话生效,就非常类似于全局变量和局部变量。

查看隔离级别:

设置隔离级别

2.读未提交

顾名思义,就是在在该隔离条件下,不同事务可以读互相读未提交的数据

我们同时开启事务1,2。这时,在事务1中插入一条记录后不提交,直接在事务2中可以读取到该记录,这就是读未提交。这时会引发脏读,一个事务在执行中,读到另一个执行中事务的更新(或其他操作)但是未commit的数据,这种现象叫做脏读。

3.读提交

顾名思义,就是在在该隔离条件下,不同事务可以读互相读已经提交的数据 。

我们同时开启事务1,2。这时,在事务1中插入一条记录后不提交,在事务2中不可以读取到该记录,当我们在事务1中commit,才能读取到记录。

此时还在当前事务2中,并未commit,那么就造成了,同一个事务内,同样的读取,在不同的时间段 (依旧还在事务操作中!),读取到了不同的值,这种现象叫做不可重复读(non reapeatable read)。也就是说读提交会造成不可重复读。

4.可重复读

这解决了上面的问题,保证同一个事务读到同一个值,这里有一个坑。

同时开启事务1,2。在事务1插入一条记录, 不提交在事务2中读取,读取不到,提交后再读取依旧读取不到。这里会有一个坑。当我们调整第3步和第4步顺序,可以在事务2中读取到插入的记录。也就是开启事务1,2后,先插入数据后立马提交,这时在事务2中进行第一次查询是能读取到俩条记录的 。 这里的原理我们后面讲解。

5.串行化

这个比较容易理解,它在每个读的数据行上面加上共享锁,通过强制事务排序,使之不可能相互冲突。如下:

事务1不提交,事务2查看就会阻塞。 每个事务必须按顺序执行。

五.多版本并发控制( MVCC )

多版本并发控制( MVCC )是一种用来解决读-写冲突的无锁并发控制,可以用来实现隔离性。

理解 MVCC 需要知道三个前提知识:

  • 3个记录隐藏字段
  • undo 日志
  • Read View

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 产生一个聚簇索引
  • 补充:实际还有一个删除flag隐藏字段, 既记录被更新或删除并不代表真的删除,而是删除flag变了

每个事务都有id作为标识,第一个字段用来记录创建这条记录/最后一次修改该记录的事物ID,第二个字段指向更新前的历史版本,第三个字段表示隐藏主键,如果数据表没有主键,就会用隐藏主键充当索引(对索引不理解的可以看上篇博客哦)。

2.uodo log日志

undo log 被称为回滚日志,用于记录数据被修改前的信息 , 作用包含两个 : 提供回滚(保证事务的原子性) 和 MVCC(多版本并发控制) 。MySQL 将来是以服务进程的方式,在内存中运行。我们之前所讲的所有机制:索引,事务,隔离性,日志等,都是在内存中完成的,即在 MySQL 内部的相关缓冲区中,保存相关数据,完成各种判断操作。然后在合适的时候,将相关数据刷新到磁盘当中的。 所以,我们这里理解undo log,简单理解成,就是 MySQL 中的一段内存缓冲区,用来保存日志数据的就行。

3.模拟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,对student表中记录进行修改(update):将age(28)改成age(38)。

  • 事务11,因为也要修改,所以要先给该记录(李四)加行锁.
  • 修改前,现将改行记录拷贝到undo log中,所以,undo log中就又有了一行副本数据。此时,新的 副本,我们采用头插方式,插入undo log。
  • 现在修改原始记录中的age,改成 38。并且修改原始记录的隐藏字段 DB_TRX_ID 为当前 事务11 的 ID。而原始记录的回滚指针 DB_ROLL_PTR 列,里面写入undo log中副本数据的地址,从而指向副本记录,既表示我的上一个版本就是它。
  • 事务11提交,释放锁。

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

上面是以更新(`upadte`)主讲的,如果是`delete`呢?一样的,别忘了,删数据不是清空,而是设置flag 为删除即可。也可以形成版本。

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

那么`select`呢? 首先,`select`不会对数据做任何修改,所以,为`select`维护多版本,没有意义。不过,此时有个问题, 就是: select读取,是读取最新的版本呢?还是读取历史版本?

select读取分为俩种:

  • 当前读:读取最新的记录,就是当前读。增删改,都叫做当前读,select也有可能当前读,比如:select lock in share mode(共享锁), select for update (这个好理解,我们后面不讨论)
  • 快照读:读取历史版本(一般而言),就叫做快照读。(这个我们后面重点讨论)

我们可以看到,在多个事务同时删改查的时候,都是当前读,是要加锁的。那同时有select过来,如果也要读取最新版(当前读),那么也就需要加锁,这就是串行化。 但如果是快照读,读取历史版本的话,是不受加锁限制的。也就是可以并行执行!换言之,提高了效率,即 MVCC的意义所在。

那么,是什么决定了,select是当前读,还是快照读呢?隔离级别!多个事务在执行中,CURD操作是会交织在一起的。为了保证事务的"有先有后",应该让不同的事务看到它该看到的内容,这就是所谓的隔离性与隔离级别要解决的问题。

那么,如何保证,不同的事务,看到不同的内容呢?也就是如何如何实现隔离级别?

4.Read View

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

我们可以把Read View 看成下面的简化结构:

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

当我们为一个事务创建视图时按按时间可以分成下面三类:

  • 1.已经提交的事务, 或者就是自己这个事务,应该能看到这些事务的数据。
  • 2.和创建视图的事务一起在并发执行的事务,这些事务的数据是不能看到的。
  • 3.后来的事务,这些事务的数据是不能看到的

总结起来就是,之前的事务能看到,同级别和之后的不能看到。 这样也符合逻辑。

这里我门重点要注意是创建Read view 时他的活跃跃事务id并不一定是连续的。如11,12,13,14,15号事务运行,在快照读前,12,14提交了,那么m_id=11,13,15。此时12,14是在已提交的范围内。

举个例子,假设当前有条记录:

事务操作:

事务4:修改name(张三) 变成name(李四)

当事务2对某行数据执行了 快照读 ,数据库为该行数据生成一个 Read View 读视图

此时版本链是:

只有事务4修改过该行记录,并在事务2执行快照读前,就提交了事务。

我们的事务2在快照读该行记录的时候,就会自上而下拿行记录的 DB_TRX_ID 去跟 up_limit_id,low_limit_id和活跃事务ID列表(trx_list) 进行比较,判断当前事务2能看到该记录的版本。 过程如下:

  • DB_TRX_ID(4)< up_limit_id(1) ? 不小于,下一步(如果小于说明是已提交)
  • DB_TRX_ID(4)>= low_limit_id(5) ? 不大于,下一步(如果大于说明是未提交)
  • m_ids.contains(DB_TRX_ID) ? 不包含,说明,事务4不在当前的活跃事务中。(不在就说明已提交,在就说明未提交)

故,事务4的更改,应该看到。 所以事务2能读到的最新数据记录是事务4所提交的版本,而事务4提交的版本也是全局角度上最新的版本。

5.RR 与 RC的本质区别

这里我们就可以解决上面一个遗留问题:

在可重复读级别下,按上面的顺序,事务2俩次读取都不能读到事务1插入的记录的,我们可能会疑惑,事务2第二次读取前,事务1不是已经提交了吗,按照read view的理解不是应该能读取到吗?读提交按照上面的顺序第二次能读到又是因为上面?

这时因为Read View生成时机的不同,从而造成RC,RR级别下快照读的结果的不同。

在RR级别下的某个事务的对某条记录的第一次快照读会创建一个快照及Read View, 将当前系统活跃的其他事务记录起来 此后在调用快照读的时候,还是使用的是同一个Read View,所以只要当前事务在其他事务提交更新之前使用过快照读,那么之后的快照读使用的都是同一个Read View,所以对之后的修改不可见。

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

总之在RC隔离级别下,是每个快照读都会生成并获取最新的Read View;而在RR隔离级别下,则是同一个事务中的第一个快照读才会创建Read View, 之后的快照读获取的都是同一个Read View。 正是RC每次快照读,都会形成Read View,所以,RC才会有不可重复读问题。

这样我们就能理解为什么在可重复读的情况下,俩次读取都不能读到插入的记录.因为事务1在第一次读取时未提交,俩次读取都是基于同一个read view,自然不行 。当3,4步的顺序调换时,俩次读取都能读到,是因为事务1在第一次读取时就提交了。

MySql事务就讲解到这里,求点赞了,啊啊啊啊啊啊啊啊啊啊啊啊。

相关推荐
姚远Oracle ACE1 小时前
解读Oracle AWR报告:Global Cache and Enqueue Services - Workload Characteristics
数据库·oracle
流星白龙1 小时前
【Qt】7.信号和槽_connect函数用法(2)
java·数据库·qt
Zzz 小生3 小时前
Claude Code学习笔记(四)-助你快速搭建首个Python项目
大数据·数据库·elasticsearch
nongcunqq6 小时前
abap 操作 excel
java·数据库·excel
rain bye bye7 小时前
calibre LVS 跑不起来 就将setup 的LVS Option connect下的 connect all nets by name 打开。
服务器·数据库·lvs
冻咸鱼7 小时前
MySQL的配置
mysql·配置
阿里云大数据AI技术8 小时前
云栖实录|MaxCompute全新升级:AI时代的原生数据仓库
大数据·数据库·云原生
不剪发的Tony老师9 小时前
Valentina Studio:一款跨平台的数据库管理工具
数据库·sql
weixin_307779139 小时前
在 Microsoft Azure 上部署 ClickHouse 数据仓库:托管服务与自行部署的全面指南
开发语言·数据库·数据仓库·云计算·azure
六元七角八分9 小时前
pom.xml
xml·数据库