mysql的mvcc和readview

1、业务系统并发对数据库执行事务

在业务系统里会开启事务来执行增删改操作的,业务系统执行一个事务,每个事务里可能是一个或多个增删改语句。

业务系统很可能是基于多线程并发的对MySQL数据库去执行多个事务的,那么每个事务里面的多个SQL语句都是如何执行的呢?

这里就有很多问题了:

1)、多个事务并发执行的时候,可能会 同时对缓存页里的一行数据进行更 新,这个冲突怎么处理?是否要加锁?

2)、可能 有的事务在对一行数据做更新,有的事务在查询这行数据 ,这里的冲突怎么处理?

解决多个事务并发运行的时候,同时写和同时读写的一些并发冲突的处理机制,包括了MySQL事务的隔离级别、MVCC多版本隔离、锁机制,等等。

2、脏写、脏读

对于我们的业务系统去访问数据库而言,他往往都是多个线程并发执行多个事务的,对于数据库而言,他会有多个事务同时执行,可能这多个事务还会同时更新和查询同一条数据。

每个事务都会执行各种增删改查的语句,把磁盘上的数据页加载到buffer pool的缓存页里来,然后更新缓存页,记录redo log和undo log,最终提交事务或者是回滚事务 ,多个事务会并发干上述一系列事情。

多个事务对缓存页里同一条数据同时更新或查询,产生哪些问题?

事务A 将旧值 null 更新为 A值, 又回滚为 null。

事务B 将A值更新为B值读取为B 和null值。

脏写:这里写是指更新。即一个事务更新了 别的事务更新后未提交,然后回滚的值,而该事务查到的是更新后和未更新前的旧值。

脏写的话是说有两个事务,事务A和事务B同时在更新一条数据,事务A先把他更新为 A值,事务B紧接着就把他更新为B值,此时事务B是后更新那行数据的值,所以此时那行数据的值是B值,此时事务A更新之后会记录一条undo log日志,事务A的undo log日志大概就是:更新之前这行数据的值为NULL,主键为XX 。

此时事务B更新完了数据的值为B,结果此时事务A突然回滚了,那么就会用他的undo log日志 去回滚。 此时事务A一回滚,直接就会把那行数据的值更新回之前的NULL值!所以此时事务A回滚了,可能看起来这行数据的值就是NULL了。然后事务B一看,为什么我更新的B值没了?就因为你事务A反悔了就把数据值回滚成NULL了,搞的更新的B值也没了!

所以对于事务 B 看到的场景,就是 自己明明更新了,结果值却没了 , 这就是脏写 !

脏读:这里读是指查询。即一个事务查询了 别的事务更新后未提交,然后回滚的值,而该事务查到的是更新后和回滚后的值。

事务A 将旧值 null 更新为 A值,又回滚为 null。

事务B 读取为A值和null值。

总结:无论是脏写还是脏读,针对未提交的事务,都是因为一个事务去更新或者查询了另外一个还没提交的事务更新过的数据。因为另外一个事务还没提交,所以他随时可能会反悔会回滚,那么必然导致你更新的数据就没了,或者你之前查询到的数据没了,这就是脏写和脏读两种场景。

3、不可重复读

针对提交的事务: 一个事务多次查询一条数据,结果每次读到的值都不一样 ,这个过程中可能别的事务会修改并提交这条数据的值,结果导致人家每次查到的值都不一样,都查到了提交事务修改过的值,这就是所谓的不可重复读。

4、幻读

针对提交的事务,幻读是查询到之前没看到过的数据。

幻读 :指的就是 你一个事务用一样的SQL多次查询,结果每次查询都会发现查到了一些之前没看到过的数 据 。注意,幻读特指的是你查询到了之前查询没看到过的数据!此时就说你是幻读了。

一个事务A,先发送一条SQL语句,里面有一个条件要查询一批数据出来,比如“select * from table where id>10”,类似这种SQL 。然后呢,他一开始查询出来了10条数据。

接着 事务B往表里插入了几条数据,而且事务B还提交了 ,此时多了几行数据出来。

接着事务A此时第三次查询,再次按照之前的一模一样的条件执行“select * from table where id>10”这条SQL语句,由于其他事务插入了几条数据,导致这次他查询出来了12条数据。

此时事务A开始怀疑自己的双眼了,为什么一模一样的SQL语句,第一次查询是10条数据,第二次查询是12条数据?难道刚才出现了幻觉?导致我刚才幻读了?

5、事务隔离机制

