记录些MySQL题集(11)

MySQL 组提交原理

MySQL 中事务的两阶段提交保证了 redo log 与 binlog 两种日志文件的数据一致性,但是并发事务场景下还需要保证事务顺序的一致性,因此通过组提交机制在保证顺序一致性的前提下提高写入效率。因此组提交是两阶段提交的一部分。

两阶段提交

组提交是两阶段提交的一部分,因此首先回顾下两阶段提交(2PC)。其中:

  • prepare 阶段,其中负责 redo log 刷盘;

  • commit 阶段,其中负责 binlog 刷盘与存储引擎层面的事务提交。

事务提交的顺序

MySQL 的内部 XA 机制也就是两阶段提交保证了单个事务在 binlog 和 InnoDB 之间的原子性,但是在多个事务并发执行的情况下,怎么保证在 binlog 和 redo log 中事务的顺序一致?

在回答这个问题之前需要明确为什么需要保证 binlog 与 redo log 中事务顺序的一致性?

原因是这种一致性对于保持数据的完整性和一致性、支持高效的数据恢复和复制机制至关重要。其中:

  • 数据恢复,比如基于 binlog 进行按时间点恢复(PITR)时,如果两者顺序不一致,将导致事务以不同的顺序被应用,违法 ACID 原则;

  • 复制一致性,如果两者顺序不一致,将导致主从事务以不同的顺序被应用,从而导致主从数据不一致;

  • 事务原子性,如果两者顺序不一致,将导致事务无法被正确的回滚或提交,可能导致只有部分事务更改被持久化,违反事务的原子性;

  • 避免死锁,如果两者顺序不一致,可能导致恢复或复制过程中发生死锁,尤其是当涉及到多个事务并发修改同一数据行时。

早期解决方法

那么,如何保证 binlog 与 redo log 中事务的顺序一致呢?

MySQL 5.6 版本之前,使用 prepare_commit_mutex 对整个 2PC 过程进行加锁,只有当上一个事务 commit 后释放锁,下个事务才可以进行 prepare 操作,通过串行化的执行方式保证了顺序一致,类似于事务隔离级别中的串行化(Serializable)。

显然 prepare_commit_mutex 的锁机制会严重影响高并发时的性能,原因是多个小 IO 是非常低效的方式,串行化导致响应变慢。

在双 1 条件下,在每个事务执行过程中, 都会至少调用 3 次刷盘操作,包括写 redo log,写 binlog,写 commit。而多个小 IO 是非常低效的方式,因此可能导致写入出现性能瓶颈。为提高并发性能,细化锁粒度,引入组提交(group commit),可以理解为批量提交。

具体又可以分为 binlog 组提交与 redo log 组提交。注意 redo log 与 binlog 都是顺序写,而磁盘的顺序写比随机写速度要快。

组提交

binlog

MySQL 5.6 中针对 binlog 引入组提交功能,prepare 阶段不变,只针对 commit 阶段,将 commit 阶段拆分为三个阶段:

  • flush:多个线程按进入的顺序将 binlog 从 cache 写入日志文件(binlog write);

  • sync:将多个线程的 binlog 合并一次刷盘(binlog sync);

  • commit:各个线程按顺序做 InnoDB commit 操作。

其中:

  • 对于每个子阶段,都可以有多个事务处于该子阶段;

  • 每个子阶段有单独的 lock 进行保护,因此保证了事务写入的顺序。

MySQL 事务二阶段提交

组提交实现的基本思想是通过批量写入将日志文件的小数据量多次 IO 变为大数据量更少次数 IO,从而提升磁盘 IO 效率。但是为了合并日志的写入、刷盘参数,就需要一个协调者的角色。如果引入一个单独的协调线程,会增加额外开销。MySQL 的解决方案是把处于同一个子阶段的事务线程分为两种角色:

  • leader 线程,第 1 个进入某个子阶段的事务线程,就是该子阶段当前分组的 leader 线程;

  • follower 线程,第 2 个及以后进入某个子阶段的事务线程,都是该子阶段当前分组的 follower 线程。

事务线程指的是事务所在的那个线程,我们可以把事务线程看成事务的容器,一个线程执行一个事务。leader 线程管事的方式,并不是指挥 follower 线程干活,而是自己帮 follower 线程把活都干了。

commit 细分为 3 个子阶段之后,每个子阶段会有一个队列用于记录哪些事务线程处于该子阶段。为了保证先进入 flush 子阶段的事务线程一定先进入 sync 子阶段,先进入 sync 子阶段的事务线程一定先进入 commit 子阶段,每个子阶段都会持有一把互斥锁

