ToB企服应用市场:ToB评测及商务社交产业平台
标题:
一文带你深度剖析MySQL 8.0事件提交原理
[打印本页]
作者:
大连全瓷种植牙齿制作中心
时间:
2024-8-13 16:27
标题:
一文带你深度剖析MySQL 8.0事件提交原理
摘要:当多个引擎/节点同时访问和修改数据时,如何保证数据在各个引擎/节点之间的同等性成为了一项寻衅。本文将深入探讨MySQL集群在保持数据同等性的解决方案。
本文分享自华为云社区
《【华为云MySQL技术专栏】MySQL 8.0事件提交原理剖析!》
,作者:GaussDB数据库。
1.
概述
MySQL是一个插件式、支持多存储引擎架构的数据库。一方面,MySQL支持一个事件跨多个引擎进行读写,使得数据库系统具备良好的可扩展性和灵活性;另一方面,MySQL也支持一个事件跨多节点进行读写,通太过布式节点架构使MySQL消除了单点故障,提高数据库系统的可靠性和可用性。
然而,当多个引擎/节点同时访问和修改数据时,如何保证数据在各个引擎/节点之间的同等性成为了一项寻衅。
本文将深入探讨MYSQL集群在保持数据同等性的解决方案。MySQL集群通过XA事件(X/Open Distributed Transaction Processing Model,简称X/Open DTP Model)解决了此问题。XA事件分为内部XA和外部XA事件,本文将聚焦内部XA的源码实现。
2. XA
事件
XA事件界说了三个到场脚色(APP、TM、RM),并通过两个阶段实现分布式事件。
图
2.1 XA
事件模子
XA
事件中的三个到场脚色分别是:
APP
(
Application Program,简称APP
):
应用程序,界说事件的开始和结束。
TM
(
Transaction Manager,简称TM
)
:
事件管理器,充当事件的协调者,监控事件的执行进度,负责事件的提交、回滚等。
RM
(
Resource Manager,简称RM
)
:
资源管理器,充当事件的到场者,如数据库、文件系统,提供访问资源的方式。
实现分布式事件的两个阶段:
阶段一
:
TM向所有的RM发出PREPARE指令,RM进行完成提交前的准备工作,并刷新相干操纵日志,此时不会进行事件提交。如果在PREPARE指令下发过程中某一RM节点失败,则回滚事件,TM向所有RM节点下发ROLLBACK指令,防止数据差别等的情况发生。
阶段二
:
如果TM收到所有RM的乐成消息,则TM向RM发出COMMIT指令,RM向TM返回提交乐成的消息后,TM确认整个事件完成。如果任意一个RM节点COMMIT失败,则TM尝试重新下发COMMIT指令,尝试失败到上限次数后将返回报错,整个事件失败。
在单实例节点中,当Server层作为TM,多个存储引擎作为RM,就会产生内部XA事件,MySQL利用内部事件保证了多个存储引擎的同等性。外部XA事件一般是针对跨多MySQL实例的分布式事件,因此,外部XA的协调者是用户的应用,到场者是MySQL节点。
外部XA事件与内部XA事件核心逻辑雷同,同时给用户提供了一套XA事件的操纵命令,包括XA start,XA end,XAprepare和XA commit等。
3.
内部XA事件
在单个MYSQL实例中,利用内部XA事件来解决Server层Binlog日志和Storage层事件日志的同等性等问题。此中,Server层作为事件协调器,而多个存储引擎作为事件到场者。
3.1
协调者对象
tc_log
MySQL启动时,包罗了事件协调者的选择。如果开启了Binlog,而且存在事件引擎,则XA协调器为mysql_bin_log对象,利用Binlog物理文件记录事件状态;如果关闭了Binlog,且存在不少于2个事件引擎,则XA协调器为tc_log_mmap对象,利用内存结构来记录事件状态;其他情况(没有事件引擎),则不需要XA,tc_log设置为tc_log_dummy 对象。
无论tc_log_dummy还是mysql_bin_log或tc_log_mmap都基于TC_LOG这个基类来实现的。TC_LOG是一个全局指针,作为事件提交的协调器,实现了事件的prepare,commit,rollback等接口。
图3.1TC_LOG类关系图
mysql_bin_log,tc_log_mmap和tc_log_dummy作为协调者的基本逻辑如下:
mysql_bin_log作为协调者:
prepare:ha_prepare_low
commit:write-binlog + ha_comit_low
tc_log_mmap作为协调者:
prepare:ha_prepare_low
commit:wrtie-xid + ha_commit_low
tc_log_dummy作为协调者:
prepare:ha_prepare_low
commit:ha_commit_low
复制代码
此中tc_log_dummy不会记录事件日志,只是做简单的转发,将Server层的调用路由到Storage层调用。tc_log_mmap是一个尺度的事件协调者实现,它会创建一个名为tc.log的日志并利用操纵系统的内存映射(memory-map,mmap)机制将内容映射到内存中,tc.log文件中分为一个一个PAGE,每个PAGE上有多个XID(X/Opentransaction IDentifier,全局事件唯一ID)。Binlog同样基于TC_LOG来实现事件协调者功能,会递增天生mysql-binlog.xxxx的文件,每个文件中包罗多个事件产生的Binlog event,并在Binlogevent中包罗XID。tc_log_mmap和Binlog都基于XID来确定事件是否已提交。
本文重要关注于如何通过内部XA 保证Binlog和Redolog的同等性,即以Binlog作为协调器的场景,这里的Binlog既是协调者也是到场者。
3.2
事件提交过程
如图3.2为一个事件的执行过程,当客户端发出COMMIT指令时,MYSQL内部将通过Prepare和Commit两个阶段完成事件的提交。
图3.2 事件提交过程
Prepare
阶段
,事件的Undo log设置为prepare状态,写PrepareLog(Prepare阶段产生的Redo Log),将事件状态设为TRX_PREPARED,写XID(事件ID号)到RedoLog,同时把Redo Log刷新到磁盘中。
Commit
阶段
,Binlog写入文件并刷盘,同时也会把XID写入到Binlog。调用引擎的Commit完成事件的提交,同时会对事件的Undo log从prepare状态设置为提交状态(可清理状态),写Commit Log(Commit阶段产生的Redolog),释放锁、read view等,最后将事件状态设置为TRX_NOT_STARTED状态。
两阶段提交保证了事件在多个引擎之间的原子性,以Binlog写入乐成作为事件提交的标志。
在崩溃恢复中,是以
Binlog
中的
XID
和
Redo log
中的
XID
进行比较,
XID
在
Binlog
里存在则提交,不存在则回滚。我们来看崩溃恢复时具体的情况:
情况一:
写入Redolog后,处于Prepare状态的时间崩溃了,此时:
由于Binlog还没写,Redo log处于Prepare状态还没提交,所以崩溃恢复的时间,这个事件会回滚,此时Binlog还没写,所以也不会传到备库。
情况二:
假设写完Binlog之后崩溃了,此时:
Redolog中的日志是不完整的,处于Prepare状态,还没有提交,那么恢复的时间,首先检查Binlog中的事件是否完整(事件
XID
在
Binlog
里中存在,标志该事件已经完成
),如果事件完整,则直接提交事件,否则回滚事件。
情况三:
假设Redolog处于Commit状态的时间崩溃了,如果Binlog中的事件完整,那么会重新写入Commit标志,并完成提交,否则回滚事件。由此可见,两阶段提交能够确保数据的同等性。
一般常用的SQL语句都是通过公共接口mysql_execute_command来执行,我们来分析该接口执行的流程:
mysql_execute_command
{
switch (command)
{
case SQLCOM_COMMIT
trans_commit();
break;
}
if thd->is_error() //语句执行报错
trans_rollback_stmt(thd);
else
trans_commit_stmt(thd);
}
复制代码
MySQL的Server层有两个提交函数trans_commit_stmt()和trans_commit()。前者在每个语句执行完成时调用,一般标志语句的结束。而后者是在整个事件真正提交的时间调用,一般对应显示执行COMMIT语句,或开启一个新事件BEGIN/START TRANSCATION,或执行一条非暂时表的DDL语句等场景。
3.3
多语句事件提交
多语句事件提交一般指BEGIN/COMMIT显示事件,重要逻辑在trans_commit()中,以下是具体实现:
// mysql层进行的事务提交
int ha_commit_trans(THD *thd, bool all, bool ignore_global_read_lock) {
Transaction_ctx *trn_ctx = thd->get_transaction();
// all为true,意味着当前是事务级提交范围,否则是语句级提交范围
Transaction_ctx::enum_trx_scope trx_scope = all ? Transaction_ctx::SESSION : Transaction_ctx::STMT ;
// 获得注册在当前事务的引擎列表,在trans_register_ha()中初始化
Ha_trx_info *ha_info = trn_ctx->ha_trx_info(trx_scope);
// 当前注册的可读可写存储引擎的数量,只有事务引擎支持读写
uint rw_ha_count = 0;
// 检查是否可以跳过两阶段提交机制
rw_ha_count = ha_check_and_coalesce_trx_read_only(thd, ha_info, all);
trn_ctx->set_rw_ha_count(trx_scope, rw_ha_count);
// Prepare 阶段
if (!trn_ctx->no_2pc(trx_scope) && (trn_ctx->rw_ha_count(trx_scope) > 1))
error = tc_log->prepare(thd, all);
}
// Commit 阶段
if (error || (error = tc_log->commit(thd, all))) {
ha_rollback_trans(thd, all);
goto end;
}
}
复制代码
协调者如何确认是否走2PC(两阶段提交)逻辑?
这里重要根据事件修改是否涉及多个引擎来决定,即函数ha_check_and_coalesce_trx_read_only()。特殊的是,如果打开Binlog,Binlog也会作为到场者而被思量在内,最终协调者会统计事件中涉及修改的到场者数量。如果数量超过1个,则进行2PC提交换程。
当满足以上条件,进入Prepare阶段,调用Binlog协调器的prepare接口。Prepare阶段,Binlog Prepare接口没什么可做,而InnoDB Prepare接口重要做的事情就是修改事件和Undo段的状态,以及记录XID。
InnoDB Prepare接口会把内存中事件对象的状态修改为TRX_STATE_PREPARED,并将事件对应Undo段在内存中的对象状态修改为TRX_UNDO_PREPARED。然后,把XID信息写入当前事件对应日志组的Undo Log Header中的XID地区。修改TRX_UNDO_STATE字段值和写入XID,这两个操纵都要修改Undo页。修改Undo页之前,会先记录相应的Redo日志。最后,刷事件更新产生的Redo日志。
// innodb prepare,innodb层事务准备阶段
static void trx_prepare(trx_t *trx) /*!< in/out: transaction */
{
lsn_t lsn = 0;
// 对于系统和undo表空间回滚段,如果有更新需要持久化到redo中
if (trx->rsegs.m_redo.rseg != nullptr && trx_is_redo_rseg_updated(trx)) {
// lsn = mtr.commit_lsn(); 开启第一个mtr,并返回写入redo log buffer后的最新位点,提交时刻对应的lsn
lsn = trx_prepare_low(trx, &trx->rsegs.m_redo, false);
}
// 对于临时表空间回滚段,如果有更新不需要持久化到redo中
if (trx->rsegs.m_noredo.rseg != nullptr && trx_is_temp_rseg_updated(trx)) {
trx_prepare_low(trx, &trx->rsegs.m_noredo, true);
}
// 更新事务和事务系统状态信息
trx->state = TRX_STATE_PREPARED;
trx_sys->n_prepared_trx++;
// 释放RC及以下隔离级别的GAP lock
if (trx->isolation_level <= TRX_ISO_READ_COMMITTED) {
trx->skip_lock_inheritance = true;
lock_trx_release_read_locks(trx, true);
}
switch (thd_requested_durability(trx->mysql_thd)) {
// thd初始化时默认设置为HA_REGULAR_DURABILITY
case HA_REGULAR_DURABILITY:
trx->ddl_must_flush = false;
// redolog刷新
trx_flush_log_if_needed(lsn, trx);
}
}
复制代码
紧接着进入2PC的Commit阶段,trans_commit()调用binlog协调器的MYSQL_BIN_LOG::Commit()接口,功能会合在MYSQL_BIN_LOG:
rdered_commit()函数中。到了Commit阶段,一个事件就已经接近尾声了。写操纵(包括增、删、改)已经完成,内存中的事件状态已经修改,Undo状态也已经修改,XID信息也已经写入Undo Log Header,Prepare阶段产生的Redo日志已经写入到Redo日志文件。剩余的收尾工作,包括Redo日志刷盘、事件的Binlog日志从暂时存放点拷贝到Binlog日志文件、Binlog日志文件刷盘以及InnoDB事件提交。
// tc_log->commit ==> MYSQL_BIN_LOG::commit()
MYSQL_BIN_LOG::commit()
// 这个函数很重要,它包含了binlog组提交三步曲,
int MYSQL_BIN_LOG::ordered_commit(THD *thd, bool all, bool skip_commit) {
//1:Flush Stag:按照事务提交的顺序,先刷Redo log到磁盘,然后把每个事务产生的 binlog 日志从临时存放点拷贝到 binlog 日志文件缓存中
flush_error = process_flush_stage_queue(&total_bytes, &do_rotate, &wait_queue);
//2: Sync Stage: binlog 日志刷盘之前会进入等待过程,目的是为了攒到更多的binlog日志后,合并IO单次刷盘
sync_binlog_file(false);//binlog fsync to disk
//3: Commit Stage: 各线程按序提交事务 process_commit_stage_queue(thd, commit_queue);
}
复制代码
Redo Binlog日志刷盘都涉及到磁盘IO。如果每提交一个事件,都把该事件中的 Redo日志、Binlog日志刷盘,那么就会涉及到许多小数据量的IO操纵,但是频繁的小数量IO操纵非常消耗磁盘的读写性能。
为了提高磁盘IO服从并进一步提拔事件的提交服从,MySQL从5.6开始引入了Binlog日志组提交功能。该功能将事件的Commit阶段细分为3个子阶段。对于每个子阶段,都可以有多个事件同时处于该子阶段,写日志和刷盘操纵可以合并。
Flush子阶段,先将Redo日志刷盘,接着将所有的binlog caches写入到binlog文件缓存中。
Sync子阶段,对binlog文件缓存做fsync操纵,多个线程的 binlog 合并为一次刷盘。
Commit子阶段,依次将redolog中已经prepare的事件在引擎层提交,commit阶段不用刷盘,因为flush阶段中的redolog刷盘已经足够保证数据库崩溃时的数据安全了。当前Commit子阶段重要包罗了InnoDB层的事件提交,真正执行事件提交入口函数为trx_commit_low()。trx_commit_low()重要分成两个部门trx_write_serialisation_history()和trx_commit_in_memory()。trx_write_serialisation_history()处理整个事件执行过程中所利用insert/update的回滚段的收尾工作。trx_commit_in_memory()在内存中设置事件提交的标志trx->state = TRX_STATE_COMMITTED_IN_MEMORY,本领件的数据可以即刻被其他事件可见;在设置事件提交已经完成的标志后,才会释放当前事件的Read View和事件过程中所持有的table lock和record lock,清除trx_sys系统中的当前事件等。
3.4
单语句事件提交
从SQL的执行过程分析可以看到,无论执行何种语句,最后都会执行trans_commit_stmt(),即单语句提交函数。如果当前是单语句事件,一般指AUTOCOMMIT为ON的场景,那么会走事件提交逻辑,即ha_commit_trans()函数。额外思量到COMMIT和DDL语句等已经在调用trans_commit_stmt()之前将事件提交,所以在这里只需要标志语句结束即可。
// 执行单语句事务提
bool trans_commit_stmt(THD *thd, bool ignore_global_read_lock) {
int res = false;
// 单语句事务,需要走2PC提交逻辑
if (thd->get_transaction()->is_active(Transaction_ctx::STMT)) {
res = ha_commit_trans(thd, false, ignore_global_read_lock);
} else if (tc_log)
// COMMIT/DDL等,只需要走引擎层提交逻辑,置为false,只标识语句结束,跳过真正提交阶段
res = tc_log->commit(thd, false);
thd->get_transaction()->reset(Transaction_ctx::STMT);
return res;
}
复制代码
ha_commit_trans()最后会走到innobase_commit()中,innobase_commit()中的参数commit_trx控制是否真的进行存储引擎层的提交处理,trans_commit_stmt()里会设置 commit_trx为0,答应跳过事件提交。
这里的判定逻辑是,只有当commit_trx= 1或者设置autocommit=1的情况下,才会真正进入事件提交逻辑。而多语句事件对应的trans_commit()函数里会设置commit_trx=1,进入innobase_commit_low()执行真正的事件提交逻辑。
/** 在innodb层提交一个事务
thd:需要提交事务的会话
commit_trx:true,需要提交事务。false,跳过事务提交。
*/
static int innobase_commit(handlerton *hton, THD *thd, bool commit_trx)
{
trx_t *trx = check_trx_exists(thd);
// innobase_commi仅在“真正的”commit时被调用,而且在每个语句之后(走trans_commit_stmt()函数)也被调用,因此这里需要will_commit判断是否要真正去提交事务。
bool will_commit =
commit_trx ||
(!thd_test_options(thd, OPTION_NOT_AUTOCOMMIT | OPTION_BEGIN)); // autocommit=1且不在显示事务块中
if (will_commit) {
/* 在显示提交commit,或者autocommit=1、且不在显示事务块内*/
innobase_commit_low(trx);
} else {
/* 其他情况,我们只是标记SQL语句结束,不做事务提交 */
trx_mark_sql_stat_end(trx);
}
return 0;
}
复制代码
4.
总结
本文从多语句/单语句事件提交原理角度出发,介绍了MySQL的两阶段提交协议。在prepare阶段,InnoDB把数据更新到内存后记录Redo log,此时Redo log的状态为prepare状态;在Commit阶段,Server天生Binlog后落盘,InnoDB把刚写入的Redo log状态更新为commit状态。两阶段提交保证了事件在多个引擎和Binlog之间的原子性,同样保证了通过备份和Binlog恢复出的数据库和原数据库的数据同等性。
点击关注,第一时间相识华为云新鲜技术~
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/)
Powered by Discuz! X3.4