【MySQL】事务与锁(二):事务并发控制方案(LBCC、MVCC)

大家想一下,如果要解决读一致性的问题,保证一个事务中前后两次读取数据结果一致,实现事务隔离,应该怎么做?因为在InnoDB里面,所有的活动都是运行在事务里面的,如果autocommit=1,每个SQL语句都是一个事务,所以这个问题也可以这么问:MySQL如何实现并发控制?

总体上来说,我们有两大类的方案:LBCC和MVCC。

1.方案一:LBCC

第一种,我既然要保证前后两次读取数据一致,那么我读取数据的时候,锁定我要操作的数据,不允许其他的事务修改就行了。这种方案我们叫做基于锁的并发控制Lock Based Concurrency Control(LBCC)。

如果仅仅是基于锁来实现事务隔离,一个事务读取的时候不允许其他时候修改,那就意味着不支持并发的读写操作,而我们的大多数应用都是读多写少的,这样会极大地影响操作数据的效率。

由于篇幅限制,锁的相关内容请参考:

2.方案二:MVCC

所以我们还有另一种解决方案,如果要让一个事务前后两次读取的数据保持一致,那么我们可以在修改数据的时候给它建立一个备份或者叫快照,后面再来读取这个快照就行了。这种方案我们叫做多版本的并发控制 Multi Version Concurrency Control(MVCC)。

2.1 MVCC是什么

MVCC的核心思想是: 我可以查到在我这个事务开始之前已经存在的数据,即使它在后面被修改或者删除了。在我这个事务之后新增的数据,我是查不到的。

这个快照什么时候创建?读取数据的时候,怎么保证能读取到这个快照而不是最新的数据?这个怎么实现呢?

InnoDB为每行记录都实现了两个隐藏字段:

  • DB_TRX_ID,6字节:插入或更新行的最后一个事务的事务ID,事务编号是自动递增的(我们把它理解为创建版本号,在数据新增或者修改为新数据的时候,记录当前事务ID)。
  • DB_ROLL_PTR,7字节:回滚指针(我们把它理解为删除版本号,数据被删除或记录为旧数据的时候,记录当前事务ID)。我们把这两个事务ID理解为版本号。

2.2 MVCC示例

1.第一个事务,初始化数据。此时数据表中,创建版本是当前事务ID,删除版本为空。

在这里插入图片描述

​ ==> 第二个事务,执行第1次查询。

在这里插入图片描述

读取到两条原始数据,这个时候事务ID是2。

2.第三个事务,插入数据。此时的数据,多了一条tom,它的创建版本号是当前事务编号3。

在这里插入图片描述

==> 第二个事务,执行第2次查询。

在这里插入图片描述

MVCC的查找规则:只能查找创建时间小于等于当前事务ID的数据,且删除时间大于当前事务ID的行(或未删除)。
==> 当前事务ID=2,所以只能查找新增事务ID<=2的数据。也就是不能查到在我的事务开始之后插入的数据,wangwu的创建ID大于2,所以还是只能查到两条数据。

3.第四个事务,删除数据,删除了id=2 jack这条记录。此时的数据,李四的删除版本被记录为当前事务ID(4),其他数据不变。

在这里插入图片描述

==> 第二个事务,执行第3次查询。

在这里插入图片描述

查找规则:只能查找创建时间小于等于当前事务ID的数据,和删除时间大于当前事务ID的行(或未删除)。
==> 当前事务ID=2,所以只能查找删除事务ID>2的数据。也就是,可以查到在我事务开始之后删除的数据,所以jack依然可以查出来。所以还是这两条数据。

4.第五个事务,执行更新操作,这个事务事务ID是5:

在这里插入图片描述

==> 第二个事务,执行第4次查询:

在这里插入图片描述

查找规则:只能查找创建时间小于等于当前事务ID的数据,和删除时间大于当前事务ID的行(或未删除)。
==> 当前事务ID=2,所以就是可以查找新增事务ID<=2和删除事务ID>2的数据。因为更新后的数据 zhaoliu 创建版本大于 2,代表是在事务之后增加的,查不出来。而旧数据 zhangsan的删除版本大于2,代表是在事务之后删除的,可以查出来。

通过以上演示我们能看到,通过版本号的控制,无论其他事务是插入、修改、删除,第一个事务查询到的数据都没有变化。

2.3 MVCC小结

上面演示的直接select都是快照读,读取的是记录数据的可见版本(可能是过期的数据),不用加锁。

而 insert/delete/update 在操作数据前的查找是当前读,即读取并操作的是记录数据的最新版本。举个例子,比如上面再 update mvcctest set name=left(name,4),最后的 name 是创建版本=5的zhao 而不是 zhan,所以能够保证最终事务结束后的结果是正确的。

那么,问题来了,如何保证在事务读取当前记录,然后操作的这段时间内,数据不被修改?答:加锁,MVCC+LBCC 搭配使用。保证其他事务不会再并发的修改这条记录。

3.最终方案:LBCC+MVCC

在InnoDB中,MVCC是通过Undo log实现的。Oracle、Postgres等等其他数据库都有MVCC的实现。需要注意,在InnoDB中,MVCC和锁是协同使用的,这两种方案并不是互斥的。

那MVCC与锁是什么关系呢?在InnoDB中,锁和MVCC都是用于并发控制。MVCC诞生的原因就是改善基于锁的方式带来的效率低的问题。使得读写之间互不阻塞,提高了单纯的基于锁的并发效率。

1)问题一:那这两者是如何协同保证并发安全的?

不同的事务隔离级别会有差异:

  1. Read Uncommited:RU隔离级别,不加锁。
  2. Read Commited:RC隔离级别下
    • 普通的 select 都是快照读,使用 MVCC 实现
    • 加锁的 select 都使用记录锁,因为没有 Gap Lock。
      注:除了两种特殊情况——外键约束检查(foreign-key constraint checking)以及重复键检查(duplicate-key checking)时会使用间隙锁封锁区间。所以RC会出现幻读的问题。
  3. Repeatable Read:RR隔离级别下
    • 普通的 select 使用快照读(snapshotread),底层使用 MVCC 来实现。
    • 加锁的 select(select … in share mode / select … for update) ,底层使用记录锁、或者间隙锁、临键锁。
    • 更新、删除操作 update, delete 等语句使用当前读(current read),底层使用记录锁、或者间隙锁、临键锁
  4. Serializable:
    1. 所有的 select 语句都会被隐式的转化为 select … in share mode,会和 update、delete互斥。

2)问题二:所以,事务隔离级别怎么选?

RU 和 Serializable 肯定不能用。为什么有些公司要用 RC,或者说网上有些文章推荐有 RC?

RC 和 RR 主要有几个区别:

  1. RR的间隙锁会导致锁定范围的扩大。
  2. 条件列未使用到索引,RR锁表,RC锁行。
  3. RC的“半一致性”(semi-consistent)读可以增加update操作的并发性。

在RC中,一个update语句,如果读到一行已经加锁的记录,此时InnoDB返回记录最近提交的版本,由MySQL上层判断此版本是否满足 update的where条件。若满足(需要更新),则MySQL会重新发起一次读操作,此时会读取行的最新版本(并加锁)。

PS:实际上,如果能够正确地使用锁(避免不使用索引去加锁),只锁定需要的数据,用默认的RR级别就可以了。

猜你喜欢

转载自blog.csdn.net/qq_33762302/article/details/114048502