这里可以提出一个问题,如何判断是否可以加入队列?

同时进入 prepare 阶段的多个事务必然没有锁冲突,因此不需要判断是否可以加入 flush 阶段,其他阶段入队顺序相同,因此同样没有冲突。

redo log

MySQL 5.7 针对 redo log 引入组提交功能,同时修改 prepare 阶段与 commit 阶段。

  • 5.6 中 redo log 的刷盘操作在 prepare 阶段完成,因此每个事务的 redo log fsync 操作成为性能瓶颈;

  • 5.7 中将 prepare 阶段中每个线程各自执行 redo log fsync 操作推迟到组提交的 flush 阶段之中,binlog fsync 阶段之前。

显然,通过推迟 redo log fsync 的时间,一次组提交中的成员更多,节省 IOPS 的效果更好。

总结

不同版本中两阶段提交过程中 redo log 与 binlog 的写入与刷盘时间对比见下图。

图片

其中:

  • redo log 与 binlog 中都分开执行 write 与 fsync;

  • redo log fsync 推迟到 binlog write 之后。

然后,整理下 5.7 中两阶段提交中每个阶段中主要做的事情:

  • prepare

    • 将 redo log 写入 redo log buffer;

    • 获取上一个事务最大的 sequence number 时间戳;

    • 将事务状态设置为 prepare,具体是将 trx_state_t 从 TRX_STATE_ACTIVE 修改为 TRX_STATE_PREPARED

    • 将 undo log segment 的状态 TRX_UNDO_STATE 从 TRX_UNDO_ACTIVE 修改为 TRX_UNDO_PREPARED

    • 将 XID 写入 undo log,然后生成并将 XID_EVENT 写入 binlog cache;

    • 针对 RC 事务隔离级别,释放 gap lock。

  • commit

    • redo log write + fsync

    • binlog write + fsync

    • 存储引擎层事务提交

其中 commit 阶段基于组提交拆分为三个阶段:

  • flush

    • redo log write + fsync,针对 prepare 状态的 redo log;

    • binlog write;

    • 生成 GTID、sequence_number、last_committed,然后生成并将 GTID_EVENT 写入 binlog 文件;

    • 如果 sync_binlog != 1,更新 binlog 位点,唤醒 DUMP 线程给从库发送 EVENT,与主从复制有关。

  • sync

    • binlog fsync;

    • 如果 sync_binlog == 1,更新 binlog 位点,唤醒 DUMP 线程给从库发送 EVENT,与主从复制有关;

    • 调用 after_sync,与半同步复制有关。

  • commit

    • 关闭 MVCC Read view

    • 持久化 GTID

    • 释放 insert undo log

    • 释放锁

    • 生成 gtid event;

    • 将事务状态更新为 TRX_STATE_COMMITTED_IN_MEMORY,表示事务已经完成了二阶段提交的 2 个阶段,还剩一些收尾工作没做,这种状态的事务修改的数据已经可以被其它事务看见了;

    • 存储引擎层事务提交;

    • 调用 after_commit,与半同步复制有关。

其中除了日志写入外,还主要包括主从复制与并行复制。

其中可以明确看到整个过程主要分为三个阶段,包括 flush、sync、commit

图片

最后,是组提交的具体实现。

前面提到,commit 细分为 3 个子阶段之后,每个子阶段会有一个队列用于记录哪些事务线程处于该子阶段,如下图所示。

图片

其中:

  • 组提交的原理是将 commit 阶段细分为多个子阶段,其中每个子阶段对应一个队列 + 锁;

  • 队列的作用是通过将多个事务分批,从而实现日志的批量写入与刷盘;

  • 锁的作用是通过给每个队列加锁,因此同一时间每种队列最多只有一个,从而保证多批事务的顺序执行;

  • 每个队列中事务的数量可能不一样,但是事务的顺序一样,保持事务加入到队列的顺序

下面结合源码分析组提交的实现,其中主要讲解流程,具体源码实现详见参考教程。

实现

trans_commit

事务提交的入口函数是 trans_commit,其中调用 ha_commit_trans 函数。

ha_commit_trans 函数中主要判断是否需要写入 GTID 信息,并开始两阶段提交。

