目标
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生成逻辑分析。