详解MySQL多版本并发控制机制(MVCC)源码


Posted in MySQL onJune 23, 2021
目录
  • 一、前言
  • 二、MVCC(多版本并发控制机制)
    • 2.1、Repeatable Read
    • 2.2、Read Commit
    • 2.3、MVCC的优势
  • 三、MVCC(实现机制)
    • 3.1、select运行栈
    • 3.2、read_view的创建过程
    • 3.3、行版本可见性
    • 3.4、undolog搜索可见版本的过程
    • 3.5、read_view创建时机再讨论
  • 四、MVCC和锁的同时作用导致的一些现象
  • 五、总结

 

一、前言

作为一个数据库爱好者,自己动手写过简单的SQL解析器以及存储引擎,但感觉还是不够过瘾。<<事务处理-概念与技术>>诚然讲的非常透彻,但只能提纲挈领,不能让你玩转某个真正的数据库。感谢cmake,能够让我在mac上用xcode去debug MySQL,从而能去领略它的各种实现细节。

(注:本文的MySQL采用的是MySQL-5.6.35版本)

 

二、MVCC(多版本并发控制机制)

隔离性也可以被称作并发控制、可串行化等。谈到并发控制首先想到的就是锁,MySQL通过使用两阶段锁的方式实现了更新的可串行化,同时为了加速查询性能,采用了MVCC(Multi Version Concurrency Control)的机制,使得不用锁也可以获取一致性的版本。

 

2.1、Repeatable Read

MySQL的通过MVCC以及(Next-Key Lock)实现了可重复读(Repeatable Read),其思想(MVCC)就是记录数据的版本变迁,通过精巧的选择不同数据的版本从而能够对用户呈现一致的结果。如下图所示:

详解MySQL多版本并发控制机制(MVCC)源码

上图中,(A=50|B=50)的初始版本为1。

1.事务t1在select A时候看到的版本为1,即A=50

2.事务t2对A和B的修改将版本升级为2,即A=0,B=100

3.事务t1再此select B的时候看到的版本还是1, 即B=50

这样就隔离了版本的影响,A+B始终为100。

 

2.2、Read Commit

而如果不通过版本控制机制,而是读到最近提交的结果的话,则隔离级别是read commit,如下图所示:

详解MySQL多版本并发控制机制(MVCC)源码

在这种情况下,就需要使用锁机制(例如select for update)将此A,B记录锁住,从而获得正确的一致结果,如下图所示:

详解MySQL多版本并发控制机制(MVCC)源码

 

2.3、MVCC的优势

当我们要对一些数据做一些只读操作来检查一致性,例如检查账务是否对齐的操作时候,并不希望加上对性能损耗很大的锁。这时候MVCC的一致性版本就有很大的优势了。

 

三、MVCC(实现机制)

本节就开始谈谈MVCC的实现机制,注意MVCC仅仅在纯select时有效(不包括select for update,lock in share mode等加锁操作,以及update\insert等)。

 

3.1、select运行栈

首先我们追踪一下一条普通的查询sql在mysql源码中的运行过程,sql为(select * from test);

详解MySQL多版本并发控制机制(MVCC)源码

其运行栈为:

handle_one_connection  MySQL的网络模型是one request one thread

 |-do_handle_one_connection

|-do_command

|-dispatch_command

|-mysql_parse 解析SQL

|-mysql_execute_command

|-execute_sqlcom_select 执行select语句

|-handle_select

...一堆parse join 等的操作,当前并不关心

|-*tab->read_record.read_record 读取记录

由于mysql默认隔离级别是repeatable_read(RR),所以read_record重载为
rr_sequential(当前我们并不关心select通过index扫描出row之后再通过condition过滤的过程)。继续追踪:

read_record

 |-rr_sequential

|-ha_rnd_next

|-ha_innobase::rnd_next 这边就已经到了innodb引擎了

|-general_fetch

|-row_search_for_mysql

|-lock_clust_rec_cons_read_sees 这边就是判断并选择版本的地方

让我们看下该函数内部:

