一、概念说明
脏读:指的是读到了其他事务未提交的数据,未提交意味着这些数据可能会回滚,也就是可能最终不会存到数据库中,也就是不存在的数据。读到了并不一定最终存在的数据,这就是脏读。
可重复读:在一个事务内,最开始读到的数据和事务结束前的任意时刻读到的同一批数据都是一致的。
不可重复读:对比可重复读,不可重复读指的是在同一事务内,不同时刻读到的同一批数据可能是不一样的,可能会受到其他事务的影响,比如其他事务改了这一批数据并提交了。
幻读:幻读是针对数据插入操作来说的。假设事务A对某些行的内容做了更改,但是还未提交,此时事务B插入了与事务A更新签的记录相同的记录行,并且在事务A提交之前先提交了,而这时,在事务A中查询,会发现好像刚刚的更改对于某些数据未起作用,但其实事务B刚插入进来的,让用户感觉很魔幻,这就是幻读。
二、事务隔离级别
2.1 事务隔离级别概述
1、读未提交(READ UNCOMMITTED)
MySQL事务隔离其实是依赖锁来实现的,加锁自然会带来性能的损失。而读未提交隔离级别是不加锁的,所以它的性能是最好的,没有加锁、解锁带来的性能开销。但是有利就有弊,这基本上就是裸奔了,所以连脏读的问题都没法解决。
2、读已提交(READ COMMITTED)
既然读未提交没办法解决脏数据的问题,那么就有了读提交。读提交就是一个事务只能读到其他事务已经提交过的数据,也就是其他事务调用commit命令之后的数据。
3、可重复读(REPEATABLE READ)
可重复读是对比不可重复而言的,上面说不可重复读是指同一个事务不同时刻读到的数据可能不一致。而重复读是指事务不会读到其他事务对已有数据的修改,即使其他事务已提交,就是说,事务开始时读到的已有数据是什么,在事务提交前的任意时刻,这些数据的值都是一样的。但是,对于其他事务新插入的数据是可以读到的,这就引发了幻读问题。
4、串行化(SERIALIZABLE)
串行化是4种事务隔离级别中隔离效果最好的,解决了脏读、不可重复读、幻读的问题,但是性能最差,它将事务的执行变为顺序执行,与其他三个隔离级别相比,它相当于单线程,后一个事务的执行必须等待前一个事务结束。
MySQL默认的事务隔离级别是可重复读(REPEATABLE READ)
事务隔离其实就是为了解决上面提到的脏读、不可重复读、幻读这几个问题
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交 | 可能 | 可能 | 可能 |
读提交 | 不可能 | 可能 | 可能 |
可重复读 | 不可能 | 不可能 | 不可能 |
串行化 | 不可能 | 不可能 | 不可能 |
2.2 事务隔离级别实现原理
1、读未提交
性能最好的事务隔离级别,也可以说是最野蛮的方式,因为它压根儿都不加锁,所以谈不上什么隔离效果,可以理解为没有隔离。
2、串行化
串行化在读的时候加共享锁,也就是其他事务可以并发读,但是不能写,写的时候加排它锁,其他事务不能并发写也不能并发读。
3、读提交和可重复读
为了解决不可重复读,或者为了实现可重复读,MySQL采用了MVVC(多版本并发控制)的方式。在数据库表中看到的一行记录可能实际上有多个版本,每个版本的记录除了数据本身外,还要有一个表示版本的字段,即为row trx_id,而这个字段就是使其产生事务的ID,事务ID记为transaction id,它在事务开始的时候向事务系统申请,按时间先后顺序递增。
按照上面这张图理解,一行记录现在就有3个版本,每一个版本都记录着使其产生的事务ID。在读的过程中,有一个快照的概念,这个学名叫做一致性视图,这就是可重复读和不可重复读的关键,可重复读是在事务开始的时候生成一个当前事务全局性的快照,而读提交则是每次执行执行语句的时候都重新生成一次快照。
对于一个快照来说,它能读到哪些版本数据,要遵循一下规则:
● 当前事务内的更新,可以读到;
● 版本未提交,不能读到;
● 版本已提交,但是却在快照创建后提交的,不能读到;
● 版本已提交,且是在快照创建前提交的,可以读到。
读提交和可重复读的主要区别就是在快照的创建上,可重复读仅在事务开始时创建一次,而读提交每次执行语句的时候都要创建一次。
2.3 并发写和幻读问题解决
1、并发写
当条件字段是索引时,直接在索引树中找到这条数据,直接加上行锁;当条件字段为非索引时,MySQL会为这张表中所有行加行锁,加上行锁后,MySQL会进行一遍过滤,发现不满足行就释放锁,最终只留下符合条件的行。
2、幻读
MySQL已经在可重复读隔离级别下解决了幻读的问题,而解决幻读用的也是锁,叫做间隙锁,MySQL把行锁和间隙锁合并在一起,解决了并发写和幻读的问题,这个锁叫做Next-Key锁。
假设现在表中有两条记录,并且age字段已经添加了索引,两条记录age的值分别为10和30。
此时,在数据库中会为索引维护一套B+树,用来快速定位行记录。B+索引数是有序的,所以会把这张表的索引分割成几个区间。
如图所示,分成了3个区间,(负无穷, 10]、(10, 30]、(30, 正无穷],在这三个区间是可以加间隙锁的。
如上图过程,在事务A提交之前,事务B的插入操作只能等待,这就是间隙锁的作用。当事务A执行update user set name='风筝2号’ where age = 10;的时候,由于条件 where age = 10 ,数据库不仅在 age =10 的行上添加了行锁,而且在这条记录的两边,也就是(负无穷,10]、(10,30]这两个区间加了间隙锁,从而导致事务B插入操作无法完成,只能等待事务A提交。不仅插入 age = 10 的记录需要等待事务A提交,age<10、10<age<30 的记录页无法完成,而大于等于30的记录则不受影响,这足以解决幻读问题了。
如果age不是索引列,那么数据库会为整个表加上间隙锁,所以在没有索引的情况下,无论age是否大于等于30,都要等待事务A提交可以成功插入。