自学内容网 自学内容网

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事务就讲解到这里,求点赞了,啊啊啊啊啊啊啊啊啊啊啊啊。 


原文地址:https://blog.csdn.net/2301_76293625/article/details/142706121

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!