bool lock_clust_rec_cons_read_sees(const rec_t* rec /*由innodb扫描出来的一行*/,....){
	...
	// 从当前扫描的行中获取其最后修改的版本trx_id(事务id)
	trx_id = row_get_rec_trx_id(rec, index, offsets);
	// 通过参数(一致性快照视图和事务id)决定看到的行快照
	return(read_view_sees_trx_id(view, trx_id));
}

 

3.2、read_view的创建过程

我们先关注一致性视图的创建过程,我们先看下read_view结构:

struct read_view_t{
	// 由于是逆序排列,所以low/up有所颠倒
	// 能看到当前行版本的高水位标识,>= low_limit_id皆不能看见
	trx_id_t	low_limit_id;
	// 能看到当前行版本的低水位标识,< up_limit_id皆能看见
	trx_id_t	up_limit_id;
	// 当前活跃事务(即未提交的事务)的数量
	ulint		n_trx_ids;
	// 以逆序排列的当前获取活跃事务id的数组
	// 其up_limit_id<tx_id<low_limit_id
	trx_id_t*	trx_ids;	
	// 创建当前视图的事务id
	trx_id_t	creator_trx_id;
	// 事务系统中的一致性视图链表
	UT_LIST_NODE_T(read_view_t) view_list;
};

然后通过debug,发现创建read_view结构也是在上述的rr_sequential中操作的,继续跟踪调用栈:

rr_sequential

 |-ha_rnd_next

  |-rnd_next

  |-index_first 在start_of_scan为true时候走当前分支index_first

  |-index_read

  |-row_search_for_mysql

  |-trx_assign_read_view

我们看下row_search_for_mysql里的一个分支:

row_search_for_mysql:
// 这边只有select不加锁模式的时候才会创建一致性视图
else if (prebuilt->select_lock_type == LOCK_NONE) {		// 创建一致性视图
		trx_assign_read_view(trx);
		prebuilt->sql_stat_start = FALSE;
}

上面的注释就是select for update(in share model)不会走MVCC的原因。让我们进一步分析trx_assign_read_view函数:

trx_assign_read_view

 |-read_view_open_now

  |-read_view_open_now_low

好了,终于到了创建read_view的主要阶段,主要过程如下图所示:

详解MySQL多版本并发控制机制(MVCC)源码

代码过程为:

static read_view_t* read_view_open_now_low(trx_id_t	cr_trx_id,mem_heap_t*	heap)
{
	read_view_t*	view;
	// 当前事务系统中max_trx_id(即尚未被分配的trx_id)设置为low_limit_no
	view->low_limit_no = trx_sys->max_trx_id;
	view->low_limit_id = view->low_limit_no;
	// CreateView构造函数,会将非当前事务和已经在内存中提交的事务给剔除,即判断条件为
	// trx->id != m_view->creator_trx_id&& !trx_state_eq(trx, TRX_STATE_COMMITTED_IN_MEMORY)的
	// 才加入当前视图列表
	ut_list_map(trx_sys->rw_trx_list, &trx_t::trx_list, CreateView(view));
	if (view->n_trx_ids > 0) {
		// 将当前事务系统中的最小id设置为up_limit_id,因为是逆序排列
		view->up_limit_id = view->trx_ids[view->n_trx_ids - 1];
	} else {
		// 如果当前没有非当前事务之外的活跃事务,则设置为low_limit_id
		view->up_limit_id = view->low_limit_id;
	}
	// 忽略purge事务,purge时,当前事务id是0
	if (cr_trx_id > 0) {
		read_view_add(view);
	}
	// 返回一致性视图
	return(view);
}

 

3.3、行版本可见性

由上面的lock_clust_rec_cons_read_sees可知,行版本可见性由read_view_sees_trx_id函数判断:

/*********************************************************************//**
Checks if a read view sees the specified transaction.
@return	true if sees */
UNIV_INLINE
bool
read_view_sees_trx_id(
/*==================*/
	const read_view_t*	view,	/*!< in: read view */
	trx_id_t		trx_id)	/*!< in: trx id */
{
	if (trx_id < view->up_limit_id) {

		return(true);
	} else if (trx_id >= view->low_limit_id) {

		return(false);
	} else {
		ulint	lower = 0;
		ulint	upper = view->n_trx_ids - 1;

		ut_a(view->n_trx_ids > 0);

		do {
			ulint		mid	= (lower + upper) >> 1;
			trx_id_t	mid_id	= view->trx_ids[mid];

			if (mid_id == trx_id) {
				return(FALSE);
			} else if (mid_id < trx_id) {
				if (mid > 0) {
					upper = mid - 1;
				} else {
					break;
				}
			} else {
				lower = mid + 1;
			}
		} while (lower <= upper);
	}

	return(true);
}

