MySQL 之多版本并发控制 MVCC

MySQL 之多版本并发控制 MVCC

  • 1、MVCC 中的两种读取方式
    • 1.1、快照读
    • 1.2、当前读
  • 2、MVCC实现原理之 ReadView
    • 2.1、隐藏字段
    • 2.2、ReadView
    • 2.3、读已提交和可重复读隔离级别下,产生 ReadView 时机的区别
  • 3、MVCC 解决幻读
  • 4、总结

       MVCC(多版本并发控制) 没有正式的标准,在不同的 DBMS 中MVCC的实现方式可能不同,本文中讲解的是 InnoDB 中 MVCC 的实现机制(MySQL 其它的存储引擎并不支持 MVCC).

 

1、MVCC 中的两种读取方式

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

 

1.1、快照读

 
快照读(又叫一致性读),读取的是快照数据,即不加锁的非阻塞读。不加锁的简单 select 都属于快照读。快照读的实现是基于 MVCC(多版本并发控制),在很多情况下,快照读避免了加锁操作,降低了开销。

  • 由于快照读基于 MVCC(多版本并发控制),所以快照读可能读到的并不一定是数据的最新版本,很有可能读的是之前的历史版本数据。
  • 快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读。

 

1.2、当前读

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

# 共享锁
select * from tb_table lock in share mode;
# 排它锁
select * from tb_table for update;
# 排它锁
insert into tb_table values# 排它锁
delete from tb_table where# 排他锁
update tb_table set

 

2、MVCC实现原理之 ReadView

 
MVCC 的实现依赖于:隐藏字段、undo log、ReadView.

 

2.1、隐藏字段

 
对于InnoDB引擎来说,每个行记录除了记录本身的数据之外,还存在几个隐藏的列。

  • DB_ROW_ID :如果没有为表显式的定义主键,并且表中也没有定义唯一索引,那么InnoDB会自动为表添加一个 row_id 的隐藏列作为主键。
  • DB_TRX_ID :每个事务都会分配一个事务ID,当对某条记录发生变更时,就会将这个事务的事务ID写入 trx_id 中。
  • DB_ROLL_PTR :回滚指针,本质上就是指向 undo log 的指针。
    MySQL 之多版本并发控制 MVCC_第1张图片

2.2、ReadView

 
       在 MVCC 机制中,多个事务对同一行记录A,进行更新会产生多个历史快照,这个历史快照保存在 undo log 中。当一个事务想要查询 记录A 这行数据时,需要读取哪个版本的记录将是一个问题。ReadView 就可以解决以上 行可见性问题。

在读未提交的隔离级别下事务,查询的数据都是最新版本,所以不需要MVCC。在串行化隔离级别下的事务,InnoDB 规定使用加锁的方式来访问记录,也不需要MVCC。

ReadView 就是事务在使用 MVCC 机制进行快照读操作时产生的读视图。ReadView 主要包含了以下4个重要内容:

  • creator_trx_id,创建该 ReadView 的事务ID.

    只要在对表中的记录做改动时(执行 INSERT、DELETE、UPDATE 这些语句时)才会为事务分配事务id,否则在一个只读事务中的事务id只都默认为0.

  • trx_ids,表示生成 ReadView 时,当前系统中活跃的读写事务的 事务id列表

  • up_limit_id,活跃的事务中最小的事务ID.

  • low_limit_id,表示生成 ReadView 时系统中应该分配给下一个事务的 id 值。low_limit_id 是系统最大的事务ID值.

    low_limit_id 并不是trx_ids 中的最大值,事务ID是递增分配的。比如:现在有ID为1、2、3这三个事务,之后ID为3的事务提交了,那么一个新的读事务在生成 ReadView 时,trx_ids 就包括1 和2,up_limit_id 的值就是1,low_limit_id的值就是4.

ReadView 的规则:在访问某条记录时,需要按照以下步骤(ReadView 的规则)判断记录的某个版本是否可见

  • 如果被访问版本的 trx_id(db_trx_id) 属性值与 ReadView 中的 creator_trx_id 值相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问。
  • 如果被访问版本的 trx_id(db_trx_id) 属性值小于 ReadView 中的 up_limit_id 值,表明生成该版本的事务在当前事务生成 ReadView 前已经提交,所以该版本可以被当前事务访问。
  • 如果被访问版本的 trx_id(db_trx_id) 属性值大于或等于 ReadView 中的 low_limit_id 值,表明生成该版本的事务在当前事务生成 ReadView 后才开启,所以该版本不可以被当前事务访问。
  • 如果被访问版本的 trx_id(db_trx_id) 属性值在 ReadView 的 up_limit_id 和 low_limit_id 之间,就需要判断一下 trx_id 属性值是否在 trx_ids 列表中。
  • 如果在,说明创建 ReadView 时,生成该版本的事务还是活跃的,该版本不可以被访问。
  • 如果不在,说明创建 ReadView时,生成该版本的事务已经被提交,该版本可以被访问。

MVCC 整体操作流程:

查询一条记录时,系统通过MVCC机制查找记录的流程:

  • ① 首先获取事务自己的版本号,也就是事务ID.
  • ② 获取 ReadView.
  • ③ 查询得到的数据,然后与 ReadView 中的事务版本号进行比较。
  • ④ 如果不符合 ReadView 规则,就需要从 undo log 中获取历史快照。
  • ⑤ 最后返回符合规则的数据。

       如果某个版本的数据对当前事务不可见,那就顺着版本链找到下一个版本的数据,继续按照上边的步骤判断可见性,依此类推,直到版本链中的最后一个版本。

