事务简介
事务就是一组sql语句同时执行,要么同时都成功,要么同时都失败。
因为mysql的事务默认是自动提交的,所以为了演示一组sql的行为,我们要关掉自动提交。
准备的表(经典的转账例子):
关闭自动提交:
我们要演示的逻辑是:
Jack向Rose转账500块。
所以这里有两条sql:
update money set balance=500 where user='Jack';
update money set balance=1500 where user='Rose';
这两条sql构成了一个事务(将多个操作包裹在一起,可以形成一个事务)。
执行它们。
此时查一下:
已经变了。
但是,这是内存中的数据变了,并未持久化到磁盘。
在我们commit
之前,可以反悔。
使用rollback
就可以回到上次的状态。
如果你已经commit
了,就无法回滚了:
将两条sql包裹在一个事务中,同时成功或者同时失败,其实就是事务的原子性的含义,原子的意思就是不可再分割。
那么mysql是如何保证这种原子性的呢?其实对于一行数据,mysql会给你生成一个undolog链表,比如你插入(1,1000, ‘Jack’), 然后执行:
update money set balance=500 where user='Jack'
这时候就有两个历史版本了:
这里多出来两个隐藏字段:transaction_id(事务ID),roll_pointer(回滚指针)。0x123456是我随便写的,意思就是指向之前的版本(形成一个链表)。
这里又有两个概念,一个叫当前读,就是读到的是版本链中最新的数据;另一个是快照读,就是版本链中的老数据。普通的select
就是快照读,而delete
、update
、insert
,还有加锁的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。
演示
read uncomitted
首先,我们还是用原来的表:
开两个session。
将隔离级别都设置成read uncomitted:
并且都阻止自动提交:
对于session1:
未提交,事务并未结束。
在session2中:
读到的数据是修改了的。
这是一种错误,叫做脏读。
现在session1rollback
:
在session2中:
id为1的balance又变成1000了。
这叫做不可重复读,因为两次读到的数据不一样(同一行数据)。
事务仍未结束,我们插一条数据(session1):
在session2中:
session1紧接着rollback
。
在session2中:
新插的数据像出现幻觉一样消失了,叫做幻读(session2两次读到的行数不同)。
两个session都commit
,结束事务。
总结一下,读未提交存在脏读、幻读和不可重复读的问题,我们不会使用这种隔离级别。
read committed
两个session都改成read committed:
session1更改数据:
session2查询:
没有了脏读。
如果此时session1commit
了。
对于session2:
其实没有问题。因为session1已经修改完了(commit
了),所以session2读到修改后的数据是对的。
现在session1再对id=1的数据进行update
。
此时session2再去读:
哦,变成了200。session2还没有commit,也就是事务没有结束。如果这写在java代码里,就会很奇怪:我开启一个事务,在事务没有结束的情况下,我取了两次id=1的数据,一次balance是500,一次又是200,这就要出bug了。所以读已提交有不可重复读的问题。
session1和session2都commit
掉并且恢复数据,Jack的balance改成1000,我们重新做实验。
我们先让session2查一次:
两行初始记录,没问题。
然后session1插入一条记录:
此时session2再去查一次:
变成了3条记录了。想象一下,在同一个方法内(事务没结束的情况下),第一次查询有两条记录,第二次查询有三条记录,是不是会很奇怪。
两个session同时commit
结束事务。
repeatable read
现在演示可重复读。
还是一样,修改隔离级别,并且开启事务:
不演示脏读了。
session2在session1更新之前读一次,session1更新、commit
之后再读一次:
两次select的结果一致。所以msql的事务隔离级别叫做可重复读。
下面检查幻读的问题。
重新开始。
session2先查一次:
session1再插入一条数据:
session2再查一次:
还是一样的结果。
所以说,可重复读也解决了幻读。
但这是innodb引擎,这是快照读。我们不打算演示myisam引擎的情况,但是要看一下当前读的情况。
重新开启事务。
session2先查一次:
session1插入一条数据:
session2再查一次:
很好,没有幻读。
然后session2试图将所有的名字更新成xxx:
session2预估是两条数据受影响,可是出现了三条数据受影响,这说明在当前读的情况下还是会有幻读。
serializable
最后是serializable。
和上面的操作一样,改级别,开事务。
session2先读,然后session1进行update。
session1阻塞。
然后看session1先写,session2再读:
session2阻塞。
虽然读写串行进行,没有了脏读、幻读和不可重复读,但是并发能力大大降低。我们不会使用该隔离级别。
简单解释
我们不管读未提交,这个东西不用,但是我们简单看看串行化。
假设事务的隔离级别是读未提交,我们能否实验出串行化的效果。
session2读的时候加S,REC_NOT_GAP
锁。
session1阻塞,无法获取X
锁。
如果session1先update:
session2无法进行当前读。
至于读已提交和可重复读,mysql有自己的算法避免脏读和不可重复读读,也用了行锁和间隙锁避免幻读。问题是:我们怎么选择这两种隔离级别。
首先我们可以看看可重复读对数据一致性的追求:
如果是报表这样的业务,我们希望在同一个时间点取出的数据是一致的,即使在代码运行的过程中,有些数据被更改了,我们还是沿用进入代码第一次读到的数据。
所以在一个方法里,即使都是读,我们也需要使用事务,并且是可重复读级别的。我们不希望第一次读出id=1的balance是1000,第二次读出来是500,这样子怎么统计嘛。
但如果真的有人把id=1的balance改成500了,此时我们自己也要改,就需要注意了:
我第一次读出来是1000,然后进入业务代码,要将balance减100,于是我得到900。在我写回去之前:
session2先下手一步改成了500。
此时如果我再写回去就会用900覆盖500了,这就出现了脏写:
最后的结果就是900。而我们希望的结果是500-100=400。
这里可以看出可重复读他的运行机制了,“读”读的是老数据,“写”要拿新的数据,这其实就是写时复制的思想,将读和写进行分离,从而提高并发能力。
以上问题的解决其实就是不要自己去算那个900,将
update money set balance = 900 where id=1;
改成
update money set balance = balance-100 where id=1;
就没有问题了,这样他就会拿最新的balance出来,复制一份,然后让你去改(减100),然后再写回去。
但是,可重复读为了解决幻读采用的间隙锁有很大开销,没有必要加锁的地方,他也加锁。
我们给balance加上二级索引。
然后修改一下数据。
就算session1没有任何命中也加上了间隙锁。
所以究竟使用RC还是RR,追求并发用RC,追求数据一致性用RR。