mysql如何实现4种事务隔离级别

MySQL 事务隔离级别

MySQL InnoDB事务的隔离级别有四级,默认是“可重复读”(REPEATABLE READ)。

未提交读(READ UNCOMMITTED)。另一个事务修改了数据,但尚未提交,而本事务中的SELECT会读到这些未被提交的数据(脏读)。

提交读(READ COMMITTED)。本事务读取到的是最新的数据(其他事务提交后的)。问题是,在同一个事务里,前后两次相同的SELECT会读到不同的结果(不重复读)。

可重复读(REPEATABLE READ)。在同一个事务里,SELECT的结果是事务开始时时间点的状态,因此,同样的SELECT操作读到的结果会是一致的。但是,会有幻读现象(稍后解释)。

串行化(SERIALIZABLE)。读操作会隐式获取共享锁,可以保证不同事务间的互斥。


四个级别逐渐增强,每个级别解决一个问题。

脏读,最容易理解。另一个事务修改了数据,但尚未提交,而本事务中的SELECT会读到这些未被提交的数据。
这个可以通过读已提交解决。

不重复读。解决了脏读后,会遇到,同一个事务执行过程中,另外一个事务提交了新数据,因此本事务先后两次读到的数据结果会不一致。
对当前数据加行锁解决。

幻读。解决了不重复读,保证了同一个事务里,查询的结果都是事务开始时的状态(一致性)。但是,如果另一个事务同时提交了新数据,本事务再更新时,就会“惊奇的”发现了这些新数据,貌似之前读到的数据是“鬼影”一样的幻觉。
通过MVCC+间隙锁解决




MySQL 事务实现原理

事务的实现是基于数据库的存储引擎,不同的存储引擎对事务的支持程度不一样。MySQL 中支持事务的存储引擎有InnoDB 和 NDB。 InnoDB 是高版本 MySQL 的默认的存储引擎,因此就以 InnoDB 的事务实现为例,InnoDB 是通过多版本并发控制(MVCC,Multiversion Concurrency Control )解决不可重复读问题,加上间隙锁(也就是并发控制)解决幻读问题。因此 InnoDB 的 RR 隔离级别其实实现了串行化级别的效果,而且保留了比较好的并发性能。


什么是MVCC?

MVCC 全称是多版本并发控制系统,InnoDB 和 Falcon 存储引擎通过多版本并发控制(MVCC,Multiversion Concurrency Control)机制解决幻读问题。

innoDB 的 MVCC 是通过在每行记录后面保存两个隐藏的列来实现,这两个列一个保存了行的创建时间,一个保存行的过期时间(删除时间)。当然存储的并不是真实的时间而是系统版本号(system version number)。每开始一个新的事务,系统版本号都会自动新增,事务开始时刻的系统版本号会作为事务的版本号,用来查询到每行记录的版本号进行比较。

在RR隔离级别下,MVCC的操作如下:

select操作
InnoDB只查找版本早于(包含等于)当前事务版本的数据行。可以确保事务读取的行,要么是事务开始前就已存在,或者事务自身插入或修改的记录。
行的删除版本要么未定义,要么大于当前事务版本号。可以确保事务读取的行,在事务开始之前未删除。

insert操作
将新插入的行保存当前版本号为行版本号。

delete操作
将删除的行保存当前版本号为删除标识。

update操作
insert和delete操作的组合,insert的行保存当前版本号为行版本号,delete则保存当前版本号到原来的行作为删除标识。

这篇文章已经举了例子:
https://www.cnblogs.com/myseries/p/10930910.html


简要把例子概括一下:

第一步:事务A插入3条记录:

innoDB为新插入的每一行保存当前系统版本号作为版本号. 第一个事务ID为1;

start transaction;
insert into yang values(NULL,'yang') ;
insert into yang values(NULL,'long');
insert into yang values(NULL,'fei');
commit;

事务A查询结果:select * from yang ;
在这里插入图片描述

第二步:事务B插入一条数据

另一个事务ID为3往这个表里插入了一条数据;  第三个事务ID为3;