其实上述函数就是一个二分法,read_view其实保存的是当前活跃事务的所有事务id,如果当前行版本对应修改的事务id不在当前活跃事务里面的话,就返回true,表示当前版本可见,否则就是不可见,如下图所示。

详解MySQL多版本并发控制机制(MVCC)源码

接上述lock_clust_rec_cons_read_sees的返回:

if (UNIV_LIKELY(srv_force_recovery < 5)
			    && !lock_clust_rec_cons_read_sees(
				    rec, index, offsets, trx->read_view)){
	// 当前处理的是当前版本不可见的情况
	// 通过undolog来返回到一致的可见版本
	err = row_sel_build_prev_vers_for_mysql(
					trx->read_view, clust_index,
					prebuilt, rec, &offsets, &heap,
					&old_vers, &mtr);			    
} else{
	// 可见,然后返回
}

 

3.4、undolog搜索可见版本的过程

我们现在考察一下row_sel_build_prev_vers_for_mysql函数:

row_sel_build_prev_vers_for_mysql

 |-row_vers_build_for_consistent_read

主要是调用了row_ver_build_for_consistent_read方法返回可见版本:

dberr_t row_vers_build_for_consistent_read(...)
{
	......
	for(;;){
		err = trx_undo_prev_version_build(rec, mtr,version,index,*offsets, heap,&prev_version);
		......
		trx_id = row_get_rec_trx_id(prev_version, index, *offsets);
		// 如果当前row版本符合一致性视图,则返回
		if (read_view_sees_trx_id(view, trx_id)) {
			......
			break;
		}
		// 如果当前row版本不符合,则继续回溯上一个版本(回到for循环的地方)
		version = prev_version;
	}
	......
}

整个过程如下图所示:

详解MySQL多版本并发控制机制(MVCC)源码

至于undolog怎么恢复出对应版本的row记录就又是一个复杂的过程了,由于篇幅原因,在此略过不表。

 

3.5、read_view创建时机再讨论

在创建一致性视图的row_search_for_mysql的代码中

// 只有非锁模式的select才创建一致性视图
else if (prebuilt->select_lock_type == LOCK_NONE) {		// 创建一致性视图
		trx_assign_read_view(trx);
		prebuilt->sql_stat_start = FALSE;
}

trx_assign_read_view中由这么一段代码

// 一致性视图在一个事务只创建一次
if (!trx->read_view) {
		trx->read_view = read_view_open_now(
			trx->id, trx->global_read_view_heap);
		trx->global_read_view = trx->read_view;
	}

所以综合这两段代码,即在一个事务中,只有第一次运行select(不加锁)的时候才会创建一致性视图,如下图所示:

详解MySQL多版本并发控制机制(MVCC)源码

笔者构造了此种场景模拟过,确实如此。

 

四、MVCC和锁的同时作用导致的一些现象

MySQL是通过MVCC和二阶段锁(2PL)来兼顾性能和一致性的,但是由于MySQL仅仅在select时候才创建一致性视图,而在update等加锁操作的时候并不做如此操作,所以就会产生一些诡异的现象。如下图所示:

详解MySQL多版本并发控制机制(MVCC)源码

如果理解了update不走一致性视图(read_view),而select走一致性视图(read_view),就可以很好解释这个现象。
如下图所示:

详解MySQL多版本并发控制机制(MVCC)源码

 

五、总结

MySQL为了兼顾性能和ACID使用了大量复杂的机制,2PL(两阶段锁)和MVCC就是其实现的典型。幸好可以通过xcode等IDE进行方便的debug,这样就可以非常精确加便捷的追踪其各种机制的实现。希望这篇文章能够帮助到喜欢研究MySQL源码的读者们。

以上就是详解MySQL多版本并发控制机制(MVCC)源码的详细内容,更多关于MySQL 并发控制机制 MVCC的资料请关注三水点靠木其它相关文章!

