(1)读未提交,Read Uncommitted(脏读):这个很坑爹,就是说某个事务修改的数据还没提交的时候,就让别的事务给读到了,这就恶心了,很容易导致出错的。
 
(2)读已提交,Read Committed(不可重复读):这个比上面那个稍微好一点,但是一样比较尴尬就是说事务A在准备付款50元钱的时候, 先查询了余额,是100,足够付款。然后过了段时间,事务B把那个100扣掉了,并且还提交了,此时事务A确认付款,但是账户金额却不够扣了。
这是读了人家事务提交的数据啊,所以是读已提交。
这也叫做不可重复读,就是所谓的一个事务内对一个数据两次读,可能会读到不一样的值。
 
(3)可重复读,Read Repeatable:这个比上面那个再好点儿,就是说事务A在执行过程中,对某个数据的值,无论读多少次都是值1;哪怕这个过程中事务B修改了数据的值还提交了,但是事务A读到的还是自己事务开始时这个数据的值。
原因:可重复读默认使用的读取方式是快照读。
 
(4)幻读:不可重复读和可重复读都是针对两个事务同时对某条数据在修改,但是幻读针对的是插入。
幻读指的是一个事务在前后两次查询同一个范围的时候,后一次查询看到了前一次查询没有看到的数据。
那么幻读会带来啥问题呢?例如:事务1要插入一条数据,我先查询一下有没有相同的数据,但是这时事务2添加了这条数据,这就会导致事务1插入失败,并且它就算再一次查询,也无法查询到与其插入相冲突的数据,同时自身死活都插入不了。
 
来分析下情形:
1)T1时刻 读取年龄为20的数据, Session1拿到了2条记录。
2)T2时刻 另一个进程Session2插入了一条新的记录,年龄也为20
3)T3时刻,Session1再次读取年龄为20的数据,发现还是2条数据,貌似 Session2新插入的数据并未影响到Session1的事务读取。
对于T1 -- T3 时刻的情形,从结果来看,在可重复度读隔离级别下似乎解决了幻读的问题。
4)T4时刻,Session1 修改年龄为20的数据, 发现影响行数为3条。 为什么T3时候只能查到2条数据,但现在修改确修改了3条数据?
5)T5时刻,Session1 再次读取年龄为20的数据,发现结果变成了3条,我们知道被修改的第三条就是Session2在T2时刻新增的一条。
T4,T5 的结果来看,Session1 读到了 Session2 新插入的数据,产生了幻读现象。
 
(5)串行化:如果要解决幻读,就需要使用串行化级别的隔离级别,所有事务都串行起来,不允许多个事务并行操作。
 
MySQL的默认隔离级别是Read Repeatable,就是可重复读,就是说每个事务都会开启一个自己要操作的某个数据的快照,事务期间,读到的都是这个数据的快照罢了,对一个数据的多次读都是一样的。
 
快照读
简单的select操作(不包括 select ... lock in share mode, select ... for update)
 
那么快照读究竟是什么呢,是对当前数据的全量拷贝吗?每开启一个事务,都要把当前数据库的数据拷贝一份出来?
很明显不是。
一方面,这样做太消耗内存了,另一方面,这样会很慢。
 
快照读其实很简单,也很绝妙 —— 数据版本,也就是我们常说的MVCC,多版本并发控制。
 
1)Innodb里面,每行数据,都可以有多个版本,每个版本都有一个字段trx_id,记录生成这个版本的事务的ID。
假设一开始,id=1这行数据,只有一个版本,trx_id是90,意味着生成这个版本的事务ID是90。
这时候Session A开始了,从上一讲,我们已经知道,begin时并不会生成快照,快照在第一次select时才会生成,那么第一次select时,Session A都做了什么呢?
 
2)Session A只需要做一件事:用一个数组(ReadView),来记录当前活跃的事务ID。
假设session A的事务ID是97,当前还有另外两个事务,事务ID是94、96,所以session A会生成一个 [94,96,97] 的数组,也叫ReadView,而ReadView中存储我们系统中当前活跃着的读写事务的trx_id。
 
注意:是系统中所有的活跃事务的trx_id,例如,查询符合 id = 1 这个条件的数据的所有的活跃事务。
 
3)这个数组有什么用?后面你就知道了。
接着,session B执行了update语句,来更新id=1这一行数据,给这一行数据生成一个新的版本,假设session B的事务ID是98,因此这行数据就有了两个版本。
 
4)这时候,session A又来select了,当前版本是session B生成的,那session A是如何找到之前的版本的呢?
 
