MySQL · 源码阅读 · 内部XA事务

概述

MySQL是一个支持多存储引擎架构的数据库,除了早期默认的存储引擎myisam,目前使用比较多的引擎包括InnoDB,XEngine以及Rocksdb等,这些引擎都是支持事务的引擎,在数据库系统中,存储引擎支持事务基本是标配,所以其它引擎也就慢慢边缘化了。由于支持多事务引擎,为了保证事务一致性,MySQL实现了经典的XA标准,通过XA事务来保证事务的特征。binlog作为MySQL生态的一个重要组件,它记录了数据库操作的逻辑更新,并作为数据传输纽带,可以搭建复杂的MySQL集群,以及同步给下游。除了作为传输纽带,binlog还有一个角色就是XA事务的协调者,协调各个参与者(存储引擎)来实现XA事务的一致性。

XA事务

MySQL的XA事务支持包括内部XA事务和外部XA事务。内部XA事务主要指单节点实例内部,一个事务跨多个存储引擎进行读写,那么就会产生内部XA事务;这里需要指出的是,MySQL内部每个事务都需要写binlog,并且需要保证binlog与引擎修改的一致性,因此binlog是一个特殊的参与者,所以在打开binlog的情况下,即使事务修改只涉及一个引擎,内部也会启动XA事务。外部XA事务与内部XA事务核心逻辑类似,提供给用户一套XA事务的操作命令,包括XA start, XA end,XA prepre和XA commit等,可以支持跨多个节点的XA事务。外部XA的协调者是用户的应用,参与者是MySQL节点,因此需要应用持久化协调信息,解决事务一致性问题。无论外部XA事务还是内部XA事务,存储引擎实现的prepare和commit接口都是同一条路径,本文重点介绍内部XA事务。

协调者

协调者的选择

MySQL内部XA事务,存储引擎是参与者,而协调者则有3个选项,包括binlog,TC_LOG_MMAP和TC_LOG_DUMMY。如果开启binlog,由于每个事务至少涉及一个存储引擎的修改,加上binlog,所以也会走XA事务流程。如果关闭binlog,事务修改涉及多个存储引擎,比如innodb和xengine引擎,那么内部会采用tc_log_map作为协调者。如果关闭binlog,且修改只涉及一个引擎innodb,那么实际上就不是XA事务,mysql内部为了保证接口统一,仍然使用了一个特殊的协调者TC_LOG_DUMMY,TC_LOG_DUMMY实际上什么也没做,只是做简单的转发,将server层的调用路由到引擎层调用,仅此而已。

  1. //协调者选择的逻辑
  2. if (total_ha_2pc > 1 || (1 == total_ha_2pc && opt_bin_log))
  3. {
  4. if (opt_bin_log)
  5. tc_log= &mysql_bin_log;
  6. else
  7. tc_log= &tc_log_mmap;
  8. }
  9. else
  10. tc_log= &tc_log_dummy

协调者逻辑

  1. //binlog,tc_log_mmap和tc_log_dummy作为协调者的基本逻辑
  2. binlog作为协调者:
  3. prepareha_prepare_low
  4. commit write-binlog + ha_comit_low
  5. tclog作为协调者:
  6. prepareha_prepare_low
  7. commitwrtie-xid + ha_commit_low
  8. tc_dummy作为协调者:
  9. prepareha_prepare_low
  10. commitha_commit_low
  11. //是否支持2PC,是否修改超过了1个以上的引擎
  12. if (!trn_ctx->no_2pc(trx_scope) && (trn_ctx->rw_ha_count(trx_scope) > 1))
  13. error = tc_log->prepare(thd, all);

执行2PC依据