脏写、脏读、不可重复读、幻读,都是因为业务系统会多线程并发执行,每个线程可能都会开启一个事务,每个事务都会执行增删改查操作。然后数据库会并发执行多个事务,多个事务可能会并发的对缓存页里的同一批数据进行增删改查操作,于是这个并发增删改查同一批数据的问题,可能就会导致我们说的脏写、脏读、不可重复读、幻读这些问题。

所以这些问题的本质,都是数据库的多事务并发问题,那么为了解决多事务并发问题,数据库才设计了 事务隔离机制、MVCC多版本隔离机制、锁机制 ,用一整套机制来解决多事务并发问题。

SQL标准中就规定了事务的几种隔离级别,用来解决这些问题。注意:SQL标准事务隔离级别并不是MySQL的事务隔离级别。这4种级别包括了:read uncommitted(读未提交),read committed(读已提交)RC, repeatable read(可重复读)RR,serializable(串行化),用来避免:脏写、脏读、不可重复读、幻读。

第一个read uncommitted隔离级别,是不允许发生脏写的。不可能两个事务在没提交的情况下去更新同一行数据的值,但是在这种隔离级别下,可能发生脏读,不可重复读,幻读。

第二个是read committed隔离级别,事务没提交的情况下修改的值,你是绝对读不到的。这个级别下,不会发生脏写和脏读。

第三个是REPEATABLE READ隔离级别,就是可重复读级别你事务一旦开始,多次查询一个值,会一直读到同一个值!

第四个是serializable级别,这种级别,根本就不允许你多个事务并发执行,只能串行起来执行,先执行事务A提交,然后执行事务B提交,接着执行事务C提交,所以此时你根本不可能有幻读的问题,因为事务压根儿都不并发执行。这级别一般除非脑子坏了,否则更不可能设置了,因为多个事务串行,那数据库恨不能一秒并发就只有几十了,性能会极差的。

大家一定要记住非常骚气的RC和RR级别,因为平时比较常见的就是用RC和RR两种隔离级别。

6、MySQL如何支持4种隔离级别?Spring事务注解如何设置?

MySQL默认设置的事务隔离级别,都是RR级别的 ,而且 MySQL 的 RR 级别是可以避免幻读发生的。

MySQL里执行的事务,默认情况下不会发生脏写、脏读、不可重复读和幻读的问题,事务的执行都是并行的,大家互相不会影响,我不会读到你没提交事务修改的值,即使你修改了值还提交了,我也不会读到的,即使你插入了一行值还提交了,我也不会读到的,总之,事务之间互相都完全不影响!

要修改MySQL的默认事务隔离级别,命令如下可以设置级别为不同的level,level值可以是RR,RC,READ UNCOMMITTED和SERIALIZABLE几种级别。

SET [GLOBAL|SESSION] TRANSACTION ISOLATION LEVEL level;

假设你在开发业务系统的时候,比如用Spring 里的 @Transactional 注解来做事务。

在@Transactional 注解里是有一个 isolation 参数的,里面是可以设置事务隔离级别的,具体的设置方式。如下: @Transactional(isolation=Isolation.DEFAULT) ,然后默认的就是 DEFAULT 值,这个就是 MySQL 默认支持什么隔离级别就是什么隔离级别。

7、undo log版本链是个什么东西?

理解MVCC机制的前奏:undo log版本链是个什么东西?

每条数据其实都有两个隐藏字段,一个是trx_id,一个是roll_pointer ,这个 trx_id 就是最近一次更新这条数据的事务id , roll_pointer 就是指向你了你更新这个事务之前生成的 undo log 。

例子:

现在假设有一个事务A(id=50),插入了一条数据,那么此时这条数据的隐藏字段以及指向的undo log如下图所示,插入的这条数据的值是值A,因为事务A的id是50,所以这条数据 的txr_id就是50,roll_pointer指向一个空的undo log,因为之前这条数据是没有的。

·接着假设有一个事务B跑来修改了一下这条数据,把值改成了值B,事务B的id是58,那么此时更新之前会生成一个undo log记录之前的值,然后会让roll_pointer指向这个实际的undo log回滚日志

事务B修改了值为值B,此时表里的那行数据的值就是值B了,那行数据的txr_id就是事务B的id,也就是58,roll_pointer指向了undo log,这个undo log就记录你更新之前 的那条数据的值。 所以大家看到roll_pointer指向的那个undo log,里面的值是值A,txr_id是50,因为undo log里记录的 这个值是事务A插入的,所以这个undo log的txr_id就是50,我还特意把表里的那行数据和undo log的 颜色弄成不一样的,以示区分。