int ha_commit_trans(THD *thd, bool all, bool ignore_global_read_lock)
{
  /*
    Save transaction owned gtid into table before transaction prepare
    if binlog is disabled, or binlog is enabled and log_slave_updates
    is disabled with slave SQL thread or slave worker thread.
  */
  // 判断是否需要写入 GTID 信息
  error= commit_owned_gtids(thd, all, &need_clear_owned_gtid);
  ...
  // 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);
    error= 1;
    goto end;
  }
 ...
}

其中:

  • 注释显示如果关闭 binlog,或者开启 binlog,但是关闭 log_slave_updates 将 GTID 写入 mysql.gtid_executed 表和 @@GLOBAL.GTID_EXECUTED 变量中,就像开启 binlog 一样;

  • MYSQL_BIN_LOG::commit 函数中调用 MYSQL_BIN_LOG::ordered_commit 函数实现组提交。其中:

    • 分为三个阶段(stage),分别对应组提交中 flush、sync、commit 阶段;

    • 每个阶段的进入都要调用 MYSQL_BIN_LOG::change_stage 函数进行阶段转换。

prepare

MYSQL_BIN_LOG::prepare 函数中调用 MYSQL_BIN_LOG::ha_prepare_low 函数,其中调用 ht->prepare 函数。

prepare 接口由存储引擎层实现,InnoDB 存储引擎初始化注册的函数原型显示 prepare 接口对应 innobase_xa_prepare 函数。

static int
<think>好的,我需要帮助用户找到PTA平台上的MySQL训练10-1的目及答案解析。根据用户提供的引用内容,现有的引用包括号10-58、10-209以及181-200中的目,但10-1并未直接出现。首先,我应该确认用户提到的10-1是否属于用户提供的引用中的某个部分,或者是否存在其他来源。 查看用户给出的引用[1]、[2]、[3],分别对应号10-58、10-209和181-200。显然,用户提供的引用中没有直接涵盖10-1的内容。因此,可能需要根据现有的目结构推测10-1的可能内容,或者建议用户检查号是否正确。此外,用户可能误解了目的编号方式,比如10-1可能属于另一个分类或章节。 接下来,我应该考虑PTA平台的常见型。例如,基础查询、条件筛选、连接操作等通常是MySQL训练的重点。例如,查询特定表的数据、使用聚合函数、子查询等。例如,用户提供的引用中涉及了子查询、更新操作中的错误处理,这可能提示10-1可能是一个基础查询目,比如简单的SELECT语句。 由于无法直接获取10-1的目,可能需要基于现有信息给出一个示例目,并提醒用户可能的目类型和解决方法。同时,建议用户确认号或提供更多信息以便更准确地帮助。此外,可以指导用户如何在PTA平台上查找特定目,比如使用搜索功能或联系平台支持。 最后,生成相关问,帮助用户扩展相关知识,比如如何编写基础查询、处理子查询中的常见错误等。</think>关于PTA平台MySQL训练10-1的具体目内容,根据现有的公开资料和用户提供的引用信息,目前无法直接提供该目的完整内容及解析。以下是基于相似型的推测性分析和学习建议: ### 1. 推测目类型 根据引用中出现的其他MySQL目(如10-58、10-209等),10-1可能属于以下类型之一: - **基础查询**:例如`SELECT`语句与条件筛选,如查询特定专业或日期的学生信息[^2] - **子查询应用**:可能涉及嵌套查询或聚合函数,如比较年龄、数量极值等[^2][^3] - **数据更新**:可能包含带子查询的`UPDATE`语句,需注意MySQL的更新限制 ### 2. 示例目与解析(假设) 假设目为: ```sql -- 10-1 查询所有选修了课程号为'C001'的学生姓名 SELECT sname FROM student WHERE sno IN (SELECT sno FROM sc WHERE cno = 'C001'); ``` **解析步骤**: 1. **子查询**:`SELECT sno FROM sc WHERE cno = 'C001'` 先找出选修C001课程的学生学号 2. **主查询**:通过`WHERE sno IN`匹配学生表中的姓名 3. **关联表**:假设存在`student`表(学生信息)和`sc`表(选课记录) ### 3. 学习建议 - **基础语法强化**:重点练习`SELECT`、`WHERE`、`JOIN`等基础语句 - **子查询陷阱**:注意MySQL不允许在`FROM`子句中直接更新目标表,需用临时表解决 - **平台操作**:在PTA库中按章节筛选“第10章”或使用关键词搜索 ### 4. 相似目参考 例如引用[2]的年龄比较查询: ```sql SELECT sname FROM stu WHERE birdate > (SELECT MAX(birdate) FROM stu WHERE mno=...) ``` 这体现了子查询与聚合函数的结合使用
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值