失眠网,内容丰富有趣,生活中的好帮手!
失眠网 > Mysql事务隔离级别及MVCC(多版本并发控制)

Mysql事务隔离级别及MVCC(多版本并发控制)

时间:2023-03-18 12:22:17

相关推荐

Mysql事务隔离级别及MVCC(多版本并发控制)

一、MySQL事务隔离级别

先注明一点:以下讨论都是在多事务并发的情境下讨论的

事务的特性(InnoDB引擎才有事务):

ACID

原子性:一个事务不可再分割,要么都执行要么都不执行

一致性:一个事务执行会使数据从一个一致状态切换到另外一个一致状态

隔离性:一个事务的执行不受其他事务的干扰

持久性:一个事务一旦提交,则会永久的改变数据库的数据.

事务隔离级别

​ 事务隔离级别指的是在处理同一个数据的多个事务中,一个事务修改数据后,其他事务何时能看到修改后的结果。数据库事务的隔离级别有4个,由低到高依次为Read uncommitted 、Read committed 、Repeatable read 、Serializable ,这四个级别可以逐个解决脏读 、不可重复读 、幻读这几类问题。脏读,幻读,不可重复读

1.脏读:脏读就是指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据。2.不可重复读:是指在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的,因此称为是不可重复读。(即不能读到相同的数据内容),例如,一个编辑人员两次读取同一文档,但在两次读取之间,作者重写了该文档。当编辑人员第二次读取文档时,文档已更改。原始读取不可重复。如果只有在作者全部完成编写后编辑人员才可以读取文档,则可以避免该问题。3.幻读:是指当事务不是独立执行时发生的一种现象,例如:第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象发生了幻觉一样。注意:不可重复读的重点是修改 :同样的条件,你读取过的数据,再次读取出来发现值不一样了幻读的重点在于新增或者删除同样的条件,第 1 次和第 2 次读出来的记录数不一样。

MySQL数据库事务隔离级别主要有四种:

1.Serializable:串行化,一个事务一个事务的执行。是最高的事务隔离级别,同时代价也花费最高,性能很低,一般很少使用,在该级别下,事务顺序执行,不仅可以避免脏读、不可重复读,还避免了幻像读。2.Repeatable read:重复读,无论其他事务是否修改并提交了数据,在这个事务中看到的数据值始终不受其他事务影响,可以避免不可重复读,但还有可能出现幻读 ;3.Read committed:读取已提交,其他事务提交了对数据的修改后,本事务就能读取到修改后的数据值,避免了脏读,但是可能会造成不可重复读;4.Read uncommitted:读取未提交,其他事务只要修改了数据,即使未提交,本事务也能看到修改后的数据值,就可能出现脏读;

MySQL数据库默认使用可重复读( Repeatable read)

各级别解决的问题如下:

具体级别对应的问题及详细过程

1.Read Uncommitted(能读到未提交的数据)

2.Read committed(解决了脏读的问题(如下图,事务二最终读取到的是a=2的正确数据),但当前会话只能读取到其他事务提交的数据,未提交的数据读不到,出现不可重复读的问题))

3.Repeatable Read(可重读)

这是MySQL的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行;但可能出现幻读现象:当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的“幻影” 行。InnoDB和Falcon存储引擎通过多版本并发控制(MVCC)机制解决了该问题。

其实对于幻读, MySQL的InnoDB引擎默认的RR级别已经通过MVCC自动帮我们解决了, 所以该级别下, 你也模拟不出幻读的场景; 退回到 RC 隔离级别的话, 又容易把幻读和不可重复读搞混淆, 具体可以参考《高性能MySQL》对 RR 隔离级别的描述, 理论上RR级别是无法解决幻读的问题, 但是由于InnoDB引擎的RR级别还使用了MVCC, 所以也就避免了幻读的出现!但是MVCC虽然解决了幻读问题, 但严格来说只是解决了部分幻读问题

解决不可重复读

出现幻读

4.serializable

在该隔离级别下,只允许一个事务在执行,其它事务必须等待这个事务执行完后才能执行,也就解决了上面的问题,但是效率太低了,没有并发,只是单纯的串行。

例:事务A操作时将整张表锁住了,当事务B尝试操作时,会被阻塞,直到事务A提交commit以后,事务B的操作才会返回结果,而且事务B等待的时间可以设置,超出时间就error。

不可重复读和幻读比较:

