MySQL MVCC

1.隔离级别

1.1.理论

1.1.1.序列化(SERIALIZABLE)

如果隔离级别为序列化,则用户之间通过一个接一个顺序地执行当前的事务,这种隔离级别提供了事务之间最大限度的隔离;

1.1.2.可重复读(REPEATABLE READ,MySQL默认的隔离级别)

在可重复读在这一隔离级别上,事务不会被看成是一个序列.不过,当前正在执行事务的变化仍然不能被外部看到,也就是说,如果用户在另外一个事务中执行同条SELECT语句数次,结果总是相同的.(因为正在执行的事务所产生的数据变化不能被外部看到);

1.1.3.读已提交(READ COMMITTED)

READ COMMITTED隔离级别的安全性比REPEATABLE READ隔离级别的安全性要差.处于READ COMMITTED级别的事务可以看到其他事务对数据的修改.也就是说,在事务处理期间,如果其他事务修改了相应的表,那么同一个事务的多个 SELECT语句可能返回不同的结果;

1.1.4.读未提交(READ UNCOMMITTED)

READ UNCOMMITTED提供了事务之间最小限度的隔离.除了容易产生虚幻的读操作和不能重复的读操作外,处于这个隔离级别的事务可以读到其他事务还没有提交的数据,如果这个事务使用其他事务未提交的变化作为计算的基础,然后那些未提交的变化被它们的父事务撤销,这就导致了大量的数据变化;

1.2.实践

1.2.1.查看隔离级别

1>.MySQL8.0之前,通过如下命令查看全局隔离级别和当前session的隔离级别:

SELECT @@GLOBAL.tx_isolation, @@tx_isolation;

在这里插入图片描述

2>.MySQL8.0开始,通过如下命令查看MySQL默认的隔离级别以及当前session隔离级别:

SELECT @@GLOBAL.transaction_isolation, @@transaction_isolation;

在这里插入图片描述

3>.修改当前session隔离级别

SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

在这里插入图片描述

注意:只是修改了当前session的隔离级别,其他session的隔离级别还是默认的!

1.2.2.读未提交
1.2.2.0.准备测试数据

读未提交是最小限度的隔离级别,这种隔离级别中存在脏读,不可重复读以及幻读等问题;
在这里插入图片描述

1.2.2.1.脏读

一个事务读到另一个事务还没有提交的数据,称之为脏读;

1>.首先打开两个SQL操作窗口,假设分别为A和B,在A窗口中输入如下几条SQL (注意:输入完成后不用执行);

START TRANSACTION;
UPDATE account set balance=balance+100 where name='zhangsan';
UPDATE account set balance=balance-100 where name='lisi';
COMMIT;

2>.在B窗口执行如下SQL,修改默认的事务隔离级别为READ UNCOMMITTED,如下:

SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

3>.在B窗口中输入如下SQL,输入完成后,首先执行第一行开启事务(注意:只需要执行一行即可):

START TRANSACTION;
SELECT * from account;
COMMIT;

4>.接下来执行A窗口中的前两条SQL,即开启事务,给zhangsan这个账户添加100元(事务未提交):

5>. 进入到B窗口,执行B窗口的第二条查询SQL(SELECT * from user;),结果如下:
在这里插入图片描述

可以看到,A窗口中的事务,虽然还未提交,但是B窗口中已经可以查询到数据的相关变化了,这就是脏读的问题;

1.2.2.2.不可重复读

不可重复读是指一个事务先后读取同一条记录,但是两次读取的数据不同,称之为不可重复读;

1>.首先打开两个查询窗口A和B,并且将B窗口的数据库事务隔离级别设置为READ UNCOMMITTED,再将数据恢复到原始状态:

SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

2>.在B窗口中输入如下SQL,然后只执行前两条SQL开启事务并查询zhangsan的账户:

START TRANSACTION;
SELECT * from account where name='zhangsan';
COMMIT;

在这里插入图片描述

3>.在A窗口中执行如下SQL,给zhangsan这个账户添加100块钱,如下:

START TRANSACTION;
UPDATE account set balance=balance+100 where name='zhangsan';
COMMIT;

4>.再次回到B窗口,执行B窗口的第二条SQL查看zhangsan的账户,结果如下:
在这里插入图片描述

可以看到,此时zhangsan的账户已经发生了变化,即前后两次查看zhangsan账户,结果不一致,这就是不可重复读;

脏读和不可重复读的区别:

