赞
踩
主要作用:用来回滚事务。
假设我们现在开启一个事务,然后执行4个sql语句,当我们执行完两个sql后,此时还剩两个sql没执行,此时我不行执行了想回滚事务,那么就要将那两个sql修改的值修改为原先的值。此时就需要undo log日志文件了。
这个回滚日志其实记录的东西是比较简单的,比如你在缓存页种执行了一个insert语句,此时你在日志里就会记录insert语句的一些信息。如果需要回滚则执行该insert语句的逆向恢复delete操作即可。
同样的道理,update则将更新之前的值记下来,回滚时再update回去即可;delete则先存对应的delete信息,需要回滚的时候执行该delete语句的逆向操作insert操作;select不用
下面以insert一条语句为例,简单的介绍下该undo log文件是怎么存储的?
insert 语句的undo log 的类型是trx_undo_insert_rec,这个undo log里包含了以下这些东西其中主键的各列长度和值(主键可能有多个或一个),即使你没有设置主键,mysql自己内部会给你弄个row_id作为隐藏字段作为主键。
undo log日志编号,一个事务执行多条sql,就会有多个undo log日志,每个事务里的undo log日志的编号都是从0开始依次递增。
日志类型这里就是trx_undo_insert_rec。
此时如果要回滚,则直接将该undo log拿出来,然后根据该条undo log就可以知道之前是向哪个表插入的数据、主键是什么,然后直接定位到这个表和主键对应的缓存页,然后执行逆向操作将该数据删除就可以达到事务回滚的效果了。
一、多事务并发下存在的问题
我们都知道,往往业务系统是多个线程并发执行多个事务的,那么对于数据库而言,他就会有多个事务同时执行,这个时候就可能有多个事务去同时更新和查询同一条数据,这个时候就会暴露很多问题:
1、脏写
有两个事务A、B同时更新一条数据,事务A先将其更新为A值,事务B紧跟着把他更新为B值。
此时事务A回滚了,而事务A的undo log记录原先的值是NULL,此时因为回滚则将这个值修改为原先的NULL值。
然而这对于事务B来说他以为还是他提交的B值。这就是脏写。
总结一句话:脏写就是自己事务更新的值后来变没了。
2、脏读
事务A先去更新一行数据为A然后还没提交,此时事务B去读取到的值为A,然后A回滚,B再次去读变成了NULL。两次读取数据不同
因其他事务回滚,导致两次读取的数据不同
其实无论脏写还是脏读,都是因为一个事务去更新或者查询了另一个还没提交的事务更新过的数据。因为另一个事务还没提交,所以随时可能返回回滚,那么必然会导致你更新的数据没了,或者之前查询到的数据没了。
总结:你的事务写或者查的是别的事务还没提交时更新的数据,所以别人的事务随时会反悔回滚,从而导致你有问题。
那么怎么解决脏读脏写?
此时只要事务不读写其他事务没提交的数据不就可以了。(这也是mysql的读已提交的事务级别)3、不可重复读
上面读已提交的事务级别解决了脏数据的情况,然而此时还是存在不可重复读的问题。
不可重复读:事务A一开始读取一行数据为A值然后未提交,此时事务B将A值变为B值然后提交事务,此时A再去读取发现值变为B值了。
这样就导致了事务A两次读取到的值不同。这就是不可重复读。
(不可重复读是建立在读已提交(解决脏读)的基础上的,即只要事务B提交了此时事务A才能读取到事务B更新的值。)
其实要说没问题也是没问题,因为事务A读取的是事务B已经提交的数据。要说有问题也是有问题,事务A可能需要在整个事务过程中该值保持不变
他希望这行数据的值是可重复读的。
解决方法:将数据库的事务隔离级别调至可重复读级别
4、幻读
举个例子:事务A查询一批数据:select * from table where id>:10 。一开始查出了10条数据,然后此时其他事务往该表中插入一些数据。当事务A再去查询这个sql时发现查出了12条数据。就像产生幻觉一样,怎么突然多出了两条数据?
注意,幻读特指你查询到了之前查询没看到的数据,此时说明你是幻读了。
解决方案:将数据库的事务隔离级别提至串行化。(当然某些情况下mysql的不可重复读级别能解决幻读现象)
总结:上面的脏写、脏读、不可重复读、幻读的这些问题,其产生的本质就是数据库的多事务并发问题,为了解决多事务并发问题,数据库才设计了事务隔离机制、MVCC多版本隔离机制、锁机制等一整套机制来解决。下面我们将会对这些机制进行分析,理解mysql是怎么去解决这些问题的。
二、sql标准中的4个事务隔离级别
为了解决上面的几个问题,在sql标准中(注意不是mysql的)规定了事务的几种隔离级别用来解决这些问题。
mysql的事务隔离级别和sql标准的有一些不同。这里先说sql标准的隔离级别,在sql标准中规定了4种事务隔离级别:
read uncommitted(读未提交),read commited(读已提交),repeatable read(可重复读),serializable(串行化)读未提交:解决了脏写。保证了两个事务在没提交的时候不可能去同时更新同一行数据。(产生脏读就是事务A写值为A后未提交,然后事务B更新为B提交,此时A后悔进行回滚,导致事务B以为还是B值。该级别限制了在事务A未提交的时候事务B不能去更新这个值,从而解决脏写问题)(该级别只解决了脏写问题,其他问题并未解决)
读已提交(RC):解决脏读问题,而出现脏读的现象是读取了不同事务中未提交的数据。而读已提交限制只能读已提交的数据,从而解决了脏读的问题。在该级别下不会发生脏读脏写的现象。该级别也简称为RC。
可重复读(RR):解决不可重复读的问题。
串行化:解决幻读问题。这种级别就是不允许多个事务并发执行,多个事务只能串行起来执行,例如先执行A提交后再执行事务B。当然这样效率会低很多。
其中最常用的是RC、RR级别(默认是RR级别,可有些场景需要将级别设置为RC级别)
注意⚠️:不可重复读重点在于update和delete,而幻读的重点在于insert。
三、mysql中的4个事务隔离级别
mysql也是支持四个隔离级别,默认是RR级别,但是要注意的是mysql的RR级别是可以避免幻读的(使用MVCC多版本并发控制隔离机制)。(而sql标准的RR级别是不能避免幻读的)
mysql设置事务隔离级别命令: https://www.cnblogs.com/ryelqy/p/11434120.html
但一般是不需要修改隔离级别的,默认RR即可。
再开发业务系统时,其实也可以根据spring的@Transactional注解来指定其数据库操作用哪个级别的,spring的事务级别默认也是RR级别的。
可以通过该注解里的isolation参数设置为RC级别。例如@Transactional(isolation=Isolation.READ_COMMITTED)来设置为RC级别。
Isolation.READ_UNCOMMITTED——读未提交;Isolation.READ_COMMITTED——RC级别;Isolation.DEFAULT——默认RR级别;Isolation.SERIALIZABLE——串行化
那么下面就开始来讲解MVCC机制是怎么解决幻读的?
四、undo log版本链
在说MVCC机制前需要先说下一些前置的知识。
简单来说,我们每条数据都有两个隐藏字段。一个是trx_id,一个是roll_pointer。其中trx_id是最近一次更新这条数据的事务id,roll_point指向更新这个事务之前的undo log。(这两个隐藏字段就是数据页里真实数据部分的隐藏字段)
举个例子:假设现在有一个事务A(id=50),插入一条数据则会在该数据的隐藏字段中设置事务id为50,roll_pointer指向一个空的undo log。并且
该事务记录一个undo log记录。
此时跑来一个事务id为58的事务B来更新数据为B,此时事务id改为58,roll_pointer改为指向之前事务A的undo log(这个undo log记录着你之前更新的那条数据的值)。
依次类推,此时事务C也进来了.........。这样就形成了一条重要的undo log版本链
五、基于undo log版本链实现的ReadView机制
简单点讲就是你执行一个事务的时候,会给你生成一个ReadView,里面有4个东西比较关键:
1、m_ids,记录在mysql中哪些事务还没提交2、min_trx_id,m_ids中的最小值
3、max_tri_id,就是mysql下一个要生成的事务id,就是最大id(我猜mysql生成的事务id是呈增加趋势的)
4、creator_trx_id,当前事务id。
有了这个
ReadView
,这样在访问某条记录时,只需要按照下边的步骤判断记录的某个版本是否可见:
情况(1)如果被访问版本的
trx_id
属性值与ReadView
中的creator_trx_id
值相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问。情况(2)如果被访问版本的
trx_id
属性值小于ReadView
中的min_trx_id
值,表明生成该版本的事务在当前事务生成ReadView
前已经提交,所以该版本可以被当前事务访问。情况(3)如果被访问版本的
trx_id
属性值大于或等于ReadView
中的max_trx_id
值,表明生成该版本的事务在当前事务生成ReadView
后才开启,所以该版本不可以被当前事务访问。情况(4)如果被访问版本的
trx_id
属性值在ReadView
的min_trx_id
和max_trx_id
之间,那就需要判断一下trx_id
属性值是不是在m_ids
列表中,如果在,说明创建ReadView
时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建ReadView
时生成该版本的事务已经被提交,该版本可以被访问。那么此时这套ReadView机制是怎么运行的?
1)、假设现在有一行数据,事务id是32,值是初始值。此时两个事务A(id=45)、B(id=59)并发过来执行。此时B是来更新这行值的,A来查询这行值。
2)事务A开启ReadView,此时ReadView中的值如下图(max_trx_id是60)。此时事务A查询该行数据,先判断这行数据的trx_id是否小于ReadView的最小的事务id(45),此时32小于45,则证明修改这行数据的事务早就提交了。所以此时允许事务A查询这个数据。
(符合情况(2))
3)、事务B开始动手了,将这行数据修改为值B。然后trx_id设置为自己的id(59),同时将roll_pointer指向修改之前生成的undo log,接着事务B就提交了。
4)、此时事务A再进行查询这行数据(注意再次查询还是用到前面旧的ReadView),发现此时记录的trx_id是59,而此时因为事务B虽然已经提交,但是事务A的ReadView没有做实时更新,并且这个59是大于ReadView的min_trx_id最小事务id(45)且小于max_trx_id(60),此时验证下m_ids中是否有此id,发现有存在(即使事务B提交了,但是由于用的是旧的ReadView,此时59旧还在m_ids中),所以不能使用
(不符合情况(4))
那么此时就会限制事务45不能直接查询这行数据,需要顺着这条数据的roll_pointer找到undo log链中最近的且符合上面可读取的规则的
后面如果事务A自己去修改了这行数据,将trx_id改为自己的45,此时事务A再去读取时可以读取自己修改的值的。
(符合情况(1))
接着事务C(78)进来了,此时A还没提交,而事务B提交了,事务C先进行读取(此时C的ReadView有45和78),发现此时数据的事务id是45,处于min_trx_id(45)和max_drx_id(78)之间,但此时m_ids有45,则不能进行读取,会沿着undo log找到59这个值。读完后事务C更新这个值为C,此时trx_id为78,然后提交了.(不符合情况(4))
接着事务A再去读一次(此时还是旧的ReadView,里面m_ids还是只有45、59,没有78),发现78大于min_trx_id(45),且78大于max_trx_id(60),此时不能进行读取,则沿着undo log链找到trx_id为45的值。
(不符合情况(3))
总结:1、自己开启的事务,不能读取比自己早开启的事务但没提交修改的值
2、不能读取比自己晚开启的事务修改的值(如果晚开启的事务提交了且m_ids没有则可以读取)
通过ReadView(用这些数据来判断undo log多版本链的事务id是发生在本身事务的前还是后,事务提交了没等情况)和undo log多版本链来控制实现,这样就只能读到事务开启前已经固定的数据了
注意这里的ReadView是事务开启时就生成的,在事务期间其他事务提交或开启了是不会进行实时更新的,此时的ReadView是旧的数据。
MVCC机制就是基于ReadView机制+undo log多版本链条实现的
一、RC
这个RC隔离级别就是读已提交,就是只要别的事务修改数据且提交了,你就可以读到别人修改的数据。这个级别是会产生不可重复读、幻读的问题的。
那这里的逻辑是不是就和上面的那套ReadVIew机制不太一样了(ReadView机制是不能读前事务修改但没提交,后事务的任何修改数据的)。
这里mysql没有去改变这个ReadView机制,而是让每次查询的时候重新生成一个ReadView。(这样就能实时的去更新ReadView的值了)
举例:有两个事(A60,B70)务同时进来操作同一行数。AB都开启事务生成自己的ReadView。
1)、此时事务B修改值为B,此时trx_id为70,然后将事务B提交了。
2)、此时A进行查询,就再次生成一个新的ReadView(之前开启事务时也会生成一个),此时新的ReadView里面m_ids没有70(事务B已提交),此时即使该行数据的trx_id为70大于本身的60,但因为m_ids没有70,则认为这是在事务开启前提交好的数据,所以A就能读取B提交了的数据了。
(符合情况(4))
3)、读取晚开启、早开启的事务修改且提交后的数据都是这样类推出来即可。
二、RR
其实看到上面的ReadView机制,是不是就觉得解决了不可重复读、幻读的问题了。
你按着上面的ReadView机制+undo log多版本链条进行推导,会发现事务A的多次读取都不会发生脏写、脏读、不可重复读、幻读的情况
在
MySQL
中,READ COMMITTED
和REPEATABLE READ
隔离级别的的一个非常大的区别就是它们生成ReadView的时机不同。
READ COMMITTED —— 每次读取数据前都生成一个ReadView
REPEATABLE READ —— 在第一次读取数据时生成一个ReadView
下一篇开始讲锁机制等内容
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。