接着假设事务C又来修改了一下这个值为值C,他的事务id是69,此时会把数据行里的txr_id改成69,然后生成一条undo log,记录之前事务B修改的那个值。

看到,数据行里的值变成了值C,txr_id是事务C的id,也就是69,然后roll_pointer 指向了本次修改之前生成的undo log,也就是记录了事务B修改的那个值,包括事务B的id,同时事务B 修改的那个undo log还串联了最早事务A插入的那个undo log。

多个事务串行执行的时候,每个人修改了一行数据,都会更新隐藏字段txr_id和roll_pointer,同时之前多个数据快照对应的undo log,会通过roll_pinter指针串联起来,形成一个重要版本链!

8、ReadView机制

基于undo log多版本链条实现的ReadView机制,到底是什么?

ReadView:当你执行一个事务的时候,就给你生成一个ReadView,里面比较关键的东西有4个

一个是m_ids,就是此时有哪些事务在MySQL里执行还没提交的;即未提交事务id。

一个是min_trx_id,就是m_ids里最小的值;最小的事务id。

一个是max_trx_id,这是说mysql下一个要生成的事务id;最大的事务id下一个值。

一个是creator_trx_id,就是你这个事务的id;当前事务id值。

通过undo log多版本链条,加上你开启事务时候生产的一个ReadView,然后再有一个查询的时候根据ReadView进行判断的机制,你就知道你应该读取哪个版本的数据。

而且他可以 保证你只能读到你事务开启前,别的提交事务更新的值和你自己事务更新的值 。

读不到: 假如说是 你事务开启之前,就有别的事务正在运行,然后你事务开启之后 ,别的事务更新了值,你是绝对读不到的!或者是你事务开启之后,比你晚开启的事务更新了值,你也是读不到的 ! 通过这套机制就可以实现多个事务并发执行时候的数据隔离。

多个事务并发运行的时候,如果同时要读写一批数据,此时读和 写时间的关系是如何协调的

9、Read Committed隔离级别是如何基于ReadView机制实现?

RC隔离级别,事务运行期间,只要别的事务修改数据还提交了,你就是可以读到人家修改的数据的,所以是会发生不可重复读的问题,包括幻读的问题,都会有的。

ReadView机制,是基于undo log版本链条实现的一套读视图机制,事务生成一个ReadView然后如果是你事务自己更新的数据,自己是可以读到的,或者是在你生成ReadView之前提交的事务修改的值,也是可以读取到的。但是如果是你生成ReadView的时候,就已经活跃的事务,在你生成ReadView之后修改了数据,接着提交了,此时你是读不到的,或者是你生成ReadView以后再开启的事务修改了数据,还提交了,此时也是读不到的。

当你一个事务设置他处于RC隔离级别的时候,他是每次发起查询,都重新生成一个ReadView!

他的关键点在于每次查询都生成新的ReadView,那么如果在你这次查询之前,有事务修改了数据还提交了,你这次查询生成的ReadView里,那个m_ids列表当然不包含这个已经提交的事务了,既然不包含已经提交的事务了,那么当然可以读到人家修改过的值了。

这就是基于ReadView实现RC隔离级别的原理,希望大家好好仔细去体会,实际上,基于undo log多版本链条以及ReadView机制实现的多事务并发执行的RC隔离级别、RR隔离级别,就是数据库的MVCC多版本并发控制机制。

10、最牛的RR隔离级别,是如何基于ReadView机制实现的

在MySQL 中让多个事务并发运行的时候能够互相隔离,避免同时读写一条数据的时候有影响,是 依托undo log版本链条和ReadView机制来实现 的。

基于ReadView机制可以实现RC隔离级别, 即你 每次查询的时候都生成一个ReadView,这样的话,只要在你这次查询之前有别的事务提交了,那么别的事务更新的数据,你是可以看 到的。

RR级别下,你这个事务读一条数据,无论读多少次,都是一个值,别的事务修改数据之后哪怕提交了,你也是看不到人家修改的值的,这就避免了不可重复读的问题。同时如果别的事务插入了一些新的数据,你也是读不到的,这样你就可以避免幻读的问题。

不管别的事务如何修改数据,事务A的ReadView始终是不变的,他基于这个ReadView始终看到的值是一样的!