脏读是看到了其他事务未提交的数据,而不可重复读是看到了其他事务已经提交的数据(由于当前SQL也是在事务中,因此有可能并不想看到其他事务已经提交的数据);

1.2.2.3.幻读

幻读和不可重复读非常像,看名字就是产生幻觉了;

1>.首先打开两个查询窗口A和B,并且将B窗口的数据库事务隔离级别设置为READ UNCOMMITTED,再将数据恢复到原始状态:

SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

2>.在A窗口中输入如下SQL:

START TRANSACTION;
insert into account(id,name,balance) values(3,'wangwu',1000);
COMMIT;

3>.在B窗口输入如下SQL:

START TRANSACTION;
SELECT * from account;
delete from account where name='wangwu';
COMMIT;

4>.执行步骤:

①.执行B窗口的前两行SQL,开启一个事务,同时查询数据库中的数据,此时可以查询到原始的两个用户;

②.执行A窗口的前两行,向数据库中插入一个名为wangwu的用户,注意不用提交事务;

③.执行B窗口的第二行,由于脏读的问题,此时可以查询到wangwu的用户;

④.执行B窗口的第三行,去删除name为wangwu的记录,这个时候删除就会出现问题,虽然可以在B窗口中查询到wangwu,但是这条记录还没有提交,是因为脏读的原因才看到,所以无法删除(一直等待).此时就产生了幻觉,明明可以看到wangwu这条数据,但是却无法删除,这就是幻读;

1.2.3.读已提交

和读未提交相比,读已提交这种隔离级别主要解决了脏读的问题,对于不可重复读和幻读则未解决;

将事务的隔离级别改为"READ COMMITTED"之后,重复上面关于脏读案例的测试,发现已经不存在脏读的问题了;再重复上面关于不可重复读案例的测试,发现不可重复读的问题依然存在;

1.2.3.1.幻读

1>.打开两个窗口A和B,将B窗口的隔离级别改为READ COMMITTED,再将数据恢复到原始状态:

SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

2>.在A窗口输入如下SQL:

START TRANSACTION;
insert into account(id,name,balance) values(3,'wangwu',1000);
COMMIT;

3>.在B窗口输入如下SQL:

START TRANSACTION;
SELECT * from account;
insert into account(id,name,balance) values(3,'wangwu',1000);
COMMIT;

4>.执行步骤:

①.执行B窗口的前两行SQL,开启事务并查询数据,此时可以查到原始的两个用户;

②.执行A窗口的前两行SQL,插入一条记录,但是并不提交事务;

③.执行B窗口的第二行SQL,由于现在已经没有了脏读的问题,所以此时查不到在A窗口添加的数据;

④.执行B窗口的第三行SQL,由于name字段唯一,因此这里会出现无法插入的情况(一直等待).此时就产生幻觉了,明明没有wangwu这个用户,却无法插入wangwu的数据,这就是幻读;

1.2.4.可重复读(InnoDB引擎默认的数据库事务隔离级别)

和读已提交相比,可重复读进一步解决了不可重复读的问题,但是幻读则未解决;

1>.可重复读中关于幻读的测试和上一小节基本一致,不同的是第二步中执行完插入SQL之后需要提交事务;

2>.由于可重复读已经解决了不可重复读的问题,因此第二步即使提交了事务,第三步也查不到已经提交的数据,第四步继续插入仍然不成功;

1.2.5.序列化

1>.序列化提供了事务之间最大限度的隔离,在这种隔离级别中,事务一个接一个顺序的执行,不会发生脏读,不可重复读以及幻象读的问题,最安全;

2>.如果设置当前事务的隔离级别为序列化,那么此时开启其他事务时就会阻塞,必须等待当前事务提交了,其他事务才能开启成功,因此前面的脏读,不可重复读以及幻象读的问题这里都不会发生;

1.3.总结

总的来说,隔离级别和脏读,不可重复读以及幻象读的对应关系如下:
在这里插入图片描述

性能关系图如下:
在这里插入图片描述

2.快照读和当前读

2.1.快照读

1>.快照读(SnapShot Read)是一种一致性不加锁的读,是InnoDB存储引擎并发如此之高的核心原因之一;

2>.在默认的可重复读的隔离级别下,事务开启的时候,就会针对当前库生成一个快照,快照读读取到的数据要么就是生成快照时的数据,要么就是当前事务自身插入/修改过的数据;

3>.日常所用的不加锁的查询,包括上一小节中涉及到的所有查询,都属于快照读;

2.2.当前读

与快照读相对应的就是当前读,当前读就是读取最新的数据,而不是历史数据,换言之,在可重复读的隔离级别下,如果使用了当前读,也可以读到其他事务已经提交的数据;

2.2.1.案例

1>.MySQL事务开启两个会话窗口A和B;

2>.首先在A窗口中开启事务并查询id为1的记录:

BEGIN;
SELECT * FROM account WHERE id = 1;

在这里插入图片描述

3>.在B窗口中对id为1的数据进行修改:

UPDATE account SET balance = 1000 WHERE id = 1;

注意: B会话中不要开启事务或者开启了事务要及时提交,否则update语句会占用一把排它锁导致之后在A会话中用锁时发生阻塞;

4>.回到A窗口继续做查询操作:

SELECT * FROM account WHERE id = 1;

在这里插入图片描述

SELECT * FROM account WHERE id = 1  FOR UPDATE;

在这里插入图片描述

SELECT * FROM account WHERE id = 1 LOCK in SHARE MODE;

在这里插入图片描述

可以看到,A会话中第一个查询是快照读,读取到的是当前事务开启时的数据状态,后面两个查询则是当前读,读取到了当前最新的数据,即B会话中修改后的数据;

3.undo log日志

1>.数据库事务有回滚的能力, 既然能够回滚,那么就必须要在数据改变之前先把旧的数据记录下来,作为将来回滚的依据,那么这个记录就是undo log日志;

2>.当我们要添加一条记录的时候,就把添加的数据id记录到undo log中,将来回滚的时候就据此把(添加的)数据删除;当我们要删除或者修改数据的时候,就把原数据记录到undo log中,将来据此恢复数据.查询操作因为不涉及到回滚操作,所以就不需要记录到undo log中;

3>.redo log日志和undo log日志区别:

1>.redo log: 记录的是物理级别上的页修改操作,比如页号,偏移量,写入的数据,主要是为了保证数据的可靠性(持久化);

2>.undo log: 记录的是逻辑操作日志,比如对某一行数据进行了insert操作,那么undo log就记录一条与之相反的delete操作.主要用于事务的回滚和一致性非锁定读;

4.行格式

1>.行格式就是InnoDB存储引擎在保存每一行数据的时候,究竟是以什么样的格式来保存这行数据;

2>.数据库中的行格式有好几种,例如COMPACT、REDUNDANT、DYNAMIC、COMPRESSED等,不过无论是哪种行格式,都绕不开以下几个隐藏的数据列:
在这里插入图片描述

上图中的列1,列2,列3…列N,就是我们数据库中表的列,保存着我们正常的数据,除了这些保存数据的列之外,还有三列额外加进来的数据,这也是我们需要重点关注的DB_ROW_ID、DB_TRX_ID、DB_ROLL_PTR三列;

①.DB_ROW_ID: 该列占用6个字节,是一个行ID,用来唯一标识一行数据.如果用户在创建表的时候没有设置主键,那么系统会根据该列建立主键索引;

②.DB_TRX_ID: 该列占用6个字节,是一个事务ID.在InnoDB存储引擎中,当我们要开启一个事务的时候,会向InnoDB的事务系统申请一个事务id,这个事务id是一个严格递增且唯一的数字,当前数据行是被哪个事务修改的,就会把对应的事务id记录在当前行中;

③.DB_ROLL_PTR: 该列占用7个字节,是一个回滚指针,这个回滚指针指向一条undo log日志的地址,通过这个undo log日志可以让这条记录恢复到前一个版本;

5.MVCC

1>.MVCC(Multi-Version Concurrency Control)多版本并发控制,其核心思想就是保存数据行的历史版本,通过对数据行的多个版本进行管理来实现数据库的并发控制;

2>.简单来说,我们平时看到的一条一条的记录,在数据库中保存的时候,可能不仅仅只有一条记录,而是有多个历史版本;

如图:
在这里插入图片描述

下面结合不同的隔离级别来说这张图;

5.1.可重复读

1>.首先,当我们通过INSERT/DELETE/UPDATE去操作一行数据的时候,就会产生一个事务ID,这个事务ID也会同时保存在行记录中(DB_TRX_ID),也就是说,当前数据行是哪个事务修改后得到的,是有记录的;

2>.INSERT/DELETE/UPDATE操作都会产生对应的undo log日志,每一行记录都有一个DB_ROLL_PTR指向undo log日志,每一行记录,通过执行undo log日志,就可以恢复到前一个记录,前前记录,前前前记录…;

