当前位置: 首页 > article >正文

MySQL InnoDB 事务commit逻辑分析

一、前言

事务提交是事务即将落盘的一系列操作,涉及redo\undo log的写盘、bin log的写盘、事务状态的重置、各种参数的改变、无用undo log的清理等方方面面。

在commit过程会有两个阶段:一个是prepare阶段,入口函数为trx_prepare_low;另一个就是commit阶段。

prepare阶段和BINLOG做XA,分别设置insert undo 和 update undo的状态为prepare,调用函数trx_undo_set_state_at_prepare,过程也比较简单,找到undo log slot对应的头页面(trx_undo_t::hdr_page_no),将页面段头的TRX_UNDO_STATE设置为TRX_UNDO_PREPARED,同时修改其他对应字段。

commit阶段较为复杂,也是接下来要详细讲述的。

在InnoDB存储引擎层的commit阶段的入口函数为innobase_commit,它的总的结构大体如下:

二、基本概念

1、事务的状态

事务状态的宏定义在文件trx0types.h中,事务有五种状态,在源代码中的定义为:

其中每个参数的含义为:

参数名称

含义

TRX_STATE_NOT_STARTED

事务尚未开始

TRX_STATE_FORCED_ROLLBACK

事务强制回滚

TRX_STATE_ACTIVE

事务处于活跃状态

TRX_STATE_PREPARED

事务处于准备状态

TRX_STATE_COMMITTED_IN_MEMORY

事务在内存中已提交

2、回滚日志的状态

回滚日志的状态的改变主要发生在trx_undo_set_state_at_finish函数,针对inisert和update两种类型的undo log做出不同的状态赋值。

三、逻辑分析

innobase_commit

在InnoDB存储引擎中进行事务的提交主要的调用的函数是innobase_commit,文件位于ha_innodb.cc。它的函数调用流程图为:

主要逻辑为:首先会判断是否将事务标记为异步回滚,如果是,则将事务回滚,调用的函数为innobase_rollback,如果不是,则会判断是否向MySQL 2PC协调器注册了事务。

接着提取出事务中read_only参数的值,判断是否标记为只读事务。调用thd_binlog_pos读取正在提交的事务的二进制日志位置。

接下来就是调用innobase_commit_low这个重要的函数,这个函数主要是在InnoDB数据库中提交一个事务。下一小节我们重点分析这个函数。

innobase commit完后,判断read_only参数,如果不是只读,则会将commit_threads数减1。检测的cond_signal,调用的函数为mysql_cond_signal。

接着就是执行写和刷新日志的操作。调用的函数为trx_copmmit_complete_for_mysql,将日志刷新到磁盘。

如果只是将sql语句标记位结束,并且不执行事务的提交。如果在此sql语句中为某些表保留了自动增量锁,则立即释放它,调用的函数为lock_unlock_table_autoinc。

并且将事务的undo_no保存,以便在必须回滚下一条sql语句时知道要回滚的位置。

最后重置所需的AUTO-INC行数和fts_next_doc_id,强制线程离开InnoDB,调用的函数为innobase_srv_conc_force_exit_innodb。

trx_commit_for_mysql

接着调用innobase_commit_low,然后调用trx_commit_for_mysql,它位于trx0trx.cc文件中。它的逻辑流程图为:

因为不通过向事务发送Innobase信号来进行提交,所以在这里必须确保trx已经启动。然后判断trx->state,如果是TRX_STATE_NOT_STARTED或TRX_STATE_FORCED_ROLLBACK状态,则进入到trx_start_low函数,如果是TRX_STATE_ACTIVE或TRX_STATE_PREPARED状态,则进入到trx_commit函数。

trx_commit

trx_commit函数,是用来提交一个事务的。它同样位于trx0trx.cc文件中,这个函数中需要用到mini事务,用一个mini事务来完成对于接下来的commit操作。它的流程图如下:

首先会检查一下redo/noredo 的回滚段是否有insert\update的修改操作,调用的函数是trx_is_rseg_updated,直接判断它的返回值即可,如果有,则会同步一个mini事务。接着进入到trx_commit_low函数。

trx_commit_low

trx_commit_low不光提交一个事务,而且还提交一个mini事务。传进来的mtr参数就是用于提交一个mini事务。

在trx_commit_low函数中,声明自动提交的非锁定选项不能在rw_trx_list中,并且它是只读事务。并且事务必须位于mysql_trx_list中。trx_commit_low的流程图如下:

如果我们正在执行最后的提交,则undo_no的值为非零,所以首先判断一下trx中的fts_trx和undo_no参数是否为零,不为零则从FTS系统的POV进行一些必要的操作。接着判断传进来的参数mtr是否为空,mini事务不为空,mtr请求将来提交以使其同步,接着才能进行将序列号写入到history list中,并且通过trx_write_serialisation_history将serialised参数的值返回,接下来调用mtr_commit进行mini事务的提交,使整个事务在基于文件中提交;mtr为空则将serialised参数的值置为false。

