更多优质内容
请关注公众号

Mysql事务和锁(二) 事务的隔离级别和MVCC-张柏沛IT博客

正文内容

Mysql事务和锁(二) 事务的隔离级别和MVCC

栏目:数据库 系列:Mysql事务和锁系列 发布时间:2020-11-02 12:08 浏览量:1932

在讨论锁之前,要从事务的隔离级别先说起

Mysql事务的四个隔离级别,级别从低到高为

读未提交【read uncommitted】(会出现脏读、不可重复读和幻读的问题)

读已提交【read committed】(会出现不可重复读和幻读)

可重复读【repeatable read】(会出现幻读)

串行化【serializable


隔离级别越高,安全性越高,但是性能越低。Mysql事务的默认级别是可重复读,而oracle的事务是读已提交的级别。由于读未提交的数据安全得不到保证,而串行化这个级别下并发度低,所以大多数数据库的隔离级别都是选的读已提交和可重复读这两种。


下面再介绍一下什么是脏读、不可重复读和幻读

AB两个客户端同时开启事务并在事务中执行一些操作


脏读情景:(隔离级别设为读未提交)

B先修改一条数据X,执行了update把xx=10改为x=20,可是还没有执行commit;此时A在事务中读数据X, 执行select,此时A查到了20



不可重复读情景:(隔离级别设为读已提交,此时脏读不会出现,但是不可重复读的问题却出现了)

  1. B先修改一条数据X,执行了update把xx=10改为x=20可是还没有执行commit;此时A在事务中读数据X, 执行了select,此时A查到了10(很好,这才应该是正常的)。


  1. 接着,B执行了commit,此时A再执行 select x,得到20,但是A还没有commit,也就是说A在同一个事务中对数据x执行了两次select,两次select的结果居然不一样,A居然在自己的事务里面读到了其他客户端事务修改提交后的数据值(这样就说明数据不安全了)。



幻读情景:(隔离级别设为可重复读,此时脏读和不可重复读不会出现,但是幻读的问题却出现了)

什么是幻读,你可以理解为一个事务两次“当前读”得到的数据集不一样。

AB同时打开事务 begin;此时表t中只有一条数据 (id=1, name=zbp1)

A先查找:

Select * from t where id=2;

A没有commit

发现没有id2的数据,于是A想要插入一条id2的数据。


但是此时 BA先插入了一条数据

Insert into t values (null, zbp2);

然后B commit了。


然后A执行

Insert into t values (null, zbp2);

报错说:id2的记录已经存在了。


执行 select * from t;

发现还是只有 (id=1, name=zbp1)



请问,幻读具体是指上面的那一条语句,或者发生在那句sql中。这个问题就可以看出是否真正理解幻读。


答案是,幻读发生在了A执行insert失败这条sql

Ainsert新数据的时候,会看看当前最新数据中是否有id2的数据(是一个当前读),但是发现有id2的数据了,所以就插入失败了。也就是说Ainsert的时候发生了幻读,幻读在这个例子中表现为事务A第一次select的时候是没有查到有id2的数据,结果在insert中隐式查询有没有id2的数据时却查到了有。


该如何解决,可以使用临键锁,让A在一开始查询的时候用 select * from t where id=2 for update (1,+)这个间隙给锁住。

间隙锁会在后面介绍行锁的时候再细说。


在介绍幻读的时候,可能大家不理解什么叫做当前读。接下来就介绍MVCC的相关概念,理解MVCC是之后理解锁机制的一个关键前提。



MVCC(多版本并发控制)

是一种不用加锁就能让多个事务并发读写的机制。


下面我们看看MVCC的底层到底发生了什么,它是如何同过不加锁的方式做到并发读写:

情景如下:
有一个innodbtt表中只有2个字段(idname

Id

Name

Trx_id

Roll_pointer

1

Lilei

100


诶,不是说表里只有两个字段吗?为什么还有trx_idroll_pointer呢?

其实 trx_id roll_pointer这两个字段是innodb表的隐藏字段。每一次事务都会有一个事务id,当在一个事务中执行增改的操作的时候,就会在操作对应的行中添加这个事务idtrx_id这个字段中(select的时候不会)。


我们知道,在事务中的每一次操作的旧数据都会被记录到undo日志以备回滚,undo日志一开始是写入缓冲区,到commit的时候才写入到磁盘。roll_pointer字段记录的是这条行数据在undo日志中的地址,以方便找到旧数据的记录进行回滚。


回到正题,现在表中只有1条记录,是由之前的一个事务id100的事务创建的记录。


现在有3个客户端A,B分别开启了事务。

1.A先执行update
Update t set name = ‘zbp’ where id = 1;

此时,底层会对id为1的记录生成一个历史快照的记录,放在undo日志中。然后再更新现在的id为1的数据的name字段。如下:

当前数据变为

Id

Name

Trx_id

Roll_pointer

1

zbp

101

X1


X1是历史数据在undo日志中的地址。


历史数据(放在undo日志中)

1

Lilei

100



现在A还没有commit


2.B执行了
Select * from t where id=1;

此时读取到的namelilei而不是zbp。因为事务B会读取历史数据而不会去读A事务更改后的数据(也就是当前数据)。

请大家注意一点:A执行了修改,会对数据上一个行级排他锁,而且还没有commit,所以这个排它锁没有释放。之后B进行查询相同的行居然没有被阻塞,说明了一点:Bselect的时候并没有加任何锁,这就是MVCC的功劳,因为A是对当前数据进行上锁,而B是去读undo日志中的历史数据,所以无需等待A释放锁。


为了解释上面的现象,需要提出下面的概念:


快照读 当前读

MVCC并发控制中,读操作可以分成两类:快照读 (snapshot read)与当前读 (current read)

快照读,读取的是记录的可见版本 (有可能是历史版本),不用加锁。

当前读,读取的是记录的最新版本,并且当前读返回的记录,都会加上锁,保证其他事务不会再并发修改这条记录。

快照读是哪一个正常的select…语句就是快照读。

当前读是哪些Insert语句、update语句、delete语句、显示加锁的select语句(select… LOCK IN SHARE MODEselect… FOR UPDATE)是当前读。为什么insertupdatedelete语句都属于当前读(是的,修改之前会先隐式查询,这个查询时一个当前读)?当前读的SQL语句,InnoDB是逐条与MySQL Server层交互的。即先对一条满足条件的记录加锁后,再返回给MySQL Server,当MySQL Server层做完DML操作后,再对下一条数据加锁并处理。


事务中同一数据的读-写和写-读操作可以并发进行而不阻塞其实就是依赖MVCC,它主要是通过在undo日志中记录了数据的一个历史版本。当select 的时候会发生一个快照读,由于快照读无需上锁,所以在一个未提交的事务中,读-写和写-读都不会发生阻塞。

假设没有undo日志保存数据的历史版本的话,在读数据的时候就必须读当前数据,读当前数据就必须上一个读锁,此时读-写或者写-读就会发生一个阻塞。


再小结一下:当前读要上锁,快照都不用上锁。




更多内容请关注微信公众号
zbpblog微信公众号

如果您需要转载,可以点击下方按钮可以进行复制粘贴;本站博客文章为原创,请转载时注明以下信息

张柏沛IT技术博客 > Mysql事务和锁(二) 事务的隔离级别和MVCC

热门推荐
推荐新闻