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。
- 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函数有定义: