Mysql中的事务隔离

Innodb的锁

  • Record lock

单条索引记录上加锁,record lock锁住的永远是索引,而非记录本身,行锁锁定的是索引记录,而不是行数据,也就是说锁定的是key。即使该表上没有任何索引,那么Innodb在后台创建一个隐藏的聚集主键索引,那么锁住的就是这个隐藏聚集主键索引。所以说当一条sql没有走任何索引的时候,那么将会在每一个条聚集索引后面加X锁,这个类似于表锁,但原理上和表锁应该是完全不同的。如果一个条件无法通过索引快速过滤,存储引擎层面就会将所有记录加锁后返回,再由MySQL Server层进行过滤。
但在实际使用过程中,MySQL做了一些改进,在MySQL Server过滤条件,发现不满足后,会调用unlock_row方法,把不满足条件的记录释放锁(违背了二段锁协议的约束)。这样做,保证了最后只会持有满足条件记录上的锁,但是每条记录的加锁操作还是不能省略的。可见即使是MySQL,为了效率也是会违反规范的。(参见《高性能MySQL》中文第三版p181)

  • gap lock

在索引记录之间的间隙加锁,或者是在某一条索引记录之前或者之后加锁,并不包括索引记录本身。区间锁
https://www.zhihu.com/question/51390849

  • next-key lock

在默认情况下,mysql的事务隔离级别是可重复读,并且innodb_locks_unsafe_for_binlog参数为0,这时默认采用next-key locks。所谓Next-Key Locks,就是Record lock和gap lock的结合,即除了锁住记录本身,还要再锁住索引之间的间隙。

如果使用的是没有索引的字段,即使没有匹配任何数据,那么会给全表加入gap锁。同时,它不能像上文行锁一样经过MySQL Server过滤自动解除不满足条件的锁,因为没有索引,则这些字段也就没有排序,也就没有区间。除非该事务提交,否则其他事务无法插入任何数据。

行锁防止别的事务修改或删除,GAP锁防止别的事务新增,行锁和GAP结合的Next-Key锁共同解决了RR级别在写数据时的幻读问题。 ???

Mysql在RR隔离级别下是可以防止幻读的

为什么可以解决幻读,由于Mysql中的MVCC多版本并发控制机制。
在RR级别下,Mysql对读是不加锁的,但是对写加锁。
MVCC中,对读并没有加锁,而是进行快照读取(snapshot)。

MVCC一致性读
当一个T事务开始的时候,T会获得一个抽象的时间戳(版本),当对数据X进行读取的时候,并不是直接看到最新写入的数据而是在T开始前的所有执行中的事务中最后一个对X标记的版本(如果T修改过X,那么看到的是自己的版本)。也就是说T是基于当前的数据库的一个镜像进行操作的而T开始执行是获得的版本就是这个快照的凭证。这样能保证所有的读都是基于一个一致的状态获取的。所以MVCC一致性读机制就保证了再RR模式下的可重复读特性。

MVCC中的并发控制写
当进行update,insert,delete等修改操作时,是会使用锁的,而且是在事务提交后释放锁,使用了长期写锁。

RR模式下的当前读和快照读

  • 当前读
    update ,insert ,delete 在更新之前会读取当前最新的数据
  • 快照读
    select 会进行快照读

Innodb内部每个事务开始时,都会有一个事务id, 同时事务对象中还有一个read_view变量,用于控制该事务可见的记录范围(MVCC)。对于每个访问到的记录行,会根据read_view的trx_id(事务id)与行记录的trx_id比较,判断记录是否逻辑上可见。 即在select的时候是会去判断记录是否可见的,但是修改操作不会,是会直接去拿最新的数据的。

幻读的解决

MVCC的一致性读机制就保证了RR隔离级别下可以解决幻读。因为读操作会根据每一个访问到的记录行,比较read_view的trx_id(事务id)与行记录的的trx_id。只会取出第一次读取到的行。

在RR模式下,写操作加锁,不仅会对记录加行锁,还会加上next-key Lock。next-key Lock在一个session对记录进行了更改后,会对记录加上next-key lock(gap lock+record lock)。此时另一个session如果想插入一个条数据或者更新一条数据,是会遇到一些限制的,在一些情况下会被阻塞到session commit。至于哪些情况下更新数据会导致gap lock起作用,要看试图更新后的吉利是否在gap lock起作用的区间里。
https://www.zhihu.com/question/51390849

MVCC一致性读
其实MVCC一致性读并不是只有在RR模式下有,在RC模式下,也是存在MVCC一致性读的,但是为什么RC下会存在不可重复读呢,原因就是在RC模式下select 读取数据并不是按照事务开始的trx_id来判断是否逻辑上可见的,而是按语句开始的时候trx_id来判断是否逻辑上可见,其实也就变成了当前读。

mysql使用了一个并发版本控制机制,他们把它叫做MVCC,通俗的也就是说:mysql为了提高系统的并发量,在事务未提交前,虽然事务内操作的数据是锁定状态,但是另一个事务仍然可以读取。

幻读和不可重复读

很多人混淆不可重复读和幻读,确实这两者有点相似。但是不可重复读重点在于update和delete,而幻读的重点在于insert。

如果使用锁机制来实现这两种隔离级别,在可重复读中,该sql第一次读取到数据后,就将这些数据 加锁,其他事务无法修改这些数据,就可以实现可重复读了。但是这种方法却无法锁住insert的数据,所以当事务A先前读取了数据,或者修改了全部数据,事务 B还是可以insert数据提交,这时事务A就会发现莫名其妙的多了一条之前没有的数据,这就是幻读,不能通过行锁来避免。需要Serializable隔离级别,读用读锁,写用写锁,读锁和写锁互斥,这么做可以有效的避免幻读、不可重复读、脏读等问题,但会几大的降低数据库的并发能力。复读和幻读最大的区别,就在于如何通过锁机制来解决他们产生的问题。

上文说的,是使用悲观锁机制来处理 这两种问题,但是MySQL、Oracle、PostgreSQL等成熟的数据库,出于性能考虑,都是使用了以乐观锁为理论基础的MVCC(多版本并发控制)来避免这两种问题。

RR级别下的更新丢失

虽然Mysql下RR隔离级别是可以防止幻读的,但是无法防止丢失更新。

首先说下为什么会出现丢失更新,出现丢失更新的原因就是在一个事务中,后续的更新操作,使用之前查询出来的结果进行更新,这样就会出现丢失更新。

用户A在银行卡有100元钱,某一刻用户B向A转账50元(称为B操作),同时有用户C向A转账50元(称为C操作);

B操作从数据库中读取他此时的余额100,计算新的余额为100+50=150

C操作也从数据库中读取他此时的余额100,计算新的余额为100+50=150

B操作将balance=150写入数据库,之后C操作也将balance=150写入数据库
最终A的余额变为150

上面的例子,A同时收到两笔50元转账,最后的余额应该是200元,但却因为并发的问题变为了150元,原因是B和C向A发起转账请求时,同时打开了两个数据库会话,进行了两个事务,后一个事务拿到了前一个事务的中间状态数据,导致更新丢失。

那我们要怎么解决更新丢失的问题呢?

两种方式,悲观锁和乐观锁。

  • 乐观锁

在进行更新操作的时候才去检查当前值是否是期望值(类似CAS)

begin;
select balance from account where id=1;
-- 得到balance=100;然后计算balance=100+50=150
update account set balance = 150 where id=1 and balance = 100;
commit;
  • 悲观锁

悲观锁,在每一次更新之前都认为会有其他的进程对该数据进行修改。即在每一次更新之前都把需要更新的数据加排他锁。
我们知道insert,update,delete等修改操作在RR模式下是会将数据加上排他锁直到事务结束的。
那我们现在想要在查询的时候就将数据加上排他锁,需要如何去做呢。mysql中提供了相应的语句 for update;

begin;
select * from account where id = 1 for update;
update account set balance=150 where id =1;
commit;

for update 和 in share mode

    SELECT ... LOCK IN SHARE MODE sets a shared mode lock on the rows read. A shared mode lock enables other sessions to read the rows but not to modify them. The rows read are the latest available, so if they belong to another transaction that has not yet committed, the read blocks until that transaction ends. 

在读取的行上设置共享模式锁。共享模式锁允许其他会话读取行,但不修改行。读取的行是最新可用的,因此,如果它们属于尚未提交的另一个事务,则读取将阻塞,直到该事务结束。

 SELECT ... FOR UPDATE sets an exclusive lock on the rows read. An exclusive lock prevents other sessions from accessing the rows for reading or writing. 

在读取的行上设置独占锁。独占锁阻止其他会话访问用于读写的行。

for update 为阻塞读吗??? 应该不会把

https://blog.csdn.net/cug_jiang126com/article/details/50544728

https://zhuanlan.zhihu.com/p/31875702

gap lock的前置条件:
1 事务隔离级别为REPEATABLE-READ,innodb_locks_unsafe_for_binlog参数为0,且sql走的索引为非唯一索引
2 事务隔离级别为REPEATABLE-READ,innodb_locks_unsafe_for_binlog参数为0,且sql是一个范围的当前读操作,这时即使不是非唯一索引也会加gap lock

RR级别下会防止幻读到底是因为MVCC的快照读还是因为gap lock

看到网上有很多文章说Mysql中RR级别可以防止幻读的出现是因为gap lock。并且给出了相应的示例,示例都是以在RR级别下将innodb_locks_unsafe_for_binlog这个参数开启来佐证gap lock防止了幻读。

semi-consistent read
https://www.cnblogs.com/yuyutianxia/p/8548063.html

猜你喜欢

转载自blog.csdn.net/BryantLmm/article/details/84552686