InnoDB 中,MVCC 是通过 Undo Log + ReadView 进行数据读取, Undo Log 保存了历史快照,而 Read View 规则帮我们判断当前版本的数据是否可见。

 

2.3、读已提交和可重复读隔离级别下,产生 ReadView 时机的区别

 

  • 在隔离级别为读已提交(Read Commit)时,一个事务中的每一次 SELECT 查询都会重新获取一次 ReadView.

    步骤 事务 说明
    1 begin;
    2 select * from tb_table where id > 2; 获取一次 ReadView
    3 ……
    4 select * from tb_table where id > 2; 获取一次 ReadView
    5 commit;
    • 步骤2和步骤4,一样的查询语句,在读已提交的隔离级别下,会分别获取一次 ReadView,如果两次的ReadView 不同,就可能产生不可重复读或者幻读的情况。
  • 当隔离级别为可重复读的时候,一个事务只在第一次 SELECT 时会获取一次 ReadView,而后面所有的 SELECT 都会复用这个 ReadView,所以可以避免不可重复读。
    MySQL 之多版本并发控制 MVCC_第2张图片

 

3、MVCC 解决幻读

 
       使用例子说明 InnoDB 如何解决幻读,以下示例是在InnoDB引擎的可重复读隔离级别下讨论。现在 tb_table 表中已存在一条 主键id = 1 的数据,该条记录隐藏的 db_trx_id = 10。

db_trx_id = 10 数据:id = 1,name = 张三 NULL

假设现有 事务A 和 事务B 并发执行,事务A 的事务id 为 20,事务B 的事务id 为 30.

  • 步骤 1:事务A 开始第一次查询数据。

    – 事务A 开始第一次查询数据

    select * from tb_table where id >=1;
     
    ① --> 在开始查询前,MySQL 会为事务A产生一个 ReadView,此时 ReadView 的内容如下:trx_ids = [20,30],up_limit_id = 20,low_limit_id = 31,creator_trx_id = 20.
     
    ② --> 执行select * from tb_table where id >=1;查出id=1,db_trx_id= 10的数据,按ReadView机制判断该行数据是否可见。
     
    ③ --> 按 ReadView 机制 该查询出的数据 db_trx_id = 10 ,小于事务A的ReadView 的up_limit_id,故该数据在事务A 开启之前,其他事务已提交,所以该行数据可见。
     
    结论:事务A 的第一次查询,能读到一条数据,id=1,name= 张三。

  • 步骤2:接着事务B(事务id=30),往tb_table 插入两条数据,并提交事务(假设不考虑 gap lock)。

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

    此时 tb_table 中就要三条数据了,对应的 undo log 如下。
    MySQL 之多版本并发控制 MVCC_第3张图片

  • 步骤3:接着事务A开启第二次查询,由于在可重复读隔离级别下,事务A不会再次生成ReadView.

    – 事务A 开始第二次查询数据

    select * from tb_table where id >=1;
     
    ① --> 此时tb_table中的3条数据都满足 id>=1 的条件,因此会先查出来,然后根据 ReadView 机制,判断每条数据是不是都可以被事务A可见。
     
    ② --> 首先 id = 1 这条数据同步骤1,对事务A可见。
     
    ③ --> 然后是 id = 2 的数据,它的 trx_id = 30,介于 ReadView 的 up_limit_id = 20 和 low_limit_id = 31 之间,需要再次判断 trx_id = 30 是否处于 ReadView 的 trx_ids=[20,30] 数值内。结果在trx_ids数组内,表示 id= 2 这条数据是与事务A 在同一时刻启动的其他事务提交的,所以这条数据不能被事务A可见。
     
    ④ --> 同理,id = 3 这条数据,trx_id 也为 30,因此也不能被事务A 可见。
     
    结论:最终事务A 的第二次查询,只能查询出 id = 1 的这条数据,这和事务A 的第一次查询的结果一样,因此没有出现不可重复读现象

    MySQL 之多版本并发控制 MVCC_第4张图片

注意只靠MVCC不能完全解决幻读问题。虽然MVCC可以提供事务的一致性视图,并保持数据的一致性版本,但它本身并不能防止其他事务插入新的数据。通过结合 next-key locking 和 MVCC,InnoDB 在可重复读隔离级别下解决了幻读问题,确保了事务在多次读取相同范围数据时的一致性

      间隙锁是在 innodb的 可重复读级别才会生效,且 Next-Key Locks 实际上是由间隙锁加行锁实现的MySQL 在 InnoDB 存储引擎下,可重复读的隔离级别下,不存在幻读问题
 

4、总结

 
MVCC(多版本并发控制)的核心点在于 ReadView 的原理,READ COMMITTED、REPEATABLE READ 这两个隔离级别的一个很大不同就是生成 ReadView 的时机不同:

  • READ COMMITTED每一次进行普通 select 操作前,都会生成一个 ReadView.
  • REPEATABLE READ 只在第一次进行普通 select 操作前生成一个 ReadView,之后的查询操作都重复使用这个 ReadView 就好了。

 

MVCC(多版本并发控制)解决的问题

  • 读写之间阻塞的问题。通过 MVCC 可以让读写相互不阻塞,即读不阻塞写,写不阻塞读,可以提升事务并发处理能力。
  • 降低了死锁的概率。因为 MVCC 采用了乐观锁的方式,读取数据时并不需要加锁,对于写操作,也只锁定必要的行。
  • 解决快照读的问题。当查询数据库在某个时间点的快照时,只能看到这个时间点之前事务提交更新的结果,而不能看到这个时间点之后事务提交的更新结果。

 

 

你可能感兴趣的:(MySQL,mvcc,多版本并发控制,innodb解决幻读,mysql幻读,readview,快照读,当前读)