两者有些相似,前者针对的是update或delete,后者针对的insert。

二、MVCC

1.MVCC:

全称是Mutil-Version Concurrency Control,翻译成中文是多版本并发控制,MySQL就利用了MVCC来判断在一个事务中,哪个数据可以被读出来,哪个数据不能被读出来。

2.多版本:

在看MVCC之前,我们有必要知道另外一个知识点,数据库存储一行行数据,是分为两个部分来存储的,一个是数据行的额外信息(本篇博客不涉及),一个是真实的数据记录,MySQL会为每一行真实数据记录添加两三个隐藏的字段:

row_id:

非必须,如果表中有自定义的主键或者有Unique键,就不会添加row_id字段,如果两者都没有,MySQL会“自作主张”添加row_id字段。transaction_id:

必须,事务Id,代表这一行数据是由哪个事务id创建的。roll_pointer:

必须,回滚指针,指向这行数据的上一个版本。

在这里需要着重说明下事务id,当我们开启一个事务,并不会马上获得事务id,哪怕我们在事务中执行select语句,也是没有事务id的(事务id为0),只有执行insert/update/delete语句才能获得事务id,这一点尤为重要。其中和MVCC紧密相关的是transaction_id和roll_pointer两个字段,在开发过程中,我们无需关心,但是要研究MVCC,我们必须关心。

如果有类似这样的一行数据:

代表这行数据是由transaction_id为9的事务创建出来的,roll_pointer是空的,因为这是一条新纪录。实际上,roll_pointer并不是空的,如果真要解释,需要绕一大圈,理解成空的,问题也不大。

当我们开启事务,对这条数据进行修改,会变成这样:

有点感觉了吧,这就像一个单向链表,称之为“版本链”,最上面的数据是这个数据的最新版本,roll_pointer指向这个数据的旧版本,给人的感觉就是一行数据有多个版本,是不是符合“多版本并发控制”中的“多版本”这个概念,

那么“并发控制”又是怎么做到的呢,别急,继续往下看。

3.ReadView:

下面又要引出一个新的概念:ReadView。

对于READ UNCOMMITTED来说,可以读取到其他事务还没有提交的数据,所以直接把这个数据的最新版本读出来就可以了,对SERIALIZABLE来说,是用加锁的方式来访问记录。剩下的就是READ COMMITTED和REPEATABLE READ,这两个事务隔离级别都要保证读到的数据是其他事务已经提交的,也就是不能无脑把一行数据的最新版本给读出来了,但是这两个还是有一定的区别,最核心的问题就在于“我到底可以读取这个数据的哪个版本”。为了解决这个问题,ReadView的概念就出现了,ReadView包含四个比较重要的内容:

m_ids:表示在生成ReadView时,系统中活跃的事务id集合。min_trx_id:表示在生成ReadView时,系统中活跃的最小事务id,也就是 m_ids中的最小值。max_trx_id:表示在生成ReadView时,系统应该分配给下一个事务的id。creator_trx_id:表示生成该ReadView的事务id。

有了这个ReadView,只要按照下面的判断方式就可以解决“我到底可以读取这个数据的哪个版本”这个千古难题了:

如果被访问的版本的trx_id和ReadView中的creator_trx_id相同,就意味着当前版本就是由你“造成”的,可以读出来。如果被访问的版本的trx_id小于ReadView中的min_trx_id,表示生成该版本的事务在创建ReadView的时候,已经提交了,所以该版本可以读出来。如果被访问版本的trx_id大于或等于ReadView中的max_trx_id值,说明生成该版本的事务在当前事务生成ReadView后才开启,所以该版本不可以被读出来。如果生成被访问版本的trx_id在min_trx_id和max_trx_id之间,那就需要判断下trx_id在不在m_ids中:如果在,说明创建ReadView的时候,生成该版本的事务还是活跃的(没有被提交),该版本不可以被读出来;如果不在,说明创建ReadView的时候,生成该版本的事务已经被提交了,该版本可以被读出来。如果某个数据的最新版本不可以被读出来,就顺着roll_pointer找到该数据的上一个版本,继续做如上的判断,以此类推,如果第一个版本也不可见的话,代表该数据对当前事务完全不可见,查询结果就不包含这条记录了。

看完上面的描述,是不是觉得“云里雾里”,“不知所云”,甚至“脑阔疼,整个人都不好了”。

我们换个方法来解释,看会不会更容易理解点:

在事务启动的一瞬间(执行CURD操作),会创建出ReadView,对于一个数据版本的trx_id来说,有以下三种情况:

如果落在低水位,表示生成这个版本的事务已经提交了,或者是当前事务自己生成的,这个版本可见。如果落在高水位,表示生成这个版本的事务是未来才创建的,这个版本不可见。 如果落在中间水位,包含两种情况:

a.如果当前版本的trx_id在活跃事务列表中,代表这个版本是由还没有提交的事务生成的,这个版本不可见;

b.如果当前版本的trx_id不在活跃事务列表中,代表这个版本是由已经提交的事务生成的,这个版本可见。

上面我比较简单的解释了下ReadView,用了两种方式来说明如何判断当前数据版本是否可见,不知道各位看官是不是有了一个比较模糊的概念,有了ReadView的基本概念,我们就可以具体看下READ COMMITTED、REPEATABLE READ这两个事务隔离级别为什么读到的数据是不同的,以及上述规则是如何应用的。

READ COMMITTED——每次读取数据都会创建ReadView

假设,现在系统只有一个活跃的事务T,事务id是100,事务中修改了数据,但是还没有提交,形成的版本链是这样的:

现在A事务启动,并且执行了select语句,此时会创建出一个ReadView,m_ids是【100】,min_trx_id是100, max_trx_id是101,creator_trx_id是0。为什么m_ids只有一个,为什么creator_trx_id是0?这里再次强调下,只有在事务中执行insert/update/delete语句才能获得事务id。

那么A事务执行的select语句会读到什么数据呢?

判断最新的数据版本,name是“梦境地底王”,对应的trx_id是100,trx_id在m_ids里面,说明当前事务是活跃事务,这个数据版本是由还没有提交的事务创建的,所以这个版本不可见。顺着roll_pointer找到这个数据的上一个版本,name是“地底王”,对应的trx_id是99,而ReadView中的min_trx_id是100,trx_id<min_trx_id,代表当前数据版本是由已经提交的事务创建的,该版本可见。

所以读到的数据的name是“地底王”。

我们把事务T提交了,事务A再次执行select语句,此时,事务A再次创建出ReadView,m_ids是【】,min_trx_id是0, max_trx_id是101,creator_trx_id是0。

因为事务T已经提交了,所以没有活跃的事务。

那么事务A第二次执行select语句又会读到什么数据呢?

判断最新的数据版本,name是“梦境地底王”,对应的trx_id是100,不在m_ids里面,说明这个数据版本是由已经提交的事务创建的,该版本可见。所以读到的数据的name是“梦境地底王”。

REPEATABLE READ ——首次读取数据会创建ReadView

假设,现在系统只有一个活跃的事务T,事务id是100,事务中修改了数据,但是还没有提交,形成的版本链是这样的:

现在A事务启动,并且执行了select语句,此时会创建出一个ReadView,m_ids是【100】,min_trx_id是100, max_trx_id是101,creator_trx_id是0。

那么A事务执行的select语句会读到什么数据呢?

判断最新的数据版本,name是“梦境地底王”,对应的trx_id是100,trx_id在m_ids里面,说明当前事务是活跃事务,这个数据版本是由还没有提交的事务创建的,所以这个版本不可见。顺着roll_ponit找到这个数据的上一个版本,name是“地底王”,对应的trx_id是99,而ReadView中的min_trx_id是100,trx_id<min_trx_id,代表当前数据版本是由已经提交的事务创建的,该版本可见。

所以读到的数据的name是“地底王”。

细心的你,一定发现了,这里我就是复制粘贴,因为在REPEATABLE READ事务隔离级别下,事务A首次执行select语句创建出来的ReadView和在READ COMMITTED事务隔离级别下,事务A首次执行select语句创建出来的ReadView是一样的,所以判断流程也是一样的,所以我就偷懒了,copy走起。

随后,事务T提交了事务,由于REPEATABLE READ是首次读取数据才会创建ReadView,所以事务A再次执行select语句,不会再创建ReadView,用的还是上一次的ReadView,所以判断流程和上面也是一样的,所以读到的name还是“地底王”。

参考:/a/1190000014837747

参考:/CodeBear/p/12710670.html

如果觉得《Mysql事务隔离级别及MVCC(多版本并发控制)》对你有帮助,请点赞、收藏,并留下你的观点哦!

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。