InnoDB中的MVCC

InnoDB中的MVCC

1 MVCC概述

MVCC,全称 Multi-Version Concurrency Control,即多版本并发控制。我们知道数据库并发场景有3种:

  1. 读-读:不存在任何问题,也不需要并发控制

  2. 读-写:有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读

  3. 写-写:有线程安全问题,可能会存在更新丢失问题,比如第一类更新丢失,第二类更新丢失

为了在这些场景中实现事务的隔离性,避免脏读、脏写、幻读、不可重复读等现象,一种常规的操作就是加悲观锁,让事务串行执行。但是这种方式会大大削弱系统的并发能力。

多版本并发控制(MVCC)就是一种用来解决读-写冲突的无锁并发控制 。MVCC在MySQL InnoDB中的实现主要是为了提高数据库并发性能,用更好的方式去处理读-写冲突,做到即使有读写冲突时,也能做到不加锁非阻塞并发读

2 MVCC实现原理

MVCC的目的就是多版本并发控制,在数据库中的实现,就是为了解决读写冲突,它的实现原理主要是依赖记录中的 2个隐式字段undo日志Read View 来实现的。

在内部实现中,InnoDB 通过数据行的 DB_TRX_IDRead View 来判断数据的可见性,如不可见,则通过数据行的 DB_ROLL_PTR 找到 undo log 中的历史版本。每个事务读到的数据版本可能是不一样的,在同一个事务中,用户只能看到该事务创建 Read View 之前已经提交的修改和该事务本身做的修改

2.1 隐藏字段

对于InnoDB来说,一张表除了我们定义的字段外,数据库会自动添加一些隐藏字段,其中就包括:

  1. DB_TRX_ID:表示最后一次插入或更新该行的事务id,每一次改动都会把该事务的id赋值给trix_id隐藏列。
  2. DB_ROLL_POINTER:回滚指针,我们知道为了实现原子性,数据库依赖于undo log进行回滚。每一条更新语句都会被记录在回滚日志中,而该指针就是指向更新改行的undo log。

下面举个例子:

  1. 比如一个有个事务插入person表插入了一条新记录,记录如下,name为Jerry, age为24岁,隐式主键是1,事务ID和回滚指针,我们假设为NULL

  2. 现在来了一个事务1对该记录的name做出了修改,改为Tom

    • 在事务1修改该行(记录)数据时,数据库会先对该行加排他锁

    • 然后把该行数据拷贝到undo log中,作为旧记录,即在undo log中有当前行的拷贝副本

    • 拷贝完毕后,修改该行name为Tom,并且修改隐藏字段的事务ID为当前事务1的ID, 我们默认从1开始,之后递增,回滚指针指向拷贝到undo log的副本记录,既表示我的上一个版本就是它

    • 事务提交后,释放锁

  3. 又来了个事务2修改person表的同一个记录,将age修改为30岁

    操作和步骤2类似,加锁,写入undo log,修改,提交事务,释放锁。

从上面,我们就可以看出,不同事务或者相同事务的对同一记录的修改,会导致该记录的undo log成为一条记录版本线性表 ,也就是我们常说的版本链。我们之后会利用这个记录的版本链来控制并发事务访问相同记录时的行为,我们把这种机制称之为多版本并发控制。

2.2 Read View

我们知道事务的隔离级别有4个,对于READ UNCOMMITTED 隔离级别的事务来说,由于可以读到未提交事务修改过的记录,所以直接读取记录的最新版本就好了; 对于使用SERIALIZABLE 隔离级别的事务来说, 设计lnnoDB 的大叔规定使用加锁的方式来访问记录;对于使用READ COMMITTED 和REPEATABLE READ 隔离级别的事务来说,都必须保证读到已经提交的事务修改过的记录. 也就是说假如另一个事务已经修改了记录但是尚未提交,则不能直接读取最新版本的记录。为此,设计lnnoDB 的大叔提出了ReadView (有的地方翻译成“一致性视图”)的概念。

Read View就是事务进行快照读操作的时候生产的读视图(Read View),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID(当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以最新的事务,ID值越大)

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

  • m_low_limit_id:目前出现过的最大的事务 ID+1,即下一个将被分配的事务 ID。大于等于这个 ID 的数据版本均不可见
  • m_up_limit_id:活跃事务列表 m_ids 中最小的事务 ID,如果 m_ids 为空,则 m_up_limit_idm_low_limit_id。小于这个 ID 的数据版本均可见
  • m_idsRead View 创建时其他未提交的活跃事务 ID 列表。创建 Read View时,将当前未提交事务 ID 记录下来,后续即使它们修改了记录行的值,对于当前事务也是不可见的。m_ids 不包括当前事务自己和已提交的事务(正在内存中)
  • m_creator_trx_id:创建该 Read View 的事务 ID

有了这个ReadView后,在访问某条记录时, 只需要对比该版本的trx_id,即可判断是否可见:

如果某个版本的数据对当前事务不可见,那就顺着版本链找到下一个版本的数据,并继续判断记录的可见性:依此类推,直到版本链中的最后一个版本.如果记录的最后一个版本也不可见,就意味着该条记录对当前事务完全不可见, 查询结果就不包含该记录。

3 其他问题

3.1 RC和RR下MVCC的差异

在事务隔离级别 RCRR (InnoDB 存储引擎的默认事务隔离级别)下,InnoDB 存储引擎使用 MVCC(非锁定一致性读),但它们生成 Read View 的时机却不同 :

  • 在 RC 隔离级别下的 每次select 查询前都生成一个Read View (m_ids 列表)
  • 在 RR 隔离级别下只在事务开始后 第一次select 数据前生成一个Read View(m_ids 列表)

3.2 二级索引和MVCC

我们知道, 只有在聚簇索号问录中才有trx_id 和roll_pointer 隐藏列。如果某个查询语句是使用二级索引来执行查询的, 该如何判断可见性呢?比如SELECT name FROM hero WHERE name = ‘刘备’,大致可以分成2步:

  1. 二级索引页面的Page Header中有一个名为PAGE_MAX_TRX_ID的属性,每当对该页面中的记录执行增删改操作时,如果执行该操作的事务的事务id 大于PAGE_MAX_TRX_ID属性值,就会把PAGE_MAX_TRX_ID属性设置为执行该操作的事务的事务id。这也就意味着PAGE_MAX_TRX_ID 属性值代表着修改该二级索引页面的最大事务id是什么。当select语句访问某个二级索引记录时,首先会看一下对应的ReadView的m_up_limit_id是否大于该页面的PAGE_MAX_TRX_ID属性值。如果是,说明该页面中的所有记录都对该ReadView可见; 否则就得执行步骤2 。在回表之后再判断可见性。
  2. 利用二级索引记录中的主键值进行回表操作, 得到对应的聚簇索引记录后再按照前面讲过的方式找到对该ReadView可见的第一个版本,然后判断该版本中相应的二级索引列的值是否与利用该二级索引查询时的值相同。本例中就是判断找到的第个可见版本的name 值是不是“刘备”。如果是, 就把这条记录发送给客户端(如果WHERE 子句中还有其他搜索条件的话还需继续判断)), 否则就跳过该记录。
打赏
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2021-2022 Yin Peng
  • 引擎: Hexo   |  主题:修改自 Ayer
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信