MySQL

76-MySQL-多版本并发控制

2023-01-11  本文已影响0人  紫荆秋雪_文

一、什么是MVCC

MVCC (Multiversion Concurrency Control),多版本并发控制。顾名思义,MVCC 是通过数据行的多个版本管理来实现数据库的 并发控制 。这项技术使得在InnoDB的事务隔离级别下执行 一致性读 操作有了保证。换言之,就是为了查询一些正在被另一个事务更新的行,并且可以看到它们被更新之前的值,这样在做查询的时候就不用等待另一个事务释放锁。无需排队,提高了并发性。MVCC是为了解决在读-写或写-读的情况下需加锁的问题

二、快照读与当前读

MVCC在MySQL InnoDB中的实现主要是为了提高数据库并发性能,用更好的方式去处理 读-写冲突 ,做到即使有读写冲突时,也能做到 不加锁 , 非阻塞并发读 ,而这个读指的就是 快照读 , 而非 当前读当前读实际上是一种加锁的操作,是悲观锁的实现。而MVCC本质是采用乐观锁思想的一种方式。

2.1 快照读

快照读又叫一致性读,读取的是快照数据不加锁的简单的 SELECT 都属于快照读,即不加锁的非阻塞读;之所以出现快照读的情况,是基于提高并发性能的考虑,快照读的实现是基于MVCC,它在很多情况下,避免了加锁操作,降低了开销。既然是基于多版本,那么快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本快照读的前提是隔离级别不是串行级别串行级别下的快照读会退化成当前读。

SELECT * FROM player WHERE ...

2.2 当前读

当前读读取的是记录的最新版本(最新数据,而不是历史版本的数据),读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。加锁的 SELECT,或者对数据进行增删改都会进行当前读。

# 共享锁
SELECT * FROM student LOCK IN SHARE MODE; 
# 排他锁
SELECT * FROM student FOR UPDATE;
# 排他锁
INSERT INTO student values ...
# 排他锁
DELETE FROM student WHERE ...
# 排他锁
UPDATE student SET ...

三、再谈隔离级别

事务有 4 个隔离级别,可能存在三种并发问题

4 中隔离级别.png

3.1、可重复读+MVCC

在MySQL中,默认的隔离级别是可重复读,可以解决脏读、不可重复读的问题,如果仅从定义的角度来看,它并不能解决幻读问题。如果想要在默认的重复读隔离级别解决幻读问题,就需要采用串行化的方式,也就是将隔离级别提升到最高,但是这样会大大降低数据库事务并发能力。为了解决幻读问题,MVCC就应用而生,MVCC可以不采用锁机制,而是通过乐观锁的方式来解决不可重复读和幻读问题!,MVCC可以在大多数情况下替代行级锁,降低系统的开销。

可重复读+MVCC.png

3.2、隐藏字段、Undo Log版本链

对于使用 InnoDB 存储引擎的表来说,它的聚簇索引记录中都包含两个必要的隐藏列trx_idroll_pointer

SELECT * FROM student;

3.3、假设之后两个事务id分别为 10 、 20 的事务对这条记录进行 UPDATE 操作 image.png

四、 MVCC实现原理之ReadView

MVCC 的实现依赖于:隐藏字段、Undo Log、Read View

4.1 什么是ReadView

在 MVCC 机制中,多个事务对同一个行记录进行更新会产生多个历史快照,这些历史快照保存在Undo Log里。如果一个事务想要查询这个行记录,需要读取哪个版本的行记录?这时就需要用到ReadView了,它帮我们解决了行的可见性问题
ReadView 就是事务在使用MVCC机制进行快照读操作时产生的读视图。当事务启动时,会生成数据库系统当前的一个快照,InnoDB为每个事务构造了一个数组,用来记录并维护系统当前活跃事务的ID(活跃指的就是,启动了但还没提交)

4.2 设计思路

使用 READ UNCOMMITTED 隔离级别的事务,由于可以读到未提交事务修改过的记录,所以直接读取记录的最新版本就好了。
使用 SERIALIZABLE 隔离级别的事务,InnoDB规定使用加锁的方式来访问记录。
使用 READ COMMITTEDREPEATABLE READ 隔离级别的事务,都必须保证读到 已经提交了的 事务修改过的记录。假如另一个事务已经修改了记录但是尚未提交,是不能直接读取最新版本的记录的,核心问题就是需要判断一下版本链中的哪个版本是当前事务可见的,这是ReadView要解决的主要问题

4.2.1 ReadView 中主要包含4个比较重要的内容

trx_ids 为 trx2、trx3、trx5和trx8的集合,系统的最大事务 ID(low_limit_id)为trx8+1(如果之前没有其他的新增事务),活跃的最小事务ID(up_limit_id)为trx2

image.png

4.3 ReadView的规则

有了这个ReadView,这样在访问某条记录时,只需要按照下边的步骤判断记录的某个版本是否可见

4.4 MVCC整体操作流程

当查询一条记录的时候,系统如何通过MVCC来寻找数据

4.4.1、举例说明
image.png

五、实战

假设 student 表中只有一条由事务 id = 8 的事务插入的一条记录

SELECT * FROM student;

5.1 READ COMMITTED隔离级别

READ COMMITTED特点 :每次读取数据前都生成一个ReadView

# Transaction 10
BEGIN;
UPDATE student SET name="李四" WHERE id=1;
UPDATE student SET name="王五" WHERE id=1;

# Transaction 20
BEGIN;
# 更新了一些别的表的记录
...
# 使用READ COMMITTED隔离级别的事务
BEGIN;

# SELECT1:Transaction 10、20未提交
SELECT * FROM student WHERE id = 1; # 得到的列name的值为'张三'
# Transaction 10
BEGIN;
UPDATE student SET name="李四" WHERE id=1;
UPDATE student SET name="王五" WHERE id=1;
COMMIT;
# Transaction 20
BEGIN;

# 更新了一些别的表的记录
...
UPDATE student SET name="钱七" WHERE id=1;
UPDATE student SET name="宋八" WHERE id=1;
# 使用READ COMMITTED隔离级别的事务
BEGIN;

# SELECT1:Transaction 10、20均未提交
SELECT * FROM student WHERE id = 1; # 得到的列name的值为'张三'

# SELECT2:Transaction 10提交,Transaction 20未提交
SELECT * FROM student WHERE id = 1; # 得到的列name的值为'王五'

5.2 REPEATABLE READ隔离级别

使用 REPEATABLE READ 隔离级别的事务来说,只会在第一次执行查询语句时生成一个ReadView,之后的查询就不会重复生成了。

5.2.1、系统里有两个 事务id 分别为 1日、2日 的事务在执行
# Transaction 10
BEGIN;
UPDATE student SET name="李四" WHERE id=1;
UPDATE student SET name="王五" WHERE id=1;

# Transaction 20
BEGIN;
# 更新了一些别的表的记录
. . .
5.2.2、此刻,表student 中 id 为 1 的记录得到的版本链表如下所示
版本链.png
5.2.3、假设现在有一个使用 REPEATABLE READ 隔离级别的事务开始执行
# 使用REPEATABLE READ隔离级别的事务
BEGIN;

# SELECT1:Transaction 10、20未提交
SELECT * FROM student WHERE id = 1; # 得到的列name的值为'张三'
5.2.4、我们把 事务id 为 10 的事务提交一下,就像这样
# Transaction 10
BEGIN;

UPDATE student SET name="李四" WHERE id=1;
UPDATE student SET name="王五" WHERE id=1;

COMMIT;
5.2.5、然后再到 事务id20 的事务中更新一下表 studentid1 的记录
# Transaction 20
BEGIN;

# 更新了一些别的表的记录
...
UPDATE student SET name="钱七" WHERE id=1;
UPDATE student SET name="宋八" WHERE id=1;
5.2.6、表student 中 id 为 1 的记录的版本链长这样 image.png
5.2.7、然后再到刚才使用 REPEATABLE READ 隔离级别的事务中继续查找这个 id 为 1 的记录
# 使用REPEATABLE READ隔离级别的事务
BEGIN;

# SELECT1:Transaction 10、20均未提交
SELECT * FROM student WHERE id = 1; # 得到的列name的值为'张三'

# SELECT2:Transaction 10提交,Transaction 20未提交
SELECT * FROM student WHERE id = 1; # 得到的列name的值仍为'张三'

5.3 如何解决幻读

假设现在表 student 中只有一条数据,数据内容中,主键id=1,隐藏的 trx_id=10,它的 undo log 如下

版本链.png
5.3.1 假设现在有事务 A事务 B 并发执行, 事务 A 的事务 id 为 20事务 B 的事务 id 为 30
select * from student where id >= 1;

在开始查询之前,MySQL 会为事务 A 产生一个 ReadView,此时 ReadView 的内容如下: trx_ids= [20,30] , up_limit_id=20 , low_limit_id=31 , creator_trx_id=20
由于此时表 student 中只有一条数据,且符合 where id>=1 条件,因此会查询出来。然后根据 ReadView机制,发现该行数据的trx_id=10,小于事务 A 的 ReadView 里 up_limit_id,这表示这条数据是事务 A 开启之前,其他事务就已经提交了的数据,因此事务 A 可以读取到。

insert into student(id,name) values(2,'李四');
insert into student(id,name) values(3,'王五');
image.png

六、总结

这里介绍了 MVCCREAD COMMITTD 、 REPEATABLE READ 这两种隔离级别的事务在执行快照读操作时访问记录的版本链的过程。这样使不同事务的 读-写 、 写-读 操作并发执行,从而提升系统性能。
核心点在于 ReadView 的原理READ COMMITTD 、 REPEATABLE READ 这两个隔离级别的一个很大不同就是生成ReadView的时机不同

说明:之前说执行`DELETE语或者更新主键的UPDATE语句并不会立即把对应的记录完全从页面中删除,而是执行一个所谓的delete mark操作,相当于只是对记录打上了一个删除标志位,这主要就是为MVCC服务的

6.1、通过 MVCC 我们可以解决

上一篇下一篇

猜你喜欢

热点阅读