MVCC

MVCC

前提条件

  • MySQL version : 5.7.17
  • MySQL 存储引擎:InnoDB

引入原因


概述

  • MVCC(Multi Version Concurrency Control)多版本并发控制
  • Multiversion two-phrase locking protocal(多版本两阶段封锁协议)
  • 两阶段锁协议,整个事务分为两个阶段,前一个阶段为加锁,后一个阶段为解锁。在加锁阶段,事务只能加锁,也可以操作数据,但不能解锁,直到事务释放第一个锁,就进入解锁阶段,此过程中事务只能解锁,也可以操作数据,不能再加锁。两阶段锁协议使得事务具有较高的并发度,因为解锁不必发生在事务结尾。它的不足是没有解决死锁的问题,因为它在加锁阶段没有顺序要求。如两个事务分别申请了A, B锁,接着又申请对方的锁,此时进入死锁状态。
  • InnoDB通过MVCC机制表示数据库某一时刻的查询快照,查询可以看该时刻之前提交的事务所做的改变,但是不能看到该时刻之后或者未提交事务所做的改变。但是,查询可以看到同一事务中之前语句所做的改变
  • 存储引擎 InnoDB 支持 MVCC
  • 一致性非阻塞读:在事务的隔离级别为可重复读,每次读取的都是事务开始时的数据快照,即读操作不加锁,所以是非阻塞读;而事务隔离级别为提交读时,可能会发生不可重复读的问题,一致性读只是对事务内部的读操作和它自己的快照而言的

实现原理

  • InnoDB有两个非常重要的模块来实现MVCC,
  • 一个是undo日志,用于记录数据的变化轨迹
  • 一个是Readview,用于判断该session对哪些数据可见,哪些不可见
  • 事务的隔离级别为可重复读时,mvcc提供基于某个时间的快照,使得对于事务看来,总是可以提供与事务开始时刻相一致的数据,而不管这个事务执行的时间有多长,故在不同事务看来,同一时刻看到的相同的行数据可能是不一样的,即:每一行数据会有多个版本数据(副本)
  • 在Mysql中MVCC是在Innodb存储引擎中得到支持的,Innodb为每行记录都实现了三个隐藏字段
  • 事务ID(DB_TRX_ID ):6字节,用来标识该行所属的事务的标识,每处理一个事务,其值自动+1;删除在内部被视为一个更新,其中行中的特殊位被设置为将其标记为已删除
  • 回滚指针(DB_ROLL_PTR):7字节,回溯查找数据历史版本,用于指向该行修改前的上一个历史版本;指向写到rollback segment(回滚段)的一条undo log记录(update操作的话,记录update前的ROW值)
  • 隐藏的ID:DB_ROW_ID: 6字节,该值随新行插入单调增加,当由innodb自动产生聚集索引时,聚集索引包括这个DB_ROW_ID的值,不然的话聚集索引中不包括这个值. 这个用于索引当中
  • 如果表中有主键或合适的唯一索引, 也就是无法生成聚簇索引的时候, InnoDB会帮我们自动生成聚集索引, 但聚簇索引会使用DB_ROW_ID的值来作为主键; 如果我们有自己的主键或者合适的唯一索引, 那么聚簇索引中也就不会包含 DB_ROW_ID 了(举例:表中用字符串做主键)
  • 聚集索引。表数据按照索引的顺序来存储的,也就是说索引项的顺序与表中记录的物理顺序一致。对于聚集索引,叶子结点即存储了真实的数据行,不再有另外单独的数据页。 在一张表上最多只能创建一个聚集索引,因为真实数据的物理顺序只能有一种
