mysql的事务及事务的隔离级别

transaction

      • 事务简介
      • 并发的情况
        • 演示
          • read uncomitted
          • read committed
          • repeatable read
          • serializable
        • 简单解释

事务简介

事务就是一组sql语句同时执行,要么同时都成功,要么同时都失败。

因为mysql的事务默认是自动提交的,所以为了演示一组sql的行为,我们要关掉自动提交。

准备的表(经典的转账例子):

mysql的事务及事务的隔离级别_第1张图片
关闭自动提交:

mysql的事务及事务的隔离级别_第2张图片

我们要演示的逻辑是:

Jack向Rose转账500块。

所以这里有两条sql:


update money set balance=500 where user='Jack';
update money set balance=1500 where user='Rose';

这两条sql构成了一个事务(将多个操作包裹在一起,可以形成一个事务)。

执行它们。

此时查一下:

mysql的事务及事务的隔离级别_第3张图片

已经变了。

但是,这是内存中的数据变了,并未持久化到磁盘。

在我们commit之前,可以反悔。

mysql的事务及事务的隔离级别_第4张图片

使用rollback就可以回到上次的状态。

如果你已经commit了,就无法回滚了:

mysql的事务及事务的隔离级别_第5张图片

将两条sql包裹在一个事务中,同时成功或者同时失败,其实就是事务的原子性的含义,原子的意思就是不可再分割。

那么mysql是如何保证这种原子性的呢?其实对于一行数据,mysql会给你生成一个undolog链表,比如你插入(1,1000, ‘Jack’), 然后执行:

update money set balance=500 where user='Jack'

这时候就有两个历史版本了:

mysql的事务及事务的隔离级别_第6张图片
这里多出来两个隐藏字段:transaction_id(事务ID),roll_pointer(回滚指针)。0x123456是我随便写的,意思就是指向之前的版本(形成一个链表)。

这里又有两个概念,一个叫当前读,就是读到的是版本链中最新的数据;另一个是快照读,就是版本链中的老数据。普通的select就是快照读,而deleteupdateinsert,还有加锁的select,比如读锁:select ... in share mode,或者写锁:select ... for update就是当前读,他们都要获取最新的数据。

有了这样的链表,也就能够回滚了,也就实现了原子性。


并发的情况

多个事务的情况会比较复杂。

比如有两个事务t1和t2(可以想象成两条线程同时操作)。

session2读到了session1更新的未提交的数据,叫脏读
session2在同一个事务中两次读到的行数不同(因为session1增加了一条),叫幻读
session2读了一个字段,session1更新了它,session2再读的时候,值不同了,叫做不可重复读

可以看到幻读强调行数不同,不可重复读强调同一条数据不同。

不同数据库的事务隔离级别是不同的。

从低到高,一共有四种:

  • read uncommitted(读未提交)
  • read committed(读已提交)
  • repeatable read(可重复读)
  • serializable(串行化)

级别越高,安全性越高,也可以说是事务的隔离级别越高。

mysql默认是repeatable read。

mysql的事务及事务的隔离级别_第7张图片


演示

read uncomitted

首先,我们还是用原来的表:

mysql的事务及事务的隔离级别_第8张图片

开两个session。

将隔离级别都设置成read uncomitted:

mysql的事务及事务的隔离级别_第9张图片

并且都阻止自动提交:

对于session1:

未提交,事务并未结束。

在session2中:
mysql的事务及事务的隔离级别_第10张图片

读到的数据是修改了的。

这是一种错误,叫做脏读。

现在session1rollback

在session2中:

mysql的事务及事务的隔离级别_第11张图片

id为1的balance又变成1000了。

这叫做不可重复读,因为两次读到的数据不一样(同一行数据)。

事务仍未结束,我们插一条数据(session1):

在session2中:

mysql的事务及事务的隔离级别_第12张图片

session1紧接着rollback

在session2中:

mysql的事务及事务的隔离级别_第13张图片

新插的数据像出现幻觉一样消失了,叫做幻读(session2两次读到的行数不同)。

两个session都commit,结束事务。

总结一下,读未提交存在脏读、幻读和不可重复读的问题,我们不会使用这种隔离级别。


read committed

两个session都改成read committed:

mysql的事务及事务的隔离级别_第14张图片

session1更改数据:

session2查询:

mysql的事务及事务的隔离级别_第15张图片

没有了脏读。

如果此时session1commit了。

对于session2:

mysql的事务及事务的隔离级别_第16张图片
其实没有问题。因为session1已经修改完了(commit了),所以session2读到修改后的数据是对的。

现在session1再对id=1的数据进行update

mysql的事务及事务的隔离级别_第17张图片
此时session2再去读:

mysql的事务及事务的隔离级别_第18张图片

哦,变成了200。session2还没有commit,也就是事务没有结束。如果这写在java代码里,就会很奇怪:我开启一个事务,在事务没有结束的情况下,我取了两次id=1的数据,一次balance是500,一次又是200,这就要出bug了。所以读已提交有不可重复读的问题。

session1和session2都commit掉并且恢复数据,Jack的balance改成1000,我们重新做实验。

mysql的事务及事务的隔离级别_第19张图片
mysql的事务及事务的隔离级别_第20张图片

我们先让session2查一次:

mysql的事务及事务的隔离级别_第21张图片

两行初始记录,没问题。