TC_LOG_MMAP和binlog作为协调者本质是相同的,就是在涉及跨引擎事务时,走2PC事务提交流程,分别调用引擎的prepare接口和commit接口。协调者如何确认是否走2PC逻辑,这里主要根据事务修改是否涉及多个引擎,特殊的是,如果打开binlog,binlog也会作为参与者考虑在内,最终统计事务涉及修改的参与者是否超过1,如果超过1,则进行2PC提交流程(prepare,commit)。注意,这里有一个前提条件是涉及的修改引擎必需都支持2PC。

  1. struct THD_TRANS {
  2. /* true is not all entries in the ht[] support 2pc */
  3. bool m_no_2pc;
  4. /* number of engine modify */
  5. int m_rw_ha_count;
  6. /* storage engines that registered in this transaction */
  7. Ha_trx_info *m_ha_list;
  8. }
  9. //统计打标,是否涉及到多个引擎的修改。
  10. ha_check_and_coalesce_trx_read_only(bool all) {
  11. //统计打标
  12. for (ha_info = ha_list; ha_info; ha_info = ha_info->next()) {
  13. if (ha_info->is_trx_read_write()) ++rw_ha_count;
  14. //语句级统计
  15. if (!all) {
  16. Ha_trx_info *ha_info_all =
  17. &thd->get_ha_data(ha_info->ht()->slot)->ha_info[1];
  18. DBUG_ASSERT(ha_info != ha_info_all);
  19. /*
  20. Merge read-only/read-write information about statement
  21. transaction to its enclosing normal transaction. Do this
  22. only if in a real transaction -- that is, if we know
  23. that ha_info_all is registered in thd->transaction.all.
  24. Since otherwise we only clutter the normal transaction flags.
  25. */
  26. //将语句级的读写修改,同步到事务级的读写修改
  27. if (ha_info_all->is_started()) /* false if autocommit. */
  28. ha_info_all->coalesce_trx_with(ha_info);
  29. } else if (rw_ha_count > 1) {
  30. /*
  31. It is a normal transaction, so we don't need to merge read/write
  32. information up, and the need for two-phase commit has been
  33. already established. Break the loop prematurely.
  34. */
  35. break;
  36. }
  37. }
  38. }

参与者

mysql内部XA事务中,参与者主要指事务型存储引擎。mysql根据引擎是否提供了prepare接口,判断引擎是否支持2PC。引擎的prepare和commit接口有一个bool类型的参数,主要含义是这次prepare/commit是语句级别,还是事务级别。事务的2PC提交流程主要都发生在事务级别,但有一个特殊场景,就是autocommit场景下的单SQL语句,这种会触发自动提交,如果这个SQL语句的修改涉及多个引擎,也会走到2PC流程。主要逻辑如下:

  1. prepare逻辑:
  2. ha_prepare(bool prepare_tx) 这里的prepare_tx由外面传递的all=true/false决定。
  3. if (prepare_tx || (!my_core::thd_test_options(thd, OPTION_NOT_AUTOCOMMIT | OPTION_BEGIN))) {
  4. tx->prepare
  5. }
  6. commit逻辑:
  7. ha_commit(bool commit_tx) 这里的commit_tx由外面传递的all=true/false决定。
  8. if (commit_tx || (!my_core::thd_test_options(thd, OPTION_NOT_AUTOCOMMIT | OPTION_BEGIN))) {
  9. tx->commit
  10. }

XA事务存储引擎接口

  1. innobase_hton->commit = innobase_commit;
  2. innobase_hton->rollback = innobase_rollback;
  3. innobase_hton->prepare = innobase_xa_prepare;
  4. innobase_hton->recover = innobase_xa_recover;
  5. innobase_hton->commit_by_xid = innobase_commit_by_xid;
  6. innobase_hton->rollback_by_xid = innobase_rollback_by_xid;

Server层与引擎层交互

从前面协调者逻辑我们了解到,MySQL内部XA事务,协调者在Server层,参与者在引擎层,因此Server层和引擎层需要有一定的通信机制来确定是否要进行2PC提交。这里主要包括两方面,一个是,事务涉及到的引擎要注册到协调者的事务列表中,二是,如果引擎有修改,要将已修改的信息通知给协调者。在MySQL中主要通过两个接口来实现,xengine_register_tx/innodbase_register_tx注册事务,handler::mark_trx_read_write标记事务读写。

DML事务

注册事务路径

server根据需要访问表进行注册事务。

  1. mysql_lock_tables
  2. lock_external
  3. handler::ha_external_lock
  4. ha_innobase::external_lock
  5. innobase_register_trx
  6. trans_register_ha
  7. Transaction_ctx::set_ha_trx_info
  8. void xengine_register_tx(handlerton *const hton, THD *const thd,
  9. Xdb_transaction *const tx) {
  10. DBUG_ASSERT(tx != nullptr);
  11. //注册stmt的trx信息
  12. trans_register_ha(thd, FALSE, xengine_hton, NULL);
  13. //显示开启的事务,ddl默认将AUTOCOMMIT关掉,符合条件
  14. if (my_core::thd_test_options(thd, OPTION_NOT_AUTOCOMMIT | OPTION_BEGIN)) {
  15. tx->start_stmt();
  16. trans_register_ha(thd, TRUE, xengine_hton, NULL);
  17. }
  18. }