事务A根本不会发生幻读,他根据条件范围查询的时候,每次读到的数据都是一样的,不会读到人家插入进去的数据,这都是依托ReadView机制实现的!

11、梳理一下数据库的多事务并发运行的隔离机制

梳理一下MySQL中的多事务并发运行的隔离原理,其实这套隔离原理,说白了就是MVCC机制,也就是multi-version concurrent control,就是多版本并发控制机制,专门控制多个事务并发运行的时候,互相之间会如何影响。

数据库的隔离机制以及底层的原理后MySQL实现MVCC机制的时候,是基于undo log多版本链条+ReadView机制来做的,默认的RR隔离级别,就是基于这套机制来实现的,依托这套机制实现了RR级别,除了避免脏写、脏读、不可重复 读,还能避免幻读问题。因此一般来说我们都用默认的RR隔离级别就好了。

12、多个事务更新同一行数据时,是如何加锁避免脏写的?

锁机制:解决的就是多个事务同时更新一行数据,此时必须要有一个加锁的机制

当有多个事务同时并发更新一行数据的时候,不就是会有脏写的问题吗? 而脏写是绝对不允许的,那么这个脏写是靠什么防止的呢?

靠锁机制,依靠锁机制让多个事务更新一行数据的时候串行化,避免同时更新一行数据,

Mysql锁原理:

在MySQL里,假设有一行数据摆在那儿不动,此时有一个事务来了要更新这行数据,这个时候他会先琢磨一下,看看这行数据此时有没有人加锁? 一看没人加锁,此时这个事务就会创建一个锁,里面包含了自己的trx_id和等待状态,然后把锁跟这行数据关联在一起。

同时更新一行数据必须把他所在的数据页从磁盘文件里读取到缓存页里来才能更新的,所以说,此 时这行数据和关联的锁数据结构,都是在内存里的 。

大家注意看上面的那个图,因为事务A给那行数据加了锁,所以此时就可以说那行数据已经被加锁了。那么既然被加锁了,此时就不能再让别人访问了!有另外一个事务B过来了,这个事务B就也想更新那行数据,此时就会检查一下,当前这行数据有没有别人加锁 然而发现事务A抢先给这行数据加锁了,这怎么办呢?事务B这个时候一想,我也加个锁,然后等着排队不就得了,这个时候事务B也会生成一个锁数据结构,里面有他的trx_id,还有自己的等待状态,但是他因为是在排队等待,所以他的等待状态就是true了,意思是我在等着呢,如下图。

接着事务A这个时候更新完了数据,就会把自己的锁给释放掉了。锁一旦释放了,他就会去找,此时还 有没有别人也对这行数据加锁了呢?他会发现事务B也加锁了于是这个时候,就会把事务B的锁里的等待状态修改为false,然后唤醒事务B继续执行,此时事务B就获取到锁了,如下图

13、对MySQL共享锁和独占锁到底是什么?

其实多个事务同时更新一行数据,此时都会加锁,然后都会排队等待,必须一个事务执行完毕了,提交了,释放了锁,才能唤醒别的事务继续执行。

多个事务运行的时候,他们加的是什么锁呢?其实是X锁,也就是Exclude独占锁,当有一个事务加了独占锁之后,此时其他事务再要更新这行数据,都是要加独占锁的,但是只能生成独占锁在后面等待。

当有人在更新数据的时候,其他的事务可以读取这行数据吗?默认情况下需要加锁吗?

答案是:不用。因为默认情况下,有人在更新数据的时候,然后你要去读取这行数据,直接默认就是开启mvcc机制的。也就是说,此时对一行数据的读和写两个操作默认是不会加锁互斥的,因为MySQL设计mvcc机制就是为了解决这个问题,避免频繁加锁互斥。

此时你读取数据,完全可以根据你的ReadView,去在undo log版本链条里找一个你能读取的版本,完全不用去顾虑别人在不在更新。就算你真的等他更新完毕了还提交了,基于mvcc机制你也读不到他更新的值啊!因为ReadView机制是不允许的,所以你默认情况下的读,完全不需要加锁,不需要去care其他事务的更新加锁问题,直接基于mvcc机制读某个快照就可以了。

那么假设万一要是你在执行查询操作的时候,就是想要加锁呢?

那也是ok 的, MySQL 首先 支持一种共享锁,就是S锁,这个共享锁的语法如下:select * from table lock in share mode ,你在一个查询语句后面加上 lock in share mode ,意思就是查询的时候对一行数据加共享锁。

