首先来看一下不同事务隔离级别可能造成的问题:
1. 脏读
脏读是指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据。
来看个例子:
#首先,修改隔离级别
set tx_isolation='READ-UNCOMMITTED';
#事务A:启动一个事务
start transaction;
select * from tx;
+------+------+
| id | num |
+------+------+
| 1 | 1 |
| 2 | 2 |
| 3 | 3 |
+------+------+
#事务B:也启动一个事务(那么两个事务交叉了)在事务B中执行更新语句,且不提交
start transaction;
update tx set num=10 where id=1;
select * from tx;
+------+------+
| id | num |
+------+------+
| 1 | 10 |
| 2 | 2 |
| 3 | 3 |
+------+------+
#事务A:这时候事务A能看到这个更新了的数据
select * from tx;
+------+------+
| id | num |
+------+------+
| 1 | 10 | --->可以看到!说明我们读到了事务B还没有提交的数据
| 2 | 2 |
| 3 | 3 |
+------+------+
#事务B:事务B回滚,仍然未提交
rollback;
select * from tx;
+------+------+
| id | num |
+------+------+
| 1 | 1 |
| 2 | 2 |
| 3 | 3 |
+------+------+
#事务A:在事务A里面看到的也是B没有提交的数据
select * from tx;
+------+------+
| id | num |
+------+------+
| 1 | 1 |
| 2 | 2 |
| 3 | 3 |
+------+------+
2.不可重复读
不可重复读是指在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的,因此称为是不可重复读。
看个例子:
set tx_isolation='read-committed';
#事务A:启动一个事务
start transaction;
select * from tx;
+------+------+
| id | num |
+------+------+
| 1 | 1 |
| 2 | 2 |
| 3 | 3 |
+------+------+
#事务B:也启动一个事务(那么两个事务交叉了)在这事务中更新数据,且未提交
start transaction;
update tx set num=10 where id=1;
select * from tx;
+------+------+
| id | num |
+------+------+
| 1 | 10 |
| 2 | 2 |
| 3 | 3 |
+------+------+
#事务A:这个时候我们在事务A中不能看到数据的变化
select * from tx;
+------+------+
| id | num |
+------+------+
| 1 | 1 |
| 2 | 2 |
| 3 | 3 |
+------+------+
#事务B:当事务B提交后,事务A再次读取数据发现数据不一致了!
commit;
#事务A:
select * from tx;
+------+------+
| id | num |
+------+------+
| 1 | 10 |--->因为事务B已经提交了,所以在A中我们看到了数据变化
| 2 | 2 |
| 3 | 3 |
+------+------+
3.幻读
网上有很多说法是这样的:幻读是指当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的“幻影” 行。这不是和不可重复读的定义是一样的吗?那幻读究竟不一样在哪里?
其实幻读并不是说两次读取获取的结果集不同,幻读侧重的方面是某一次的 select 操作得到的结果所表征的数据状态无法支撑后续的业务操作。更为具体一些:select 某记录是否存在,不存在,准备插入此记录,但执行 insert 时发现此记录已存在,无法插入,此时就发生了幻读。
看个例子:
#首先,更改隔离级别
set tx_isolation='repeatable-read';
#事务A:启动一个事务
start transaction;
select * from tx;
+------+------+
| id | num |
+------+------+
| 1 | 1 |
| 2 | 2 |
| 3 | 3 |
+------+------+
#事务B:开启一个新事务(那么这两个事务交叉了)在事务B中更新数据,并提交
start transaction;
insert into tx values(4,4);
select * from tx;
+------+------+
| id | num |
+------+------+
| 1 | 1 |
| 2 | 2 |
| 3 | 3 |
| 4 | 4 |
+------+------+
commit;
#事务A:这时候即使事务B已经提交了,但A不能看到数据变化
select * from tx;
+------+------+
| id | num |
+------+------+
| 1 | 1 |
| 2 | 2 |
| 3 | 3 |
+------+------+
#事务A:此时B已经增加了一条记录,那A重复插入会怎样呢?
insert into tx values(4,4);
ERROR 1317(70100) : Query execution was interrupted
commit;
隔离级别
看完不同隔离级别可能造成的问题后,再来分析一下这几种隔离级别分别是怎么处理这些问题的。数据库的四种隔离级别由低到高分别为:
READ-UNCOMMIT —> READ-COMMIT—>REPEATABLE-READ —>SERIALIZABLE
1. READ-UNCOMMIT
READ_UNCOMMIT就是“读未提交”,这个隔离级别下可以读取到其他事务未提交的数据,会产生脏读、不可重复读、幻读。
2.READ-COMMIT
READ_COMMIT在上一级别的基础上,不能读取未提交的事务的数据,但是在同一个事务中还是有可能出现两次读取的数据不相同的情况也就是不可重复读。
3.REPEATABLE-READ
REPEATABLE-READ解决了不可重复读的问题,它采用了MVCC(Multi-Version Concurrency Control),即多版本并发控制。读取数据时会产生一次快照,之后每一次读都从当前版本快照中读取,当我们insert\update\delete时,才会修改版本号。
4.SERIALIABLE
SERIALIABLE是MYSQL的最高级别,它的实现是通过对 SELECT 隐式加锁(因为InnoDB的行锁锁定的是索引,故记录实体存在与否没关系,存在就加 行X锁,不存在就加 next-key lock间隙X锁)来实现的。所以我们在REPTEATABLE-READ下对 SELECT 操作也手动加行(X)锁即可类似 SERIALIZABLE 级别,例如:
# 这里需要用 X锁, 用 LOCK IN SHARE MODE 拿到 S锁后我们没办法做写操作
SELECT `id` FROM `users` WHERE `id` = 1 FOR UPDATE;