start transaction;
insert into yang values(NULL,'tian');
commit;

事务B的查询结果:
在这里插入图片描述

事务A这个时候的查询结果:select * from yang ;
在这里插入图片描述
事务A的查询结果没变,因为:
select * from yang ; 这个普通查询语句属于快照读。使用多版本并发控制MVCC保证事务。

InnoDB会根据以下两个条件检查每行记录:
  a.InnoDB只会查找版本早于当前事务版本的数据行(也就是,行的系统版本号小于或等于事务的系统版本号),这样可以确保事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或者修改过的.
  b.行的删除版本要么未定义,要么大于当前事务版本号,这可以确保事务读取到的行,在事务开始之前未被删除.
 只有a,b同时满足的记录,才能返回作为查询结果.

 

快照读和当前读

当前读:
  select...lock in share mode (共享读锁)
  select...for update
  update , delete , insert
当前读:  
单纯的select操作,不包括上述 select ... lock in share mode, select ... for update。 


第3步:事务C删除并更新了一条数据:

start   transaction;  
delete from yang where id=1;
update yang set name='Long' where id=2;
commit;  

当前数据库中的实际状态是:
在这里插入图片描述

事务A读到的是:select * from yang;
在这里插入图片描述
事务A的查询结果没变,因为还是上面的:
select * from yang ; 这个普通查询语句属于快照读。使用多版本并发控制MVCC保证事务。
InnoDB会根据以下两个条件检查每行记录:
  a.InnoDB只会查找版本早于当前事务版本的数据行(也就是,行的系统版本号小于或等于事务的系统版本号),这样可以确保事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或者修改过的.
  b.行的删除版本要么未定义,要么大于当前事务版本号,这可以确保事务读取到的行,在事务开始之前未被删除.
 只有a,b同时满足的记录,才能返回作为查询结果.



但是如果事务A这个时候用当前读sql去读:select * from yang for update
在这里插入图片描述
得到的是当前最新的记录。也就是当前读查询出了其他事务新插入的行。


结论:

对于使用MVCC的可重复读(REPEATABLE READ)隔离级别。普通的快照读,已经解决不可重复读和幻读的问题,同时没有使用锁,并发性能高。但是,快照读读取的都是历史数据,并没有解决当前读情况下的幻读问题:即可以看到其他数据的新增。所以需要通过间隙锁来保证当前读情况下的幻读问题



间隙锁:

间隙锁:只有在Read Repeatable、Serializable隔离级别才有,就是锁定范围空间的数据。

比如:

select * from yang where id>2 for update;
id>2的记录全部被锁住了,其他事务无法更新id>2的所有记录。包括新增id=6的记录。
因为此时如果不锁定没有的数据,例如当加入了新的数据id=6,就会出现幻读,间隙锁避免了幻读。

间隙锁注意事项:

1.对主键或唯一索引,如果当前读时,where条件全部精确命中(=或者in),这种场景本身就不会出现幻读,所以只会加行记录锁。

2.没有索引的列,当前读操作时,会加全表gap锁,生产环境要注意。

3.非唯一索引列,如果where条件部分命中(>、<、like等)或者全未命中,则会加附近Gap间隙锁。例如,某表数据如下,非唯一索引2,6,9,9,11,15。如下语句要操作非唯一索引列9的数据,gap锁将会锁定的列是(6,11],该区间内无法插入数据。



结论:

对于可重复度隔离级别,如果普通读也用间隙锁那就和串行化没有区别了,并发性能很差。所以使用MVCC实现了读的非阻塞,提升了读性能。可是无法避免当前读情况下的幻读,所以加上间隙锁可以手工保证实现不出现幻读。





参考资料:
https://www.cnblogs.com/myseries/p/10930910.html
https://www.cnblogs.com/tiancai/p/12053126.html
http://blog.sina.com.cn/s/blog_499740cb0100ugs7.html
https://www.cnblogs.com/wwcom123/p/10727194.html

猜你喜欢

转载自blog.csdn.net/qq_38205881/article/details/120386529