MySQL事务隔离级别

事务隔离级别介绍

隔离级别 脏读 不可重复读 幻读
未提交读(Read uncommitted) 可能 可能 可能
已提交读(Read committed) 不可能 可能 可能
可重复读(Repeatable read) 不可能 不可能 可能
可串行化(Serializable ) 不可能 不可能 不可能
  1. 未提交读(Read Uncommitted):允许脏读,也就是可能读取到其他会话中未提交事务修改的数据
  2. 提交读(Read Committed):只能读取到已经提交的数据。Oracle等多数数据库默认都是该级别 (不重复读)
  3. 可重复读(Repeated Read):可重复读。在同一个事务内的查询都是事务开始时刻一致的,InnoDB默认级别。在SQL标准中,该隔离级别消除了不可重复读,但是还存在幻象读,但是innoDB解决了幻读
  4. 串行读(Serializable):完全串行化的读,每次读都需要获得表级共享锁,读写相互都会阻塞

接下来一次来验证每个隔离级别的特性,首先我们先建一张表,我们建立账户表account用来测试我们的事务隔离级别:

1
2
3
4
5
6
7
CREATE TABLE account (
`id` int(11) NOT NULL AUTO_INCREMENT,
`customer_name` varchar(255) NOT NULL,
`money` int(11) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE `uniq_name` USING BTREE (customer_name)
) ENGINE=`InnoDB` AUTO_INCREMENT=10 DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci ROW_FORMAT=COMPACT

RU (read uncommitted)读未提交隔离级别

首先我们开启Console A,然后设置session事务隔离级别为read uncommitted; 然后同样开启Console B,设置成read uncommitted;

1
2
3
4
5
6
7
8
9
10
mysql> set session transaction isolation level read uncommitted;
Query OK, 0 rows affected (0.03 sec)

mysql> select @@session.tx_isolation;
+------------------------+
| @@session.tx_isolation |
+------------------------+
| READ-UNCOMMITTED |
+------------------------+
1 rows in set (0.03 sec)

我们两个console的事务隔离级别都是read uncommitted,下面测试RU级别会发生的情况

img

小结:

可以发现RU模式下,一个事务可以读取到另一个未提交(commit)的数据,导致了脏读。如果B事务回滚了,就会造成数据的不一致。RU是事务隔离级别最低的。

RC (read committed)读提交隔离级别

现在我们将事务隔离级别设置成RC (read committed)

1
set session transaction isolation level read uncommitted;

img

img

小结

我们在RC模式下,可以发现。在console B没有提交数据修改的commit的时候,console A是读不到修改后的数据的,这就避免了在RU模式中的脏读,但是有一个问题我们会发现,在console A同一个事务中。两次select的数据不一样,这就存在了不可重复读的问题.PS:RC事务隔离级别是Oracle数据库的默认隔离级别.

RR (Repeatable read)可重复读隔离级别

img

img

小结

在RR级别中,我们解决了不可重复读的问题,即在这种隔离级别下,在一个事务中我们能够保证能够获取到一样的数据(即使已经有其他事务修改了我们的数据)。但是无法避免幻读,幻读简单的解释就是在数据有新增的时候,也无法保证两次得到的数据不一致,但是不同数据库对不同的RR级别有不同的实现,有时候或加上间隙锁来避免幻读。

innoDB 解决了幻读

前面的定义中RR级别是可能产生幻读,这是在传统的RR级别定义中会出现的。但是在innoDB引擎中利用MVCC多版本并发控制解决了这个问题

img

img

这算是幻读吗?在标准的RR隔离级别定义中是无法解决幻读问题的,比如我要保证可重复读,那么我们可以在我们的结果集的范围加一个锁(between 1 and 11),防止数据更改.但是我们毕竟不是锁住真个表,所以insert数据我们并不能保证他不插入。所以是有幻读的问题存在的。但是innodb引擎解决了幻读的问题,基于MVCC(多版本并发控制):在InnoDB中,会在每行数据后添加两个额外的隐藏的值来实现MVCC,这两个值一个记录这行数据何时被创建,另外一个记录这行数据何时过期(或者被删除)。 在实际操作中,存储的并不是时间,而是事务的版本号,每开启一个新事务,事务的版本号就会递增。所以当我们执行update的时候,当前事务的版本号已经更新了?所以也算是幻读??(存疑)主要是gap间隙锁+MVCC解决幻读问题?

Serializable 串行化隔离级别

所有事物串行,最高隔离级别,性能最差

存在的问题?

在RR模型,我们虽然避免了幻读,但是存在一个问题,我们得到的数据不是数据中实时的数据,如果是对实时数据比较敏感的业务,这是不现实的。
对于这种读取历史数据的方式,我们叫它快照读 (snapshot read),而读取数据库当前版本数据的方式,叫当前读 (current read)。很显然,在MVCC中:

快照读:就是select

  • select * from table ….;

当前读:特殊的读操作,插入/更新/删除操作,属于当前读,处理的都是当前的数据,需要加锁。

  • select * from table where ? lock in share mode;
  • select * from table where ? for update;
  • insert;
  • update ;
  • delete;

事务的隔离级别实际上都是定义了当前读的级别,MySQL为了减少锁处理(包括等待其它锁)的时间,提升并发能力,引入了快照读的概念,使得select不用加锁。而update、insert这些“当前读”,就需要另外的模块来解决了。

比如,我们有以下的订单业务场景,我们队一个商品下单的操作,我们得首先检查这个订单的数量还剩多少,然后下单。

事务1:

1
2
select num from t_goods where id=1;
update t_goods set num=num-$mynum where id=1;

事务2:

1
2
select num from t_goods where id=1;
update t_goods set num=num-$mynum where id=1;

假设这个时候数量只有1,我们下单也是只有1.如果在并发的情况下,事务1查询到还有一单准备下单,但是这个时候事务2已经提交了。订单变成0.这个事务1在执行update,就会造成事故。

  1. 解决问题方法1(悲观锁):就是利用for update对着个商品加锁,事务完成之后释放锁。切记where条件的有索引,否则会锁全表。
  2. 解决方法2(乐观锁):给数据库表加上个version字段。然后SQL改写:
1
2
select num,version from t_goods where id=1;
update t_goods set num=num-1,version=verison+1 where id=1 and version=${version}

参考

MySQL事务隔离级别和Spring事务关系介绍

Innodb 中 RR 隔离级别能否防止幻读?

MySQL 加锁处理分析