标记事务修改

  1. handler::ha_delete_row
  2. handler::ha_write_row
  3. handler::ha_update_row
  4. handler::mark_trx_read_write
  5. /**
  6. A helper function to mark a transaction read-write,
  7. if it is started.
  8. */
  9. void handler::mark_trx_read_write() {
  10. Ha_trx_info *ha_info = &ha_thd()->get_ha_data(ht->slot)->ha_info[0];
  11. /*
  12. When a storage engine method is called, the transaction must
  13. have been started, unless it's a DDL call, for which the
  14. storage engine starts the transaction internally, and commits
  15. it internally, without registering in the ha_list.
  16. Unfortunately here we can't know know for sure if the engine
  17. has registered the transaction or not, so we must check.
  18. */
  19. if (ha_info->is_started()) {
  20. DBUG_ASSERT(has_transactions());
  21. /*
  22. table_share can be NULL in ha_delete_table(). See implementation
  23. of standalone function ha_delete_table() in sql_base.cc.
  24. */
  25. if (table_share == NULL || table_share->tmp_table == NO_TMP_TABLE) {
  26. /* TempTable and Heap tables don't use/support transactions. */
  27. ha_info->set_trx_read_write();
  28. }
  29. }
  30. }

DDL事务

对于ddl事务,由于涉及到字典的多次修改,为了避免中途提交,临时将自动提交关闭。

  1. /**
  2. Check if statement (typically DDL) needs auto-commit mode temporarily
  3. turned off.
  4. @note This is necessary to prevent InnoDB from automatically committing
  5. InnoDB transaction each time data-dictionary tables are closed
  6. after being updated.
  7. */
  8. static bool sqlcom_needs_autocommit_off(const LEX *lex) {
  9. return (sql_command_flags[lex->sql_command] & CF_NEEDS_AUTOCOMMIT_OFF) ||
  10. (lex->sql_command == SQLCOM_CREATE_TABLE &&
  11. !(lex->create_info->options & HA_LEX_CREATE_TMP_TABLE)) ||
  12. (lex->sql_command == SQLCOM_DROP_TABLE && !lex->drop_temporary);
  13. }
  14. /*
  15. For statements which need this, prevent InnoDB from automatically
  16. committing InnoDB transaction each time data-dictionary tables are
  17. closed after being updated.
  18. */
  19. Disable_autocommit_guard(THD *thd) {
  20. m_thd->variables.option_bits &= ~OPTION_AUTOCOMMIT;
  21. m_thd->variables.option_bits |= OPTION_NOT_AUTOCOMMIT;
  22. }

ddl注册事务

所有dml操作,都会通过mysql_lock_tables路径来进行注册事务操作,但对于ddl,由于有些操作只涉及数据字典的修改,server层认为不涉及引擎层修改,则不会显示注册事务。xengine通过原子ddl日志和2PC支持xengine表的ddl,需要显示注册事务,通知server层。

ddl标记事务修改

除了主动在server层注册事务,还需要主动将事务标记为read-write,标识这个ddl中xengine引擎有修改,这样server层在统计修改的事务引擎数时,会将xengine计算在內,最后再抉择是采用1PC事务提交还是2PC事务提交。目前,实际上在handler层的所有ddl路径,都主动调用了接口mark_trx_read_write,但由于在之前,并没有将引擎注册到server,导致整个调用对部分DDL操作无效。

典型场景分析

这里考虑不开binlog的场景,因为开binlog情况下,任何一个事务只要有更新,加上binlog就会走内部XA事务。不开binlog场景下,如果同时启用xengine和innodb引擎,根据事务实际情况,可能会走到2PC流程。

| 1 | 场景 | 类别 | 是否走2PC流程 | 备注 | | — | — | — | — | — |
| 2 | (one-stmt)+(modify xengine) | DML事务 | no | 隐式事务,autocommit=on,单语句自动提交事务|
| 3 | (one-stmt)(modify xengine+innodb) | | yes | 隐式事务,autocommit=on,单语句自动提交事务 |
| 4 | (multi-stmt)+(modify xengine) | | no | 显示事务, 结合begin/commit |
| 5 | (multi-stmt)+(modify xengine+innodb) | | yes | 显示事务, 结合begin/commit |
| 6 | create table | DDL事务 | yes | storage engine mark_read_write |
| 7 | drop table | | yes | storage engine mark_read_write |
| 8 | rename table | | yes | storage engine mark_read_write |
| 9 | alter table online | | yes | |
| 10 | alter table copy-offline | | yes | |
说明,目前tc_log作为协调者,对于双引擎XA事务在部分路径存在问题。比如,对于场景3,应该走2PC流程没有走;对于场景4,不需要走2PC流程的场景反而走了2PC。