Mysql数据库事务:从未提交读—>MVCC机制—>Next-Key Lock,各种隔离级别及其解决对应问题的原理

为了更好地理清类似脏读、不可重复读、幻读,未提交读、提交读、可重复读、串行化等概念,必需有这样一个认识:即这些概念都是属于数据库四大特性之一——隔离级别下的内容。而所谓的"隔离",当然是为了把问题给隔离和解决掉,而不同的隔离级别解决的便是不同级别的问题。可大致表示为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$这个小范围的记录加上锁不就行了吗?也不需要对整个表加锁以至于进化成效率极低的SERIALIZABLE。

而正好,Next-Key Lock锁恰好就是对这个范围 "智能" 加锁的,Next-Key Lock会有一套算法,以确定一段范围,然后对这个范围加锁,这样,也就保证A在where的条件下读到的数据是一致的,因为在where这个范围其他事务根本插不了也删不了数据,都被Next-Key Lock锁堵在一边阻塞掉了。


[ 以上就是一整套流程, 也希望看完后能对隔离级别及其所引申出的一系列问题与解决原理有一个整体上的认识 ]

[ 主要参考文章 ] [ ]去除,[dot]替换成 .

http[ ]s://dev[dot]mysql[dot]com/doc/refman/5[dot]7/en/innodb-next-key-locking[dot]html
http[ ]://hedengcheng[dot]com/?p=771
http[ ]s://segmentfault[dot]com/a/1190000012669504?utm_source=tag-newest
http[ ]s://blog[dot]csdn[dot]net/liwenqiang758/article/details/81013005
http[ ]s://blog[dot]csdn[dot]net/u010365819/article/details/84026040
http[ ]s://blog[dot]csdn[dot]net/ashic/article/details/53735537
http[ ]s://blog[dot]csdn[dot]net/w_linux/article/details/79666086
http[ ]s://blog[dot]csdn[dot]net/chenyiminnanjing/article/details/82714341
http[ ]s://www[dot]cnblogs[dot]com/zhoujinyi/p/3435982[dot]html
http[ ]s://www[dot]sohu[dot]com/a/194511597_610509

发布了37 篇原创文章 · 获赞 42 · 访问量 14万+

猜你喜欢

转载自blog.csdn.net/qq_37960007/article/details/90644635