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