在MySQL5.6引入了GTID(Global Transaction Identifier)特性,它可以在集群中唯一标识一个事务,在MySQL主从复制时,从节点可以使用GTID来确定复制位点,用于取代使用binlog文件偏移量的传统方式,在发生主备切换时从节点可以自动在新主上找到正确的复制位置,大大简化了复杂复制拓扑下集群的维护,也减少了人为设置复制位点发生误操作的风险,另外,基于GTID的复制可以跳过已经执行过的事务,减少了数据发生不一致的风险。
【GTID的组成】 GTID在实现上是由 server_uuid + gno 组成的,例如:3E11FA47-71CA-11E1-9E33-C80AA9429562:23 。server_uuid 是在server启动时生成的128位的uuid, gno 是序列号(sequence number),在每台mysql服务器上从1开始顺序递增,是事务在该实例上的唯一标识。同一个实例上的GTID一般情况下是连续的,例如 3E11FA47-71CA-11E1-9E33-C80AA9429562:1-100 ,如果这组 GTIDs 来自不同的实例,各组实例之间用逗号分隔;如果gno有多个范围区间,则各组范围之间用冒号分隔,例如:3E11FA47-71CA-11E1-9E33-C80AA9429562:23, 3E11FA47-71CA-11E1-9E33-C80AA9429562:1-5 举个例子,show slave status时可以看到在slave上executed_gtid_set包含了两组GTID,分别是slave上自身执行gtid和和来自主节点上的事务GTID:
【GTID的生命周期】
【GTID的产生】 GTID是在事务提交阶段产生的,在Group Commit的Flush Stage阶段 MYSQL_BIN_LOG::process_flush_stage_queue 函数中会调用 assign_automatic_gtids_to_flush_group 为Commit Group中每个事务产生GTID,同一个Commit Group内由Leader线程负责为Group中所有事务产生GTID。 相应调用栈为:
MYSQL_BIN_LOG::ordered_commit() // stage #1 flushing transactions to binary log. |-process_flush_stage_queue |-assign_automatic_gtids_to_flush_group // Leader遍历queue |-gtid_state::generate_automatic_gtid |-get_automatic_gno // 产生gno |-acquire_ownership // 申请ownership
Gtid_state::generate_automatic_gtid 函数负责产生GTID并为线程申请该GTID的ownership,其中产生GTID的一个关键函数是 Gtid_state::get_automatic_gno ,它负责判断该实例上最小未被使用的事务序号作为GTID的gno:
rpl_gno Gtid_state::get_automatic_gno(rpl_sidno sidno) const { DBUG_TRACE; Gtid_set::Const_interval_iterator ivit(&executed_gtids, sidno); Gtid next_candidate = {sidno, sidno == get_server_sidno() ? next_free_gno : 1}; while (true) { const Gtid_set::Interval *iv = ivit.get(); rpl_gno next_interval_start = iv != nullptr ? iv->start : MAX_GNO; while (next_candidate.gno < next_interval_start && DBUG_EVALUATE_IF("simulate_gno_exhausted", false, true)) { DBUG_PRINT("debug", ("Checking availability of gno= %llu", next_candidate.gno)); if (owned_gtids.is_owned_by(next_candidate, 0)) return next_candidate.gno; next_candidate.gno++; } if (iv == nullptr || DBUG_EVALUATE_IF("simulate_gno_exhausted", true, false)) { my_error(ER_GNO_EXHAUSTED, MYF(0)); return -1; } if (next_candidate.gno <= iv->end) next_candidate.gno = iv->end; ivit.next(); }}
在产生完GTID后,会调用 Gtid_state::acquire_ownership 函数申请GTID ownership:
enum_return_status Gtid_state::generate_automatic_gtid( THD *thd, rpl_sidno specified_sidno, rpl_gno specified_gno, rpl_sidno *locked_sidno) {// ...
if (automatic_gtid.gno == 0) { automatic_gtid.gno = get_automatic_gno(automatic_gtid.sidno); if (automatic_gtid.sidno == get_server_sidno() && automatic_gtid.gno != -1) next_free_gno = automatic_gtid.gno + 1; }
if (automatic_gtid.gno != -1) acquire_ownership(thd, automatic_gtid); else ret = RETURN_STATUS_REPORTED_ERROR;// ...}
在分配GTID时,会从当前实例上可用的最小GTID开始单调递增分配,通常情况下一个实例上GTID的分配是不会产生空洞的,如果由于特殊情况(例如手动set gtid_next)使得GTID产生空洞,在使用AUTOMATIC模式分配GTID时也会从最小未被使用的GTID开始分配,从而消除空洞。 此处做个简单的演示:
# 此实例上已执行事务的GTID序号为1-119686mysql> show variables like '%gtid%';+----------------------------------+-----------------------------------------------+| Variable_name | Value |+----------------------------------+-----------------------------------------------+| binlog_gtid_simple_recovery | ON || enforce_gtid_consistency | ON || gtid_executed | d4255688-0718-11ec-9687-506b4b430198:1-119686 || gtid_executed_compression_period | 1000 || gtid_mode | ON || gtid_next | AUTOMATIC || gtid_owned | || gtid_purged | || session_track_gtids | OFF |+----------------------------------+-----------------------------------------------+9 rows in set (0.00 sec)
# 手动设置gtid_next=119690 (大于已分配的最大序号119686,使得产生空洞)mysql> set gtid_next='d4255688-0718-11ec-9687-506b4b430198:119690';mysql> update sbtest9 set k=k+1 where id=1;
# 此时已执行事务的GTID产生了空洞:d4255688-0718-11ec-9687-506b4b430198:1-119686:119690mysql> show variables like '%gtid%';+----------------------------------+------------------------------------------------------+| Variable_name | Value |+----------------------------------+------------------------------------------------------+| binlog_gtid_simple_recovery | ON || enforce_gtid_consistency | ON || gtid_executed | d4255688-0718-11ec-9687-506b4b430198:1-119686:119690 || gtid_executed_compression_period | 1000 || gtid_mode | ON || gtid_next | d4255688-0718-11ec-9687-506b4b430198:119690 || gtid_owned | || gtid_purged | || session_track_gtids | OFF |+----------------------------------+------------------------------------------------------+
# 恢复 gtid_next='automatic'mysql> set gtid_next='automatic';
# 执行一条update语句mysql> update sbtest9 set k=k+1 where id=1;
# 可以看到gtid是从最小未使用的gtid开始分配的,是119687而不是119691mysql> show variables like '%gtid%';+----------------------------------+------------------------------------------------------+| Variable_name | Value |+----------------------------------+------------------------------------------------------+| binlog_gtid_simple_recovery | ON || enforce_gtid_consistency | ON || gtid_executed | d4255688-0718-11ec-9687-506b4b430198:1-119687:119690 || gtid_executed_compression_period | 1000 || gtid_mode | ON || gtid_next | AUTOMATIC || gtid_owned | || gtid_purged | || session_track_gtids | OFF |+----------------------------------+------------------------------------------------------+
可以发现在automatic模式下GTID分配是从最小可用序号开始分配的,因此GTID的大小并不能代表事务实际执行的顺序。
【GTID的维护】 MySQL通过Gtid_state对象来整体维护GTID系统的正常运转,在实现中有一些常见的数据结构和变量:
在gtid_state中还有几个关键对象用于维护GTID的生命周期:
group_commit中相关调用栈:
MYSQL_BIN_LOG::ordered_commit() // stage #1 flushing transactions to binary log. |-process_flush_stage_queue |-fetch_and_process_flush_stage_queue |-ha_flush_logs |-assign_automatic_gtids_to_flush_group // Leader遍历queue |-gtid_state::generate_automatic_gtid |-get_automatic_gno // 产生gno |-acquire_ownership // 申请ownership,加入owned_gtids |-flush_thread_caches |-flush_cache_to_file // stage #2 Syncing binary log file to disk. // ...... // stage #3 Commit all transactions in order. |-change_stage // 该阶段受到binlog_order_commits参数限制 |-process_commit_stage_queue |-ha_commit_low |-gtid_state::update_commit_group // Leader遍历queue |-Gtid_state::update_gtids_impl_own_gtid // 从owned_gtids中删除,并加入executed_gtids |-process_after_commit_stage_queue |- stage_manager.signal_done | |-finish_commit // binlog_order_commits=0时,线程各自提交 |-Gtid_state::update_on_commit/update_on_rollback |-Gtid_state::update_gtids_impl |-Gtid_state::update_gtids_impl_own_gtid // 从owned_gtids中删除,并加入executed_gtids
本文从GTID的组成、生命周期、GTID的产生和维护4个方面对MySQL中的GTID实现进行了简单介绍。在主从复制中,GTID的出现大大降低了维护的复杂度,但由于gtid_state中维护各个不同GTID集合对象依赖于全局锁 global_sid_lock 和针对各个sidno的锁 sid_locks ,在高并发场景下可能存在锁冲突瓶颈,存在一定优化提升空间,这块txsql内核正在做相关优化,敬请期待。
https://dev.mysql.com/doc/refman/5.6/en/replication-gtids-concepts.html https://dev.mysql.com/doc/refman/8.0/en/replication-gtids.html
腾讯数据库技术团队对内支持QQ空间、微信红包、腾讯广告、腾讯音乐、腾讯新闻等公司自研业务,对外在腾讯云上依托于CBS+CFS的底座,支持TencentDB相关产品,如TDSQL-C(原CynosDB)、TencentDB for MySQL(CDB)、CTSDB、MongoDB、CES等。腾讯数据库技术团队专注于持续优化数据库内核和架构能力,提升数据库性能和稳定性,为腾讯自研业务和腾讯云客户提供“省心、放心”的数据库服务。此公众号旨在和广大数据库技术爱好者一起推广和分享数据库领域专业知识,希望对大家有所帮助。