MySQL InnoDB MVCC读写逻辑分析与调测

目录

目标

前提

准备1

准备2

准备3

一、读写场景

1、简单两事务读写场景

2、复杂五事务读写场景

二、MVCC读写过程函数调用流程图

1、select过程

(1)、线程连接处理过程

(2)、解析调度指令过程

(3)、通过函数指针调用过程

(4)、进入InnoDB引擎调用过程

(5)、判断并选择版本过程

(6)、undo log 搜索行可见版本过程

(7)、readview创建过程

2、insert\update过程

生成undo log过程


目标

1、构建MVCC读写场景

2、gdb调试MVCC过程,输出流程图(函数级别调用过程)

前提

准备1

打开服务端

查询mysqld进程号 线程树

打开客户端,想创建几个事务号就打开几个客户端

准备2

数据库mvcc,两个表test和stu:

test表    作用:只生成事务号

create table test (id int(4))engine=innodb charset=utf8;

id

null

stu表      作用:真正触发MVCC机制的表

create table stu (id int(4),name varchar(10))engine=innodb charset=utf8;

id

name

1

zhangsan

准备3

gdb调试当前服务端的进程号

如果需要记录每次操作的输出内容,可以事先设置日志

方法:set logging file xxx.txt          set logging on

一、读写场景

1、简单两事务读写场景

Session 1

Session 2

begin;

begin;

insert into test values(1);

insert into test values(2);

select * from stu where id=1;

update stu set name=”lisi” where id=1;

select * from stu where id=1;

select * from stu where id=1;

commit;

select * from stu where id=1;

2、复杂五事务读写场景

Session 1

Session 2

Session 3

Session 4

Session 5

begin;

begin;

begin;

begin;

begin;

insert into test values(1);

insert into test values(2);

insert into test values(3);

update stu set name=”li4” where id=1;

commit;

select * from stu where id=1;

update stu set name=”wang5” where id=1;

select * from stu where id=1;

commit;

update stu set name=”zhao6” where id=1;

select * from stu where id=1;

select * from stu where id=1;

commit;

二、MVCC读写过程函数调用流程图

1、select过程

(1)、线程连接处理过程

在两个事务读写场景中,初次触发MVCC机制是select语句。它的大体函数调用流程图是这样的:

客户端与服务端建立链接的流程图:

在每次客户端输入命令后,都会进入一个handle_connection函数,这个函数中有一个循环始终在监控客户端的链接状态(即the_connection_alive的返回值),一旦客户端链接进来,就将这个函数的返回值一直置为true,循环条件成立,接下来只要客户端将客户端输入的命令解析执行(即do_co mmand函数)处理就可以了。

源码(handle_connection()函数中):

for(;;)
{
	……
	while(the_connection_alive(thd))
    {
	    if (do_command(thd))
            break;
    }
    end_connection(thd);
    ……
}
close_connection(thd, 0, false, false);
……

(2)、解析调度指令过程

收到客户端发来的命令(语句),需要对命令进行解析,这些操作都是在do_command函数中进行的。它的内部主要调用的函数有:

其中,get_command函数用来获取客户端输入的命令,然后读取命令包,对命令包进行解析,解析好了以后,就可以将命令发送出去,然后执行下一步操作。

在parse_packet函数中是一个大的switch语句,根据实测,我们找到了select * from stu where id=1;语句是执行的COM_QUERY分支。它对传进来的data进行一些参数的写入。

case COM_QUERY:
{
    data->com_query.query= reinterpret_cast<const char*>(raw_packet);
    data->com_query.length= packet_length;
    break;
}

dispatch_command函数用于执行一个连接级别的命令(COM_XXXX)。它的函数原型为bool dispatch_command(THD *thd, const COM_DATA *com_data, enum enum_server_command command);

其中这里面也有一些给传入参数进行重新赋值,在此函数中,有一个switch语句,执行到case COM_QUERY分支后,执行的主要函数为mysql_parse。

mysql_parse用于解析一条查询。mysql_execute_command执行保存在thd和lex-> sql_command中的命令。在switch语句case SQLCOM_SELECT:分支中执行execute_sqlcom_select。execute_sqlcom_select用于执行SQLCOM_SELECT情况。handle_query处理数据操作查询。

接下来进入到do_select函数。这个函数联接所有表并将其写入套接字或表中。

(3)、通过函数指针调用过程

在do_select函数之后的三个函数sub_select、join_init_read_record、rr_sequential,都是通过函数指针来调用的。根据不同的情况来确定具体的调用函数。这三个函数都是顺序执行的。它的调用流程图如下:

在do_select函数中有几行很重要的代码:

error= (*end_select)(join, 0, 0);

error= join->first_select(join,qep_tab,0);

通过查找,找到了first_select返回的是一个函数指针,进一步查看它的定义为:

typedef enum_nested_loop_state  (*Next_select_func)(JOIN *, class QEP_TAB *, bool);

通过函数指针来调用函数,实测中,第一次select过程具体调用的函数为sub_select函数。

