详解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索引知识的一些小妙招总结
May 10 MySQL
MySQL 使用自定义变量进行查询优化
May 14 MySQL
Mysql中 unique列插入重复值该怎么解决呢
May 26 MySQL
MySQL 5.7常见数据类型
Jul 15 MySQL
MySQL 那些常见的错误设计规范,你都知道吗
Jul 16 MySQL
浅谈MySQL之select优化方案
Aug 07 MySQL
一次MySQL启动导致的事故实战记录
Sep 15 MySQL
记一次Mysql不走日期字段索引的原因小结
Oct 24 MySQL
浅谈redis的过期时间设置和过期删除机制
Mar 18 MySQL
MySQL 表锁定 LOCK和UNLOCK TABLES的 SQL语法
Apr 18 MySQL
MySQL数据库中的锁、解锁以及删除事务
May 06 MySQL
MySQL数据库实验之 触发器和存储过程
Jun 21 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自定义函数实现汉字转换utf8编码的方法
2016/09/29 PHP
php结合md5的加密解密算法实例
2016/09/30 PHP
微信开发之php表单微信中自动提交两次问题解决办法
2017/01/08 PHP
php使用curl实现ftp文件下载功能
2017/05/16 PHP
Laravel 修改验证异常的响应格式实例代码详解
2020/05/25 PHP
YUI 读码日记之 YAHOO.util.Dom - Part.1
2008/03/22 Javascript
checkbox 复选框不能为空
2009/07/11 Javascript
JS实现self的resend
2010/07/22 Javascript
js call方法详细介绍(js 的继承)
2013/11/18 Javascript
js实现百度联盟中一款不错的图片切换效果完整实例
2015/03/04 Javascript
简单分析javascript面向对象与原型
2015/05/21 Javascript
javascript事件委托的方式绑定详解
2015/06/10 Javascript
node.js读取文件到字符串的方法
2015/06/29 Javascript
全面解析Bootstrap排版使用方法(文字样式)
2015/11/30 Javascript
js css实现垂直方向自适应的三角提示菜单
2016/06/26 Javascript
Bootstrap编写一个兼容主流浏览器的受众门户式风格页面
2016/07/01 Javascript
jQuery Validate让普通按钮触发表单验证的方法
2016/12/15 Javascript
微信小程序 input输入框详解及简单实例
2017/01/10 Javascript
VUE简单的定时器实时刷新的实现方法
2019/01/20 Javascript
Layui点击图片弹框预览的实现方法
2019/09/16 Javascript
浅谈Vue static 静态资源路径 和 style问题
2020/11/07 Javascript
[01:39](回顾)各路豪强针锋相对,几经鏖战四强产生
2014/07/01 DOTA
django使用图片延时加载引起后台404错误
2017/04/18 Python
Python之csv文件从MySQL数据库导入导出的方法
2018/06/21 Python
PyTorch实现AlexNet示例
2020/01/14 Python
在tensorflow实现直接读取网络的参数(weight and bias)的值
2020/06/24 Python
爱普生美国官网:Epson美国
2018/11/05 全球购物
运动鞋、足球鞋和慕尼黑球衣:Sport Münzinger
2019/08/26 全球购物
美国校服网上商店:French Toast
2019/10/08 全球购物
食品安全承诺书
2014/05/22 职场文书
电子商务专业应届毕业生求职信
2014/06/21 职场文书
室内趣味活动方案
2014/08/24 职场文书
优秀党员先进事迹材料
2014/12/18 职场文书
《山中访友》教学反思
2016/02/24 职场文书
浅谈PHP7中的一些小技巧
2021/05/29 PHP
如何在python中实现ECDSA你知道吗
2021/11/23 Python