MySQL 相关文章推荐
mysql 8.0.24版本安装配置方法图文教程
May 12 MySQL
详解GaussDB for MySQL性能优化
May 18 MySQL
MySQL 常见存储引擎的优劣
Jun 02 MySQL
MySQL分库分表详情
Sep 25 MySQL
mysql 获取时间方式
Mar 20 MySQL
MySQL学习必备条件查询数据
Mar 25 MySQL
mysql的Buffer Pool存储及原理
Apr 02 MySQL
MySQL库表太大怎么办? 数据库分库分表项目实践
Apr 11 MySQL
MySQL数据库Innodb 引擎实现mvcc锁
May 06 MySQL
Mysql数据库事务的脏读幻读及不可重复读详解
May 30 MySQL
MySQL详解进行JDBC编程与增删改查方法
Jun 16 MySQL
MySQL控制流函数(-if ,elseif,else,case...when)
Jul 07 MySQL
MySQL快速插入一亿测试数据
MySQL8.0.18配置多主一从
Jun 21 #MySQL
MySQL中日期型单行函数代码详解
如何搭建 MySQL 高可用高性能集群
Jun 21 #MySQL
MySQL 发生同步延迟时Seconds_Behind_Master还为0的原因
Jun 21 #MySQL
分析mysql中一条SQL查询语句是如何执行的
MySQL如何使用使用Xtrabackup进行备份和恢复
Jun 21 #MySQL
You might like
PHP输出控制功能在简繁体转换中的应用
2006/10/09 PHP
PHP开发负载均衡指南
2010/07/17 PHP
php截取后台登陆密码的代码
2012/05/05 PHP
PHP中的gzcompress、gzdeflate、gzencode函数详解
2014/07/29 PHP
100多行PHP代码实现socks5代理服务器[2]
2016/05/05 PHP
PHP递归遍历文件夹去除注释并压缩php源代码的方法示例
2018/05/23 PHP
用Laravel Sms实现laravel短信验证码的发送的实现
2018/11/29 PHP
php如何实现数据库的备份和恢复
2020/11/30 PHP
javascript 匿名函数的理解(透彻版)
2010/01/28 Javascript
js实时监听文本框状态的方法
2011/04/26 Javascript
jquery中选择块并改变属性值的方法
2013/07/31 Javascript
js数组循环遍历数组内所有元素的方法
2014/01/18 Javascript
使用RequireJS优化JavaScript引用代码的方法
2015/07/01 Javascript
JavaScript严格模式详解
2015/11/18 Javascript
JS实现的适合做faq或menu滑动效果示例
2016/11/17 Javascript
使用 Node.js 对文本内容分词和关键词抽取
2017/05/27 Javascript
js截取字符串功能的实现方法
2017/09/27 Javascript
深入理解ES6之数据解构的用法
2018/01/13 Javascript
vue iview组件表格 render函数的使用方法详解
2018/03/15 Javascript
Vue表单类的父子组件数据传递示例
2018/05/03 Javascript
使用Python的Scrapy框架编写web爬虫的简单示例
2015/04/17 Python
Pandas之排序函数sort_values()的实现
2019/07/09 Python
在Python中使用filter去除列表中值为假及空字符串的例子
2019/11/18 Python
原生 JS+CSS+HTML 实现时序图的方法
2019/07/31 HTML / CSS
英国工艺品购物网站:Minerva Crafts
2018/01/29 全球购物
销售行业个人求职自荐信
2013/09/25 职场文书
集团公司人力资源部岗位职责
2014/01/03 职场文书
高校教师自荐信范文
2014/03/13 职场文书
市级三好学生事迹材料
2014/08/27 职场文书
群众路线剖析材料
2014/09/30 职场文书
村支部书记群众路线对照检查材料思想汇报
2014/10/08 职场文书
毕业生就业推荐表导师评语
2014/12/31 职场文书
大连星海广场导游词
2015/02/10 职场文书
雷锋电影观后感
2015/06/10 职场文书
企业反腐倡廉心得体会
2015/08/15 职场文书
Java面试题冲刺第十六天--消息队列
2021/08/07 面试题