最后调用trx_commit_in_memory函数在内存中提交一个事务。

先看trx_write_serialisation_history函数的具体逻辑,然后在看trx_commit_in_memory函数。

trx_write_serialisation_history

history list需要持久化在磁盘上,在undo log header的最后有对它的定义,在源代码trx0undo.h文件中可以看到:

函数trx_write_serialisation_history的作用是为事务分配其历史序列号,并且将update undo log记录写入到分配的回滚段。如果写入了序列化日志,则函数的返回值为true,否则为false,它的流程图如下:

1、trx_undo_set_state_at_finish

首先,获取到回滚段的mutex,对回滚段中两个slot(m_redo、m_noredo)都进行mutex_enter。然后将insert undo设置成完成状态,调用的函数为trx_undo_set_state_at_finish。函数trx_undo_set_state_at_finish首先会获取到一个undo log页(trx_undo_page_get)的地址,然后判断并设置事务的完成状态。完成状态一共有三种:

a、如果undo log页的大小为1,并且占用的header page使用大小小于TRX_UNDO_PAGE_REUSE_LIMIT(3 * UNIV_PAGE_SIZE / 4)时,则将状态设置为TRX_UNDO_CACHED,该undo对象会随后加入到undo cached list上,以备重用。trx0undo.h中的定义:

b、如果是insert undo(type为TRX_UNDO_INSERT),则状态设置为TRX_UNDO_TO_FREE,这也说明insert类型的undo log在commit完后,会直接释放,不会再保留。

c、如果不是上两种情况,也就是delete和update操作产生的update undo log类型的日志,则需要purge线程适时进行清理操作,状态设置为TRX_UNDO_TO_PURGE。三种状态的设置:

在undo的状态信息state设置好后,将state写入到undo header页的TRX_UNDO_STATE位置中,调用的函数为mlog_write_ulint。

insert undo设置完完成状态后,接下来对update undo进行设置完成状态。首先获取到回滚段的两个slot(m_redo、m_noredo)中的update_undo链表地址,然后将回滚段添加到purge queue,调用的函数为trx_serialisation_number_get,它在函数中的比较重要的实现语句为purge_sys->purge_queue->push(elem)。然后进行完成状态的设置,调用的函数与insert设置完成状态的函数一样,都是trx_undo_set_state_at_finish,这里就不在重复介绍。

然后就是对update undo进行清理,调用的函数为trx_undo_update_cleanup

2、trx_undo_update_cleanup

它将update undo log header添加为历史记录列表中的第一个,并释放内存对象,或将其放入缓存的update undo log 段列表中。它的具体实现为:

首先会获取到update_undo以及rseg,然后调用trx_purge_add_update_undo_to_history函数,将update_log添加到history链表中,purge线程在合适的时间进行清理。

添加完后,将undo log从update_undo_list中移除,重新赋值为NULL,然后将undo log添加到update_undo_cached链表中,最后在内存中释放。这样就可以重用这些已经生成的undo log,不用重新创建生成,大大节省了效率。

其中比较重要的函数是trx_purge_add_update_undo_to_history函数,接下来重点分析这个这个函数。

(1)、trx_purge_add_update_undo_to_history

它的流程步骤如下:

a、首先,获取undo回滚段头,调用的函数为trx_rsegf_get,通过upda_page和undo->hdr_offset确定undo_header的值。

b、判断undo状态是否为cached(TRX_UNDO_CACHED)。如果不是cached状态,则设置第n个回滚日志插槽的文件页号为FIL_NULL,意为slot空闲,调用的函数为trx_rsegf_set_nth_undo。同时更新TRX_RSEG_HISTORY_SIZE的大小(大小为原来TRX_RSEG_HISTORY_SIZE的大小+undo->size),读取调用的函数为mtr_read_ulint,写入调用的函数为mlog_write_ulint,写入的大小为4byte(MLOG_4BYTES)。

c、将undo日志添加为历史记录列表(TRX_RSEG_HISTORY)中的第一条,节点指针为undo header的TRX_UNDO_HISTORY_NODE,调用的函数为flst_add_first。

d、根据update_rseg_history_len的值对trx_sys->rseg_history_len进行更新(也就是show engine innodb status看到的history list),如果只有普通的update_undo,则加1,如果还有临时表的update_undo,则加2,调用的函数为os_atomic_increment_ulint;然后唤醒purge线程,调用的函数为srv_wake_purge_thread_if_not_active。

e、将事务编号写入回滚日志头。将trx->no写到undo头的TRX_UNDO_TRX_NO段,调用的函数为mlog_write_ull。

f、判断undo log的已删除标记del_marks的值,如果不为空则将del_marks跟新为false,写入到TRX_UNDO_DEL_MARKS段,大小为2byte(MLOG_2BYTES)。

g、如果undo所在回滚段中的rseg->last_page_no为FIL_NULL,表示该回滚段的旧的清理已经完成,进行如下赋值,记录这个回滚段上第一个需要purge的undo记录信息:

rseg->last_page_no = undo->hdr_page_no;

rseg->last_offset = undo->hdr_offset;

rseg->last_trx_no = trx->no;

rseg->last_del_marks = undo->del_marks;

(2)、trx_undo_mem_free

将undo log放到history list后,就可以将update undo置为空(undo_ptr->update_undo = NULL)。如果undo需要cache,将undo对象放到回滚段的update_undo_cached链表上;否则释放undo对象,释放调用函数为trx_undo_mem_free。

3、trx_sys_update_mysql_binlog_offset

最后,如果启用了MySQL Binlogging或数据库服务器是MySQL复制从服务器,则更新trx sys header中的最新MySQL Binlog名称和偏移信息,在trx0sys.h文件中有对trx系统header中关于MySQL binlog偏移量信息的偏移量的定义:

写binlog调用的函数为trx_sys_update_mysql_binlog_offset,它的流程大体如下:

a、调用trx_sysf_get获取事务系统头sys_header。

b、调用mlog_write_ulint函数更新TRX_SYS_MYSQL_LOG_MAGIC_N_FLD段。

c、调用mlog_write_string函数更新TRX_SYS_MYSQL_LOG_NAME段。

d、调用mlog_write_ulint函数更新TRX_SYS_MYSQL_LOG_OFFSET_HIGH段。

e、调用mlog_write_ulint函数更新TRX_SYS_MYSQL_LOG_OFFSET_LOW段。

trx_commit_in_memory

函数trx_commit_in_memory用于在内存中提交事务。它的流程图如下:

首先,确定事务是否为非锁定自动提交选择(表示为只读),调用的函数为trx_is_autocommit_non_locking,如果是非锁定自动提交选择,则判断read_view是否存在,若存在,则关闭read_view,并且将事务的状态设置为TRX_STATE_NOT_STARTED。如果不是非锁定自动提交选择,为了获得一致的快照,需要在执行提交和释放锁之前从正在运行的mvcc的事务ID列表中删除当前事务,调用的函数为trx_erase_lists;这样,该事务不再拥有任何用户锁,然后从活跃事务列表中删除该事务,当从rw_trx_list中删除trx时,此调用还将释放所有隐式锁,调用的函数为lock_trx_release_locks。

事务释放完记录锁、从读写事务链表中清除、以及关闭read、 view后,这时将对回滚段的两种slot(m_redo、m_noredo)中的insert_undo进行清理,调用的函数为trx_undo_insert_cleanup

  1. trx_undo_insert_cleanup

trx_undo_insert_cleanup用于在事务提交或回滚后释放insert 回滚日志。

(1)、首先将undo对象从回滚段中的insert_undo_list中移除

(2)、判断undo->state是否是TRX_UNDO_CACHED状态,如果是,则将undo对象添加到insert_undo_cached链表中,以备重用;如果不是,则首先删除文件中的撤消日志段,调用的函数为trx_undo_seg_free,并且修改当前回滚段的大小(rseg->curr_size),并释放undo对象所占的内存。调用的函数为trx_undo_mem_free。

事务完成提交后,需要将其使用的回滚段引用计数rseg->trx_ref_count减1;

2、trx_flush_log_if_needed

现在,根据my.cnf选项,我们可以将日志缓冲区写入日志文件,如果操作系统不崩溃,则使事务持久。我们还可以将日志文件刷新到磁盘,从而使事务在操作系统崩溃或断电时也持久。

InnoDB的组提交中的想法是,一组事务聚集在一个trx后面,对日志文件进行物理磁盘写操作,并且当该物理写操作完成时,这些事务之一进行写操作,从而提交整个组。

注意,只有在数据库中有> 2个用户时,此组提交才会带来好处。然后,至少2个用户可以聚集在一个后面,将物理日志写入磁盘。

如果我们在prepare_commit_mutex下调用trx_commit,则将延迟可能的日志写入,并刷新到单独的函数trx_commit_complete_for_mysql,该函数仅在线程释放互斥量时才调用。

这是为了使组提交算法起作用。否则,prepare_commit互斥锁将序列化所有提交,并阻止收集一组事务。

一旦进行组提交,则调用函数trx_flush_log_if_needed。这个函数接着调用trx_flush_log_if_needed_low函数,这个函数中会根据srv_flush_log_at_trx_commit的值进行switch分支选择:

