为了更好地理清类似脏读、不可重复读、幻读,未提交读、提交读、可重复读、串行化等概念,必需有这样一个认识:即这些概念都是属于数据库四大特性之一——隔离级别下的内容。而所谓的"隔离",当然是为了把问题给隔离和解决掉,而不同的隔离级别解决的便是不同级别的问题。可大致表示为4种隔离级别分别隔离4种问题
尽是问题 (丢失修改)
未提交读—————————————————————————————— (隔离线)
脏读问题
提交读————————————————————————————————
不可重复读问题
可重复读——————————————————————————————
幻读问题
串行化————————————————————————————————
从上可知,隔离级别 “可重复读” 解决的是问题 “不可重复读” ,但不能解决幻读。串行化可以解决所有问题,而对应的未提交读几乎什么问题都解决不了,只能解决最基本的丢失修改。
当然这是一般的情况,如果是Innodb引擎,由于引入了MVCC多版本并发控制和Next-Key Lock等锁机制,从而使得隔离级别 “可重复读” 也能解决幻读问题,那么也就变成了如下
尽是问题 (丢失修改)
未提交读—————————————————————————————— (隔离线)
脏读问题
提交读————————————————————————————————
不可重复 + 幻读问题
可重复读/串行化————————————————————————————————
下面便讲解下每种隔离级别解决对应问题的原理
[ 读前应知道:
共享锁,又称S锁、读锁,事务A对一个资源加了S锁后其他事务仍能共享读该资源,但不能对其进行写,直到A释放锁为止。
排它锁,又称X锁、写锁,事务A对一个资源加了X锁后只有A本身能对该资源进行读和写操作,其他事务对该资源的读和写操作都将被阻塞,直到A释放锁为止 ]
1> 未提交读(READ_UNCOMMITED)解决丢失修改
[ 丢失修改:多个事务同时盯上了一个数据,然后各写各的,谁把谁覆盖了都不知道,总之谁写的快谁就会被覆盖丢失信息。]
为了解决丢失修改的写覆盖问题,未提交读规定:
1.事务A对当前被读取的数据不加锁
2.事务A开始更新一行数据时,必须先对其加共享锁,直到事务结束才释放
从第二点就可以看出,事务A在写入数据的时候加了共享锁,其他事务只能读,不能写,所以事务A在写的过程中也就不会被覆盖,从而就解决了丢失修改的问题
然而根据未提交读的原理, 其会引入新的问题:脏读
2> 提交读(READ_COMMITED)解决脏读
[ 脏读:指别的事务修改后还没提交数据,事务A就可以读到这些数据,比如银行不小心转了一个亿到你的账户,银行人员发现幸好还没提交,此时银行人员马上回滚撤销操作,但你却在其之前取读发现自己账户居然有一个亿,这就是脏读 ]
为了解决未提交读下的脏读问题,提交读规定:
1.事务A对当前被读取的数据加共享锁,一旦读完该行,立即释放该共享锁(注意是读完立即释放)
2.事务A在更新某行数据时,必须对其加上排他锁,直到事务结束才释放(注意是事务结束才释放)
从第二点就可以看出,事务A在写入数据时加了排他锁,意味着其他事务根本就不能读到A更新但未提交的数据,如果把A换成银行,其他事务换成你,那么在上面的例子中你就不可能读到银行转了但未提交的一个亿,所以也就避免了脏读。因为排它锁从根本上使得事务只能读取到已提交的数据。
然而根据提交读的原理,其会引入新的问题:不可重复读
3> 可重复读(REPEATABLE READ)解决不可重复读
[ 不可重复读:两次读之间有别的事务修改,导致读的结果不一致,比如你两次快速读支苻宝之间你的花呗迅速还款并提交了数据,这样你会发现你第二次读突然少了一笔钱,这就是不可重复读。当然有些时候不可重复读根本不是个问题,比如你会觉得少一笔钱很正常,猜得到是花呗还了。但有些时候不行,举个例子,A和B跑马拉松,本来都是100KM,但在A和B同时读取跑步距离的一瞬间,赛委会调整了比赛距离为20KM并提交了数据,此时一个人就知道跑20KM,另一个则会继续跑100KM ]
为了解决不可重复读,可重复读规定:
1.事务A在读取某数据时,必须先对其加共享锁,直到事务结束才释放(注意是事务结束才释放)
2.事务A在更新某数据时,必须先对其加排他锁,直到事务结束才释放(注意是事务结束才释放)
从第一点就可以看出,事务A在加的是共享锁,并且要到事务结束才会释放该锁,也就意味着A在两次读取数据期间,其他事务不能对该数据进行更改,也就不会出现上面跑马拉松时对比赛距离的修改,从而解决了不可重复读
然而根据可重复读的原理,其会引入新的问题:幻读
4> 串行化(SERIALIZABLE)解决幻读
[ 幻读:两次读之间有别的事务增删,比如事务A想统计年薪100W以上的有多少人,当A两次读数据之间有其他事务新添加了一个CTO的记录,他的年薪也是100W+,所以A第二次读取到的数据突然多了一个,仿佛出现了幻觉一般,这就是一种幻读 ]
为了解决幻读,串行化规定:
1.事务在读取数据时,必须先对其加表级共享锁(注意这里是表级) ,直到事务结束才释放;
2.事务在更新数据时,必须先对其加表级排他锁(注意这里是表级) ,直到事务结束才释放。
从表级锁就可以看出,通过在一次操作中对整张表进行加锁,从而其他事务对整张表既不能insert,也不能delete,所以不会有行记录的增加或减少,从而保证了当前事务两次读之间数据的一致性,解决了幻读问题
然而根串行化的原理,其会导致写冲突,因此并发度急剧下降,一般不推荐使用该隔离级别
快照读下的MVCC
回到Innodb引擎上,由先前的结论知,其在可重复读(RR)隔离级别上亦可以解决幻读,要解决幻读就要保证自身事务读不到其他事务insert、delete所提交的内容,那么如何实现呢?
简单点讲:
其实RR级别下的MVCC为表提供了额外的隐藏字段,这些字段会记录每一个事务操作的版本号等信息,这些版本信息就类似于快照,由事务开始后第一次的普通select读操作生成,并且规定事务只能读取到比自身版本要早的数据,这也就意味着,假设事务A创建快照的版本是 “version-17”,那么A在commit前就只能读到 “version-17” 以前的内容,即使其他事务在A读之后进行了insert,delete提交,都不会影响A的读取,从而解决了幻读。
具体来讲:
其基本隐藏字段分别是:DATA_TRX_ID、DATA_ROLL_PTR、DB_ROW_ID以及DELETE BIT,其中DATA_TRX_ID是当前事务的版本号,又叫行的创建时间,DELETE BIT标志该记录是否被删除,又叫行的删除时间(当然要等到commit时才会生效),所以Innodb在查找数据的时候会比较这两个字段,并确保
1.查找版本早于当前事务版本
2.行的删除操作的版本要么未定义(未删除),要么大于当前事务的版本号(说明事务A读之前该数据未被删除)
这样即使其他事务在A读之后进行了insert,delete提交,都不会影响A的读取,从而解决了幻读。
[ 那MVCC的快照读就把幻读彻底就解决了吗? ]
当然不是。这里要注意,以上幻读的解决是基于快照读的,也就是说,MVCC能解决快照读下的幻读问题,但是没法保证当前读下的幻读问题。于是我们将引出下文
[ 读前应知道:
快照读:读取的是快照数据。note. (简单的select操作,属于快照读,一般不加锁)。
eg. select * from tb where ?;
当前读:即读取最新数据,避免读取快照。note. (当前读是特殊的读操作,插入/更新/删除操作,属于当前读,需要加锁)。
eg. select * from tb where ? lock in share mode;
select * from tb where ? for update;
insert into tb values (…);
update tb set ? where ?;
delete from tb where ?;
可以看到,快照读就是读操作,但当前读却不局限于读操作 ]
当前读下的Next-Key Lock
看完当前读后的定义便知道,如果不借助快照,对于select版的当前读:
事务A同样可能会读到其他事务insert、delete提交后的数据,导致多出或少掉数据,造成幻读
对于update和delete版的当前读:
由于Mysql Server会针对update和delete操作里面的where条件查找满足条件的记录(查找的不是快照)然后Innodb引擎会返回的满足条件的加锁记录,待Mysql Server收到后,再进行下一步查找。所以每一次查找就会进行一次当前读,根据select版当前读的结论,可以明显的发现其会导致幻读
对于insert版的当前读:
其插入操作可能会触发Unique Key的冲突检查,也会进行一个当前读,同样,根据select版当前读结论,其也会导致幻读
所以总结下来,这也就是上文提到的MVCC没法保证解决当前读下的幻读问题的原因
[ 那么如何解决当前读下的幻读问题? ]
这里就要引入Next-Key Lock锁机制了,Next-Key Lock是Gap Lock间隙锁和Record Lock行锁的结合版,都属于Innodb的锁机制,当然,如果详细的讲解Next-Key Lock锁机制的话可能需要再开一篇文章叙述,所以这里就大致讲一下其解决当前读下幻读的思路:
回到上面出现问题的原因,都是因为并发环境下其他事务插入/删除了满足当前查询条件的记录,而导致事务A前后读出的数据集不对。举个例子,假设A当前查询条件是where 年薪>1,000,000 , 正 是 因 为 其 他 事 务 在 A 两 次 读 之 间 插 入 了 年 薪 > 1 , 000 , 000 ,正是因为其他事务在A两次读之间插入了年薪>1,000,000 ,正是因为其他事务在A两次读之间插入了年薪>1,000,000的记录,才导致A在这个条件下出现幻读,所以看到这里,思路也就出来了,只要我们能对年薪>1,000,000$这个小范围的记录加上锁不就行了吗?也不需要对整个表加锁以至于进化成效率极低的SERIALIZABLE。
而正好,Next-Key Lock锁恰好就是对这个范围 “智能” 加锁的,Next-Key Lock会有一套算法,以确定一段范围,然后对这个范围加锁,这样,也就保证A在where的条件下读到的数据是一致的,因为在where这个范围其他事务根本插不了也删不了数据,都被Next-Key Lock锁堵在一边阻塞掉了。
————————————————
版权声明:本文为CSDN博主「虚拟WORLD-er」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_37960007/article/details/90644635