然后session1插入一条记录:

mysql的事务及事务的隔离级别_第22张图片

此时session2再去查一次:

mysql的事务及事务的隔离级别_第23张图片

变成了3条记录了。想象一下,在同一个方法内(事务没结束的情况下),第一次查询有两条记录,第二次查询有三条记录,是不是会很奇怪。

两个session同时commit结束事务。


repeatable read

现在演示可重复读。

还是一样,修改隔离级别,并且开启事务:

mysql的事务及事务的隔离级别_第24张图片

不演示脏读了。

session2在session1更新之前读一次,session1更新、commit之后再读一次:

mysql的事务及事务的隔离级别_第25张图片

mysql的事务及事务的隔离级别_第26张图片

两次select的结果一致。所以msql的事务隔离级别叫做可重复读。

下面检查幻读的问题。

重新开始。

session2先查一次:

mysql的事务及事务的隔离级别_第27张图片

session1再插入一条数据:

mysql的事务及事务的隔离级别_第28张图片

session2再查一次:

mysql的事务及事务的隔离级别_第29张图片

还是一样的结果。

所以说,可重复读也解决了幻读。

但这是innodb引擎,这是快照读。我们不打算演示myisam引擎的情况,但是要看一下当前读的情况。

重新开启事务。

session2先查一次:

mysql的事务及事务的隔离级别_第30张图片
session1插入一条数据:

mysql的事务及事务的隔离级别_第31张图片

session2再查一次:

mysql的事务及事务的隔离级别_第32张图片
很好,没有幻读。

然后session2试图将所有的名字更新成xxx:

mysql的事务及事务的隔离级别_第33张图片

session2预估是两条数据受影响,可是出现了三条数据受影响,这说明在当前读的情况下还是会有幻读。


serializable

最后是serializable。

和上面的操作一样,改级别,开事务。

mysql的事务及事务的隔离级别_第34张图片

mysql的事务及事务的隔离级别_第35张图片
session2先读,然后session1进行update。

mysql的事务及事务的隔离级别_第36张图片

mysql的事务及事务的隔离级别_第37张图片

session1阻塞。

然后看session1先写,session2再读:

mysql的事务及事务的隔离级别_第38张图片

mysql的事务及事务的隔离级别_第39张图片

session2阻塞。

虽然读写串行进行,没有了脏读、幻读和不可重复读,但是并发能力大大降低。我们不会使用该隔离级别。

简单解释

我们不管读未提交,这个东西不用,但是我们简单看看串行化。

假设事务的隔离级别是读未提交,我们能否实验出串行化的效果。

mysql的事务及事务的隔离级别_第40张图片
mysql的事务及事务的隔离级别_第41张图片
mysql的事务及事务的隔离级别_第42张图片
session2读的时候加S,REC_NOT_GAP锁。

mysql的事务及事务的隔离级别_第43张图片
session1阻塞,无法获取X锁。

如果session1先update:

mysql的事务及事务的隔离级别_第44张图片
mysql的事务及事务的隔离级别_第45张图片
session2无法进行当前读。


至于读已提交和可重复读,mysql有自己的算法避免脏读和不可重复读读,也用了行锁和间隙锁避免幻读。问题是:我们怎么选择这两种隔离级别。

首先我们可以看看可重复读对数据一致性的追求:

如果是报表这样的业务,我们希望在同一个时间点取出的数据是一致的,即使在代码运行的过程中,有些数据被更改了,我们还是沿用进入代码第一次读到的数据。

所以在一个方法里,即使都是读,我们也需要使用事务,并且是可重复读级别的。我们不希望第一次读出id=1的balance是1000,第二次读出来是500,这样子怎么统计嘛。

但如果真的有人把id=1的balance改成500了,此时我们自己也要改,就需要注意了:

mysql的事务及事务的隔离级别_第46张图片

我第一次读出来是1000,然后进入业务代码,要将balance减100,于是我得到900。在我写回去之前:

mysql的事务及事务的隔离级别_第47张图片

session2先下手一步改成了500。

此时如果我再写回去就会用900覆盖500了,这就出现了脏写:

mysql的事务及事务的隔离级别_第48张图片
mysql的事务及事务的隔离级别_第49张图片
最后的结果就是900。而我们希望的结果是500-100=400。

这里可以看出可重复读他的运行机制了,“读”读的是老数据,“写”要拿新的数据,这其实就是写时复制的思想,将读和写进行分离,从而提高并发能力。

以上问题的解决其实就是不要自己去算那个900,将

update money set balance = 900 where id=1;

改成

update money set balance = balance-100 where id=1;

就没有问题了,这样他就会拿最新的balance出来,复制一份,然后让你去改(减100),然后再写回去。


但是,可重复读为了解决幻读采用的间隙锁有很大开销,没有必要加锁的地方,他也加锁。

mysql的事务及事务的隔离级别_第50张图片
我们给balance加上二级索引。

mysql的事务及事务的隔离级别_第51张图片
然后修改一下数据。

mysql的事务及事务的隔离级别_第52张图片
mysql的事务及事务的隔离级别_第53张图片

就算session1没有任何命中也加上了间隙锁。

所以究竟使用RC还是RR,追求并发用RC,追求数据一致性用RR。

你可能感兴趣的:(MySQL,数据库,mysql,隔离级别,演示,事务)