如果此时有别的事务在更新这行数据,已经加了独占锁了,此时你的共享锁能加吗? 当然不行了,共享锁和独占锁是互斥的!此时你这个查询就只能等着了。

如果你先加了共享锁,然后别人来更新要加独占锁行吗?当然不行了,此时共享锁和独占锁是互斥的,他只能等待。

如果你在加共享锁的时候,别人也加共享锁呢?此时是可以的,你们俩都是可以加共享锁的,共享锁和共享锁是不会互斥的。

总结

更新数据的时候必然加独占锁,独占锁和独占锁是互斥的,此时别人不能更新;但是此时你要查询,默认是不加锁的,走mvcc机制读快照版本,但是你查询是可以手动加共享锁的,共享锁和独占锁是互斥的,但是共享锁和共享锁是不互斥的。

一般开发业务系统的时候,其实你查询主动加共享锁,这种情况较为少见,数据库的行锁是实用功能,但是一般不会在数据库层面做复杂的手动加锁操作,反而会用基于redis/zookeeper的分布式锁来控制业务系统的锁逻辑。

查询操作还能加互斥锁方法是:select * from table for update。意思就是,我查出来数据以后还要更新,此时我加独占锁了,其他闲杂人等,都不要更新这个数据了。一旦你查询的时候加了独占锁,此时在你事务提交之前,任何人都不能更新数据了,只能你在本事务里更新数据,等你提交了,别人再更新数据。

14、在数据库里,哪些操作会导致在表级别加锁呢

在多个事务并发更新数据的时候,都是要在行级别加独占锁的,这就是行锁,独占锁都是互斥的,所以不可能发生脏写问题,一个事务提交了才会释放自己的独占锁,唤醒下一个事务执行。

如果你此时 去读取别的事务在更新的数据 ,有两种可能:

第一种可能是基于mvcc机制进行事务隔离,读取快照版本,这是比较常见的;

第二种可能是查询的同时基于特殊语法去加独占锁或者共享锁。

如果你查询的时候加独占锁,那么跟其他更新数据的事务加的独占锁都是互斥的;如果你查询的时候加共享锁,那么跟其他查询加的共享锁是不互斥的,但是跟其他事务更新数据就加的独占锁是互斥的,跟其他查询加的独占锁也是互斥的。

当然一般我个人从多年研发经验而言, 不是太建议在数据库粒度去通过行锁实现复杂的业务锁机制,而更加建议通过redis、zookeeper来用分布式锁实现复杂业务下的锁机制 ,更为合适一些。

为什么呢?因为如果你把分布式系统里的复杂业务的一些锁机制依托数据库查询的时候,在SQL语句里加共享锁或者独占锁,会导致这个加锁逻辑隐藏在SQL语句里,在你的Java业务系统层面其实是非常的不好维护的,所以一般是不建议这么做的。

比较正常的情况而言,其实还是 多个事务并发运行更新一条数据,默认加独占锁互斥,同时其他事务读 取基于mvcc机制进行快照版本读,实现事务隔离 。

15、表锁和行锁互相之间的关系以及互斥规则是什么呢?

表级锁

在数据库里,你不光可以通过查询中的特殊语法加行锁,比如lock in share mode、for update等等,还可以通过一些方式在表级别去加锁。

DDL语句和增删改操作,确实是互斥的。

表锁其实是InnoDB存储引擎的概念,InnoDB存储引擎提供了自己的表级锁。

这个MySQL 的表锁,其实是极为鸡肋的一个东西,几乎一般很少会用到, 表锁分为两种,一种就是表锁,一种是表级的意向锁。

表锁,这个表锁,可以用如下语法来加:

LOCK TABLES xxx READ:这是加表级共享锁

LOCK TABLES xxx WRITE:这是加表级独占锁

一般几乎没人会用这两个语法去加表锁,这不是纯属没事儿找事儿么,所以才说表锁特别的鸡肋。

一般来讲,都是对同一行数据的更新操作加的行级独占锁是互斥,跟读操作都是不互斥的,读操作默认都是走mvcc机制读快照版本的!

————————————————

版权声明:本文为CSDN博主「CodingALife」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

原文链接:https://blog.csdn.net/mingyuli/article/details/120371391

https://blog.csdn.net/mingyuli/article/details/120371391?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522167776661416800225566499%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=167776661416800225566499&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduend~default-1-120371391-null-null.142^v73^wechat_v2,201^v4^add_ask,239^v2^insert_chatgpt&utm_term=readview&spm=1018.2226.3001.4187

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值