2:flush = false;   写入日志,但不要将其刷新到磁盘;

1:flush = true;   写入日志并有选择地将其刷新到磁盘;

0:return; 不做任何操作。

写入日志的调用的函数为log_write_up_to

(1)、log_write_up_to

函数log_write_up_to用于将缓冲区写入日志文件组。如果传入参数flush_to_disk为true,还将写入的日志页刷新到文件系统。

写日志文件调用log_group_write_buf函数,刷新到盘调用log_write_flush_to_disk_low函数

3、srv_active_wake_master_thread

写完日志文件并且刷新到盘后,需要告诉服务器发生了一些活动,因为事务中确实更改的某些内容,这时就需要唤醒master线程,诸如主线程,清除线程或page_cleaner线程之类的后台实用程序线程可能需要做一些工作,调用的函数为srv_active_wake_master_thread。

4、trx_roll_savepoints_free

接下来就需要释放所有的保存点(savepoints)了,保存点保存的是事务中一部分,释放保存点调用的函数为trx_roll_savepoints_free。

因为我们可以异步回滚事务,所以我们在最后一步更改了状态。一旦提交或回滚开始, trx_t :: abort便无法更改,因为在到达此处之前,我们将释放锁。如果参数trx->abort为true,则将state置为TRX_STATE_FORCED_ROLLBACK,否则设置为TRX_STATE_NOT_STARTED。

最后重新初始化事务(trx_init),释放锁(trx_mutest_exit)。

trx_commit_complete_for_mysql

innobase commit完后,判断read_only参数,如果不是只读,则会将commit_threads数减1。检测的cond_signal,调用的函数为mysql_cond_signal。

如果需要,调用trx_commit_complete_for_mysql函数将日志刷新到磁盘。这个函数的流程图如下:

函数调用trx_flush_log_if_needed,这个函数之前分析过,就不在重复。

四、调测结果:

1、-->PREPARED 或者 -->NOT_STARTED

innobase_commit函数中,首次commit过程check_trx_exists函数初始化事务的state、must_flush_log_later、commit_lsn、read_only等参数。

深入函数内部,check_trx_exists中的thd_to_trx函数会将函数初始化为PREPARED状态:

如果事务的地址为空,则会调用函数trx_create_low函数创建事务,将trx的各个参数进行初始化,其中状态初始化为NOT_STARTED。trx0trx.cc:475行trx_create_low函数中定义:

2、-->ACTIVE

开启事务后第一个语句触发,将事务设置为ACTIVE,调用的函数为:trx_start_low。

trx0trx.cc:1434行trx_start_low函数中定义:

3、-->COMMITED_IN_MEMORY

在lock_trx_release_locks函数将PREPARED状态设置为COMMITED_IN_MEMORY状态

在lock0lock.cc:6885行函数lock_trx_release_locks中定义:

实测结果:

4、-->NOT_STARTED

在commit_in_memory函数将COMMITED_IN_MEMORY状态设置为NOT_STARTED。实测结果为:

在trx0trx.cc:2086行commit_in_memory函数有定义:


http://www.kler.cn/a/326659.html

相关文章:

  • JAVA 插入 JSON 对象到 PostgreSQL
  • 柯桥零基础学日语日语培训中为什么不说「ご客様」而是「お客様」?
  • map和set和pair
  • 用于nodejs的开源违禁词检测工具 JavaScript node-word-detection
  • Template Method(模板方法)
  • C++20 STL CookBook2 更强大的编译时 + 安全比较 + spaceship比较符
  • C++的new关键字
  • 如何在Android上运行Llama 3.2
  • 关于TrustedInstaller权限
  • c++-类和对象-设计立方体类
  • 每天学习一个技术栈 ——【Django Channels】篇(2)
  • ansible实现远程创建用户
  • [BUUCTF从零单排] Web方向 03.Web入门篇之sql注入-1(手工注入详解)
  • Java 编码系列:注解处理器详解与面试题解析
  • Uptime Kuma运维监控服务本地部署结合内网穿透实现远程在线监控
  • PostgreSQL的扩展Citus介绍
  • 非常全面的中考总复习资料-快速提升中考成绩!
  • 总结C/C++中内存区域划分
  • 点餐小程序实战教程14点餐功能
  • 心理咨询行业为何要有自己的知识付费小程序平台 心理咨询小程序搭建 集师saas知识付费小程序平台搭建
  • 遇到 Docker 镜像拉取失败的问题时该如何解决
  • 六、设计模式-6.3、责任链模式
  • WebAssembly 为什么能提升性能,怎么使用它 ?
  • 晶圆厂如何突破多网隔离实现安全稳定又快速的跨网域文件传输?
  • 执行力怎么培养?
  • 建投数据自主研发相关系统获得欧拉操作系统及华为鲲鹏技术认证书