背景介绍
事务隔离级别及错误情况
MySQL中共有4中事务隔离级别,分别是
- 未提交读(Read uncommitted)
- 已提交读(Read committed)
- 可重复读(Repeatable read)
- 可串行化(Serializable )
同时也有三种可能出现的错误情况
- 脏读(Dirty Read)
- 不可重复读(NonRepeatable Read)
- 幻读(Phantom Read)
其对应关系如下:
我在学习的过程中,共有两个难点
- 由于隔离级别与错误情况名称过于相似,有时会将二者搞混
相关知识
- 如何开启事务?
三个语句可以开启事务,分别是begin
,start transaction
,set autocommit=0
- 如何查看及修改MySQL当前事务隔离级别?
查看会话事务隔离级别:select @@tx_isolation;
查看全局事务隔离级别:select @@global.tx_isolation;
设置会话事务隔离级别:set session transaction isolation level ...
;
设置全局事务隔离级别:set global transaction isolation level ...
; - 示例数据表建表语句如下:
create table tbl
(
id int primary key,
name varchar(30)
)engine=innodb;
三种错误情况的含义
脏读:若事务A对数据进行了修改(包括insert和update),但这种修改还未提交到数据库中,此时事务B能访问事务A修改的数据,即为脏读
不可重复读:在事务B事务开始后,事务A对数据进行了修改并提交,事务B可以读取到的事务A修改后的数据,导致在事务B的整个过程中可以读取到的数据不一致,即不可重复读.
幻读:幻读是我在学习MySQL事务隔离级别中一直没有弄清含义的术语,在网上相关资料也未找到一个较为准确的解释.所以,在我的理解中,幻读有一个非常流氓的含义,即在串行化中不会出现但在可重复读会出现的异常情况.
其实在我看来,若知道在相应事务隔离级别下,两个事务在特定执行顺序下会出现什么结果即可,三种错误情况是为了区分四种事务级别而提出的术语,无需深究.
实验分析
未提交读
事务B可以读取到事务A未提交(commit)的数据,导致了脏读。如果事务A回滚了,就会造成数据的不一致。RU是事务隔离级别最低的。
首先设置事务隔离级别为未提交读.
set session transaction isolation level read uncommitted;
set global transaction isolation level read uncommitted;
事务A | 事务B |
---|---|
begin; | |
begin; | |
insert into tbl (id) values(1); | |
select * from tbl; | |
insert into tbl (id) values(2); | |
select * from tbl; | |
update tbl set name=’22’ where id=2; | |
select * from tbl; | |
commit; | |
commit; |
从上图可以看出,事务A(左边)即使没有提交,其执行的两次insert和一次update都可以在事务B中读到,即出现脏读.
提交读
只有事务A提交后,事务B方可看到事务A执行的操作.
首先设置事务隔离级别为提交读.
set session transaction isolation level read committed;
set global transaction isolation level read committed;
事务A | 事务B |
---|---|
begin; | |
begin; | |
select * from tbl; | |
insert into tbl (id) values(3); | |
select * from tbl; | |
commit; | |
select * from tbl; | |
commit; |
从上图可以看出,即使在事务A(左边)中执行了insert,若事务A没有提交,在事务B中无法读到事务A的insert数据,只有在事务A提交之后,事务B才可以读到事务A的insert数据,即避免了脏读.
虽然提交读避免了脏读,但仍然无法避免不可重复读.
事务A | 事务B |
---|---|
begin; | |
select * from tbl; | |
begin; | |
insert into tbl (id) values(4); | |
commit; | |
select * from tbl; | |
commit; |
由上图可知,由于事务A提交了,导致事务B可以看到事务A的操作,导致事务B中的两次select * from tbl
返回了不一致的数据,即不可重复读.
请思考一个问题,若在第2步执行之后,此时在事务B中无法读到事务A新增的id=3的数据,但,若此时事务B执行insert into tbl (id)values(3);
再插入一条id=3的数据,会出现什么情况呢?
可重复读
首先设置事务隔离级别为可重复读.
set session transaction isolation level repeatable read;
set global transaction isolation level repeatable read;
事务A | 事务B |
---|---|
begin; | |
select * from tbl; | |
begin; | |
insert into tbl (id) values(5); | |
commit; | |
select * from tbl; | |
commit; |
由上图可知,即使事务A提交了,但为了保证在事务B中看到的数据是一致的,第4步返回的数据和第1步是一致的,仍然无法看到事务A插入的新数据,即解决了不可重复读.
思考两个个问题,如果第3步之后,此时在事务B中执行insert into tbl (id)values(5)
会出现什么情况?此时事务B是读不到id=5的数据的,能插入成功吗?
若第3步之后在事务B中执行update tbl set name='55' where id=5
可以更新成功吗?若更新成功,此时执行select * from tbl
会返回id=5的数据吗?
最好的回答是做实验,结果如下:
答案显而易见.
(1)虽然事务B无法读取到id=6的数据,但id=6的数据确实存在,因此因为主键冲突插入失败.
(2)update
是可以执行成功的,因为事务A已经提交,释放了id=6的数据上的锁,所以事务B可以更新id=6的数据(若事务A没有commit,则update语句因无法获取锁而阻塞).而且更新之后,因为MySQL使用MVCC机制实现事务隔离,此时id=6的数据最高版本号即为事务B,所以事务B可以查看id=6的数据.
备注:由于id=5的数据已存在,实验中插入了id=6的数据,因此问题中问的是id=5的情况,而回答中回答的是id=6的情况.
可串行化
首先设置事务隔离级别为可串行化.
set session transaction isolation level serializable ;
set global transaction isolation level serializable ;
可串行化严格使用锁来进行隔离控制,select
对数据加S锁,update
和insert
对数据加X锁.因此在可串行化级别更易发生死锁.
原理
MySQL中使用锁和MVCC(Multi-Version Concurrency Control 多版本并发控制)实现隔离,在此不再深入.
mysql死锁问题分析
轻松理解MYSQL MVCC 实现机制