在sub_select函数中也是通过函数指针的形式调用函数。具体的代码为:

error= (*qep_tab->read_first_record)(qep_tab);

它的定义为:

typedef int (*Setup_func)(QEP_TAB*);

它的具体调用函数为join_init_read_record。join_init_read_record用于读取记录,这个函数中最后一行,返回一个函数

return (*tab->read_record.read_record)(&tab->read_record);

通过这个函数指针,调用的具体函数是rr_sequential。由于mysql默认隔离级别是repeatable_read(RR),所以read_record具体调用的是rr_sequential函数。

(4)、进入InnoDB引擎调用过程

接下来的几个函数内部实现比较短,调用也特别简单,内部函数一般没有特别多其他函数的调用。运行到rnd_next函数就已经进入到InnoDB引擎了。它的调用流程如下:

ha_rnd_next通过随机扫描来读下一行。rnd_next在表扫描中读下一行。这个时候,就已经进入到InnoDB存储引擎了。index_first将光标放在索引的第一条记录上,并将对应的行读取到buf。index_read主要实现如何执行选择SQL查询。row_search_mvcc函数使用cusor在数据库中搜索行。这个函数主要用于共享连接的表,因此它采用的技术可以帮助重新构造事务应该看到的行。它还具有优化功能,例如预缓存行,使用AHI等

(5)、判断并选择版本过程

         到这里,才开始了mvcc机制的真正核心实现。

其中,lock_clust_rec_cons_read_sees就是判断并选择版本的地方。这个函数检查是否在一致的读取中看到一条记录。如果看到,返回值则为true;如果是检索记录的早期版本,则为false 。从函数内部可以看到具体实现:

bool lock_clust_rec_cons_read_sees(const rec_t* rec /*由innodb扫描出来的一行*/,....)
{  
	...  
	trx_id_t	trx_id = row_get_rec_trx_id(rec, index, offsets);
	return(view->changes_visible(trx_id, index->table->name));
}

在从行中获得当前事务id后,传入changes_visible函数中,通过changes_visible来判断(一致性快照视图和事务id)决定看到的行快照,即读取何种版本的行。

(6)、undo log 搜索行可见版本过程

对于一条记录,有多个版本,需要用undo log来判断它的可见性。用到的函数有row_sel_pre_vers_for_mysql、row_vers_build_for_consistent_read、trx_undo_version_build。它的调用关系为:

具体的调用流程图如下:

如果当前的一致性读视图不可见,需要通过undo log回溯, 这主要是调用了row_ver_build_for_consistent_read函数来返回可见版,row_ver_build_for_consistent_read函数中有一个for循环不断在检查版本是否可见,如果不可见则回溯找到前一个版本,直到遇到可以看见的版本。下面的源代码简略列出主要的函数实现:

for (;;)
{
    bool	purge_sees = trx_undo_prev_version_build()			
    trx_id = row_get_rec_trx_id();
    …
    // 如果当前row版本符合一致性视图,则返回
    if (view->changes_visible(trx_id, index->table->name))
    {
	    break;
	}
    …
    // 如果当前row版本不符合,则继续回溯上一个版本(回到for循环的地方)
	version = prev_version;
}

(7)、readview创建过程

通过第一次select,调试跟踪到了在创建快照的过程中调用了trx_assign_read_view,这个函数会生成当前时刻的数据库的快照。

这个函数位于trx0trx.cc文件,在源码中具体的实现为:

可以看到它会通过全局变量trx_sys中的成员,调用mvcc类的view_open方法,具体的调用关系如下:

分析:

首先需要判断一下readview的状态是否已经处于active,如果为真则open readview。进入到open函数后,判断readview是否为空,不为空则从空闲的readview链表中获取第一个readview。然后prepare,完成,退出。

实际调测中的结果:

可以看到第一次select过程通过trx_assign_read_view函数创建了一个一致性视图,并将它的地址保存在事务trx->readview。

进一步,我们开了三个事务进行update,其中两个事务已经update并且提交:

其中一个尚未提交:

此时卡在row_search_mvcc函数开始处,通过show engine innodb status\G;查看状态:

验证了此时的undo链表为6条,卡住的那条update语句还没执行完,不算。

并且,尚未提交的事务中有两次select过程,所以在mvcc中有两个readview:

2、insert\update过程

生成undo log过程

insert\update过程都是其实就是undo log生成过程的。对于rr和rc隔离级别的事务而言,当前事务不能看到其他事务已修改的数据,而是应该给它返回老版本的数据。

对于insert\update操作,生成的undo log的格式是不一样的。

undo log有两种类型:一种是insert undo log,insert操作会生成insert undo log;另一种是update undo log,update和delete操作都会生成update undo log,其中delete操作是在的删除标记置为true来分辨是delete操作。delete操作并没有立即将数据删除,这样就能够实现回滚。

insert\update操作来说,undo log生成的过程大体如下:       

关于undo log的生成逻辑详见undo log生成逻辑分析。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值