3>.当我们开启一个事务的时候,首先会向InnoDB的事务系统申请一个事务id,这个事务id是一个严格递增的数字,在当前事务开启的一瞬间系统会创建一个数组,数组中保存了目前所有的活跃事务id,所谓的活跃事务就是指已开启但是还没有提交的事务;

这个数组中的最小值好理解就是本次第一个申请的事务id,但是最大值并不一定是当前事务的id.因为从申请好trx_id到创建数组这个过程是需要时间的,期间可能有其他会话也申请到了trx_id;

4>.如果当前事务想要去查看某一行数据,会先去查看该行数据的DB_TRX_ID:

①.如果这个值等于当前事务id,说明这就是当前事务修改的,那么这行数据对当前事务可见;

②.如果这个值小于数组中的最小值,说明当我们开启当前事务的时候,这行数据修改所涉及到的事务已经提交了,当前数据行对当前事务是可见的;

③.如果这个值大于数组中的最大值,说明这行数据是我们在开启事务之后,还没有提交的时候,有另外一个会话也开启了事务.并且修改了这行数据,那么此时这行数据对当前事务就是不可见的;

④.如果这个值的大小介于数组中最大值与最小值之间(闭区间),且该值不在数组中,说明这也是一个已经提交的事务修改的数据,这行数据对于当前事务也是可见的;

⑤.如果这个值的大小介于数组中最大值与最小值之间(闭区间),且该值存在数组中(不等于当前事务id),说明这是一个未提交的事务修改的数据,那么该行数据对当前事务是不可见的;

例如:

我们有 A、B、C、D 四个会话,首先A、B、C分别开启一个事务,事务ID是3、4、5,然后C会话提交了事务,A、B未提交.接下来D会话也开启了一个事务,事务ID是6,那么当D会话开启事务的时候,数组中的值就是[3,4,6].现在假设有一行数据的DB_TRX_ID是5(第四种情况),那么该行数据对会话D的事务就是可见的(因为会话D事务开启的时候它已经提交了),如果有一行数据的DB_TRX_ID是4,那么该行数据对会话D的事务就不可见(因为还未提交);

注意: 如果当前事务中涉及到数据的更新操作,那么更新操作是在当前读的基础上更新的,而不是在快照读的基础上更新的,如果是后者(快照读)则有可能导致数据丢失;

5>.案例
①.开启两个会话窗口A和B,首先在A窗口中开启事务:

BEGIN;
SELECT * FROM account;

在这里插入图片描述

②.在B窗口中做一次修改操作(不用显式开启事务,更新操作MySQL内部会开启事务,更新完成后事务会自动提交):

UPDATE account SET balance = balance + 100 WHERE id = 1;
SELECT * FROM account;

在这里插入图片描述

③.回到A窗口中,查询这行数据:
在这里插入图片描述

可以看到,该行数据并没有发生变化,符合预期(目前数据库事务隔离级别为可重复读);

④.在A窗口中做一次修改操作,然后再去查询:

UPDATE account SET balance = balance + 200 WHERE id = 1;
SELECT * FROM account;

在这里插入图片描述

可以看到该行数据是基于当前读(最新数据)做的修改操作,符合预期.如果基于快照读的来做修改,那么会话B的修改就会丢失,这显然是不对的;

其实MySQL中的update就是先读再更新,读的时候默认就是当前读,即会加锁,所以在上面的案例中,如果会话B中显式的开启了事务并且没有提交事务,那么会话A中的update语句就会被阻塞;这就是MVCC,一行记录存在多个版本.实现了读写并发控制,读写互不阻塞;同时MVCC中采用了乐观锁,读数据不加锁,写数据只锁行,降低死锁概率,并且还能据此实现快照读;

5.2.读已提交

读已提交和可重复读类似,区别主要是后者在每次事务开始的时候创建一致性视图(创建数组列出活跃事务id),而前者则每一个语句执行前都会重新算出一个新的视图,所以可重复读这种隔离级别会看到别的会话已经提交的数据(即使别的会话比当前会话开启的晚);

6.小结

1>.MVCC在一定程度上实现了读写并发,不过它只在读已提交和可重复读两个隔离级别下有效;

2>.读未提交总是会读取最新的数据行,序列化则会对所有读取的行都加锁,这两个隔离级别都和MVCC不兼容;

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值