// MySQL InnoDB 存储引擎 表中每行记录会添加三个隐藏字段

    dict_table_add_system_columns(
    /*==========================*/
    dict_table_t* table, /*!< in/out: table */
    mem_heap_t* heap) /*!< in: temporary heap */
    {
    ut_ad(table);
    ut_ad(table->n_def == (table->n_cols - table->get_n_sys_cols()));
    ut_ad(table->magic_n == DICT_TABLE_MAGIC_N);
    ut_ad(!table->cached);
    /* NOTE: the system columns MUST be added in the following order
    (so that they can be indexed by the numerical value of DATA_ROW_ID,
    etc.) and as the last columns of the table memory object.
    The clustered index will not always physically contain all system
    columns.
    Intrinsic table don't need DB_ROLL_PTR as UNDO logging is turned off
    for these tables. */
    dict_mem_table_add_col(table, heap, "DB_ROW_ID", DATA_SYS,
          DATA_ROW_ID | DATA_NOT_NULL,
          DATA_ROW_ID_LEN);
    #if (DATA_ITT_N_SYS_COLS != 2)
    #error "DATA_ITT_N_SYS_COLS != 2"
    #endif
    #if DATA_ROW_ID != 0
    #error "DATA_ROW_ID != 0"
    #endif
    dict_mem_table_add_col(table, heap, "DB_TRX_ID", DATA_SYS,
          DATA_TRX_ID | DATA_NOT_NULL,
          DATA_TRX_ID_LEN);
    #if DATA_TRX_ID != 1
    #error "DATA_TRX_ID != 1"
    #endif
    if (!table->is_intrinsic()) {
    dict_mem_table_add_col(table, heap, "DB_ROLL_PTR", DATA_SYS,
          DATA_ROLL_PTR | DATA_NOT_NULL,
          DATA_ROLL_PTR_LEN);
    #if DATA_ROLL_PTR != 2
    #error "DATA_ROLL_PTR != 2"
    #endif
    /* This check reminds that if a new system column is added to
    the program, it should be dealt with here */
    #if DATA_N_SYS_COLS != 3
    #error "DATA_N_SYS_COLS != 3"
    #endif
    }
    }

事务隔离级别与MVCC

  • read uncommitted(读没有提交的数据):无法避免脏读;总是读取最新的数据行, 而不是符合当前事务版本的数据行;由于是读到未提交的,所以不存在版本的问题,无法应用MVCC;
  • read committed(只能读提交的数据):其他事务对数据库的修改,只能已提交,其修改的结果可以看见,与这2个事务开始的先后顺序无关,这个级别避免脏读,无法实现可重复读,可能会产生幻读
  • repeatable read(可重复读):只能读取在它开始之前提交事务对数据库的修改,在它开始之后,所有其他事务对数据库的修改对它来说均不可见
  • serializable(串行):读写都加锁,不会出现多个版本

示例:不同事务的隔离级别与MVCC

不同的事务隔离级别情况下A1 A2的查询结果是不同的
时间片SESSION 1SESSION 2
T1SELECT A FROM TEST; return A = 10;-
T2BEGIN ;-
T3UPDATE TEST SET A = 20 ;-
T4-BEGIN;
T5-SELECT A FROM TEST; return A1 = ? ;
T6COMMIT;-
T7-SELECT A FROM TEST; return A2 = ? ;
T8-COMMIT;
查询结果如下:
事务的隔离级别A1A2
未提交读(READ-UNCOMMITTED)2020
提交读(READ-COMMITTED)1020
可重复读(REPEATABLE-READ)1010
串行(SERIALIZABLE)1010

  • MVCC只在 READ COMMITTED 和 REPEATABLE READ 两个隔离级别下工作
  • 并且根据 read view 的生成原则, 导致在这两个不同隔离级别下, read committed 总是读最新一份快照数据, 而repeatable read 读事务开始时的行数据版本;
  • 使得 READ COMMITED 级别能够保证, 只要是当前语句执行前已经提交的数据都是可见的
  • 使得 REPEATABLE READ 级别能够保证, 只要是当前事务执行前已经提交的数据都是可见的。