这时候,session A一开始生成的事务数组就派上用场了,session A的事务数组是[94,96,97],最小事务ID是94,最大事务ID是97,所以,当它遇到一行数据时,会先判断这行数据的版本号X:
如果X大于97,那么意味着这行数据,是在session A开始之后,才提交的,应该对session A不可见。
如果X小于97,那么分两种情况:
1.如果X在数组里面,比如X是96,那么意味着,当session A开始时,生成这个版本的数据的事务,还没提交,因此这行数据对Session A不可见。
2.如果X不在数组里面,比如X是95,那么意味着,当session A开始时,生成这个版本的数据的事务,已经提交,因此这行数据对Session A可见。
 
5)好,现在session A开始遍历 id=1 这行数据的所有版本:
当前版本是98,大于97,表示是在 session A 的事务之后生成的版本,所以不可见,继续看上一个版本,96也不符合,再往上,再往上,可能看到的版本是90,小于94,可见,就它了,而为什么要小于94呢?
因为94作为当前数据中的最早的版本,既然被记录进来了, 那么就证明94是跟97在同一时间活跃的事务,所以可能还未提交,那么自然不能读它的数据,而小于94的版本,意味着在Session A事务之前就已经提交了,所以就可以读。
 
小结:
    1. 大于当前事务的trx_id的数据版本不能读。
    2. 小于当前事务的trx_id,并且不在快照数组中的版本,可以读。
    3. 不确定,好像当前的事务id版本也不可以读?因为是未提交版本?
 
那么所有的版本的数据在哪呢?
答案是 -> 版本链,从版本链中先找最近记录,如果最近这一条记录事务id不符合条件,不可见的话,再去找上一个版本再比较当前事务的id和这个版本事务id看能不能访问,以此类推直到返回可见的版本或者结束。
 
举个例子 ,在已提交读隔离级别下:
比如此时有一个事务id为100的事务,修改了name,使得的name等于小明2,但是事务还没提交。则此时的版本链是,如下:
MySQL事务隔离级别-LMLPHP
 
那此时另一个事务发起了select 语句要查询id为1的记录,那此时生成的ReadView 列表,也就是数组,只有[100]。那就去版本链去找了,首先肯定找最近的一条,发现trx_id是100,也就是name为小明2的那条记录,发现在列表内,所以不能访问。
 
这时候就通过指针继续找下一条,name为小明1的记录,发现trx_id是60,小于列表中的最小id,所以可以访问,直接访问结果为小明1。
 
那这时候我们把事务id为100的事务提交了,并且新建了一个事务id为110也修改id为1的记录,并且不提交事务
MySQL事务隔离级别-LMLPHP
 
这时候版本链就是
MySQL事务隔离级别-LMLPHP
 
这时候之前那个select事务又执行了一次查询,要查询id为1的记录。
 
这个时候关键的地方来了
如果你是读已提交隔离级别,这时候你会重新创建一个ReadView,那你的活动事务列表中的值就变了,变成了[110]。
 
按照上的说法,你去版本链通过trx_id对比查找到合适的结果就是小明2。
如果你是可重复读隔离级别,这时候你的ReadView还是第一次select时候生成的ReadView,也就是列表的值还是[100]。所以select的结果是小明1。所以第二次select结果和第一次一样,所以叫可重复读!
 
在《Mysql实战45讲》里,丁奇给出了这样一个“等价判断”可见性的原则:
  • 版本未提交,不可见;
  • 版本已提交,但是是在快照创建后提交的,不可见;
  • 版本已提交,而且是在快照创建前提交的,可见;
 
这其实就是可重复读的想要实现的效果。
 
 
当前读
  读取的是记录数据的最新版本,并且当前读返回的记录都会加上锁,保证其他事务不会再并发的修改这条记录
  概念说的比较虚,也不好理解,接着举一个例子吧,假设你开启了两个事务,分别是A和B,这里有个张表,user表,里面有四条数据
MySQL事务隔离级别-LMLPHP
 
查询如果想要使用当读的话可以手动加上 lock in share mode 或者 for update ,例如:
select ... lock in share mode, select ... for update

update、insert、delete

  当执行这几个操作的时候默认会执行当前读,也就是会读取最新的记录,也就是别的事务提交的数据你也可以看到,这样很好理解啊,假设你要update一个记录,另一个事务已经delete这条数据并且commit了,这样不是会产生冲突吗,所以你update的时候肯定要知道最新的信息啊。
 
  我在这里介绍一下update的过程吧,首先会执行当前读,然后把返回的数据加上锁,之后执行update。加锁是防止别的事务在这个时候对这条记录做什么,默认加的是排他锁,也就是你读都不可以,这样就可以保证数据不会出错了。但注意一点,就算你这里加了写锁,别的事务也还是能访问的,是不是很奇怪?数据库采取了一致性非锁定读,别的事务会去读取一个快照数据。
 
  innodb默认隔离级别是RR, 是通过MVVC来实现了,读方式有两种,执行select的时候是快照读,其余是当前读,所以,mvvc不能根本上解决幻读的情况。
10-17 13:07