Read View

  • Read View是事务开启时当前所有事务的一个集合,这个类中存储了当前Read View中最大事务ID及最小事务ID
  • Read View 也就是事务视图,它用于控制数据的可见性
  • 在InnoDB中,只有查询才需要通过Readview来控制可见性,对于DML等数据变更操作,如果操作了不可见的数据,则直接进入锁等待
  • 判断当前版本数据项是否可见
  • 在innodb中, 每创建一个新事务, 存储引擎都会将当前系统中的活跃事务列表(trx_sys->trx_list)创建一个副本(read view), 副本中保存的是系统中当前不应该被本事务看到的其他事务id列表
  • 当用户在事务中要读取某行记录的时候, innodb会将该行当前的版本号与该read view进行比较
  • 主要组成:
  • ReadView::id 创建该视图的事务ID;
  • ReadView::m_ids 创建ReadView时,活跃的读写事务ID数组,有序存储;
  • ReadView::m_low_limit_id 设置为当前最大事务ID;
  • ReadView::m_up_limit_id m_ids集合中的最小值,如果m_ids集合为空,表示当前没有活跃读写事务,则设置为当前最大事务ID。
  • READ-COMMITTED (提交读):事务内的每个查询语句都会重新创建Read View,这样就会产生不可重复读现象发生
  • REPEATABLE-READ (可重复读):事务内开始时创建Read View , 在事务结束这段时间内 每一次查询都不会重新重建Read View , 从而实现了可重复读。

当前数据行记录的可见性

  • 聚集索引的可见性判断和二级索引的可见性判断略有不同。因为二级索引记录并没有存储事务ID信息,相应的,只是在数据页头存储了最近更新该page的trx_id
  • 如果记录(记录指的是当前的查询结果中的行,每行记录InnoDB都隐藏了DB_TRX_ID 即事务ID)的trx_id小于ReadView::m_up_limit_id,则说明该事务在创建ReadView时已经提交了,肯定可见;
  • 如果记录的trx_id大于等于ReadView::m_low_limit_id,则说明该事务是创建readview之后开启的,肯定不可见;
  • 当trx_id在m_up_limit_id和m_low_limit_id之间时,如果在ReadView::m_ids数组中,说明创建readview时该事务是活跃的,其做的变更对当前视图不可见,否则对该trx_id的变更可见。
  • 如果当前记录不可见,则判断该记录是否有上一个版本(DB_ROLL_PTR回滚指针的指向是否为空),判断前上一个版本的记录是否可见;

MVCC-READ-VIEW 可见性判断


MVCC SQL-DML操作

行的插入更新过程

  • 1.初始化数据

初始化数据

F1~F6是某行列的名字,1~6是其对应的数据。后面三个隐含字段分别对应该行的事务号和回滚指针,假如这条数据是刚INSERT的,可以认为ID为1,其他两个字段为空。

  • 2.事务1更改该行的各字段的值

事务1更改该行的各字段的值

当事务1更改该行的值时,会进行如下操作:
用排他锁锁定该行
把该行修改前的值Copy到undo log,即上图中下面的行
修改当前行的值,填写事务编号,使回滚指针指向undo log中的修改前的行
记录redo log

  • 3.事务2修改该行的值

事务2修改该行的值
与事务1相同,此时undo log,中有有两行记录,并且通过回滚指针连在一起。
因此,如果undo log一直不删除,则会通过当前记录的回滚指针回溯到该行创建时的初始内容,所幸的时在Innodb中存在purge线程,它会查询那些比现在最老的活动事务还早的undo log,并删除它们,从而保证undo log文件不至于无限增长。


优缺点

  • 优点:在读取数据时,innodb几乎不用获取任何锁,在每个查询通过版本检查,只获取需要的数据版本,提高系统并发度
  • 缺点:为了实现多版本,innodb必须对每行增加相应字段来存储版本信息,同时需要维护每一行的版本信息,而且在检索行的时候,需要进行版本的比较,因而减低了查询效率;innodb还需要定期清理不再需要的行版本,及时回收空间,这也增加开销;

参考资料


待整理内容:

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值