分布式事务主要应用领域主要体现在数据库领域, 微服务应用领域. 微服务应用领域一般是柔性事务, 不完全满足 ACID 特性, 特别是 I 隔离性, 比如说 saga 不满足隔离性, 主要是通过根据分支事务执行成功或失败, 执行相应的前滚的重试或者后滚的补偿操作来达成全局事务的最终一致性, 但是全局事务与全局事务之间没有隔离性.
笔者了解到的分布式事务方案有 2PC 的 XA 规范, 以及 Google 的 percolator 方案(TiDB 就采用这个实现, 本质上是基于全局时间戳的乐观锁版本校验).
MySQL 的 XA 应用场景分为外部 XA 与内部 XA, 内部 XA 用于 binlog 与 stroage engine 之间, 协调 binlog 与 redo 事务写入的原子性. 外部 XA 用于 MySQL 节点与 MySQL 节点之间, 协调跨物理库之间的原子性. 本文主要介绍外部 XA.
基于 MySQL 的 XA 两阶段事务提交 (2PC) 分布式事务, 需要一个事务协调器 (TransactionManager) 来接受应用提交的全局事务(Global Transaction), 全局事务经过 TM 的分解后, 分解成多个分支事务(Branch Transaction), 每个分支事务在具体的某个 MySQL 实例上运行, 其中 MySQL 作为资源管理器(Resource Manager).
在实际的分布式数据库的分布式事务的开发中, 一般选择 DBProxy 作为 TM 载体, 比如腾讯的 TDSQL 和阿里的 POLARDB-X 的分布式事务方案, 都是这样的实现.
XA 的 2PC 提交流程的主要处理逻辑在事务协调器(Transaction Manager), 一般选择 DBProxy 作为 TM 载体, 如果 DBProxy 用 Java 开发, 可以参考 Atomikos 的实现
2. XA 协作流程
图 2.1 XA 的 2PC 协作流程
XA 的 2PC 提交流程如图 2.1, 主要分为以下几个步骤.
1) App 发送 start global transaction 到 TM,TM 生成全局事务 ID,xid
2) App 发送 global transaction 语句到 TM,TM 根据具体的 Sharding 算法分解出 branch transaction, 并且发送到各个 MySQL 节点.
3) App 发送 commit 语句到 TM,TM 往各个 branch transaction 的 MySQL 节点发送 XA prepare'xid'语句.
4)TM 收集各个 Prepare 语句的响应, 如果各个响应都是 OK, 则向每个 branch transaction 的 MySQL 节点发送 XA commit'xid'语句, 如果各个 RM 响应有不 OK 的, 往每个 RM 上发送 XA rollback'xid'语句.
3. XA 优化与异常处理
优化 1: 持久化事务协调阶段的各个状态
TM 作为一个单点的事务协同器, 很有可能宕机, 出现单点故障. 其本身的职责主要是事务协调, 属于无状态的服务. 宕机重启后, 可以根据持久化的全局事务状态来恢复 TM 的执行逻辑, 所以, 需要将阶段的各个协调阶段以及该阶段中每个 RM 的执行状态持久化到独立的 DB 中, 多个 TM 共享一个持久化 DB. 具体的阶段有, prepare 阶段的子阶段有 branch_tansaction_ send,prepare_send,prepare_ack 阶段, commit 阶段的子阶段有 commit_send,commit_ack 阶段, 记录每个子阶段每个 RM 的执行状态
优化 2: 并行发送语句
在 branch_tansaction_ send,prepare_ send,commit_send 阶段, 如果 TM 往 RM 发送语句是串行执行的, 单个 global transaction 的执行时间加长, TM 的 TPS(每秒事务请求数)会降低, 可以在这些阶段将已生成的语句, 通过线程池并行发送到各个 RM,TM 同时同步等待语句的返回值, 延时大为降低.
异常 1:TM 在 prepare_send 阶段前宕机, 重启恢复后, 继续执行 prepare_send 动作.
异常 2:TM 在 prepare_send 阶段时宕机, 可能会有部分 RM 收到 prepare 语句, 部份没有收到, 重启后, 往收到 prepare 语句的 RM 发送 rollback 语句.
异常 3:TM 在 prepare_ack 阶段记录完各个 RM 的执行状态后宕机, 重启后, 根据日志状态发起 rollback 或者 commit 语句.
异常 4:TM 在 commit_send 阶段时宕机, 可能会有部分 RM 收到 commit 语句, 部份没有收到, 重启后, 往没有收到 commit 语句的 RM 发送 commit 语句.
异常 5:TM 在 commit_ ack 阶段记录完各个 RM 的执行状态后宕机, 重启后, 根据日志状态发起重试 commit 语句或者不操作.
异常 6:RM 超长时间没有收到 TM 的 rollback 或者 commit 语句, 一直持有记录锁, RM 要有自动 rollback 或者 commit 的功能.
4. 2PC 与 1PC 对比
XA 的两阶段提交, 直观感觉和 RM 的交互次数太多, RPC 次数太多, 影响单个全局事务的响应时间, TPS 肯定降低. 但是, prepare 阶段有存在的意义, 如果某个单机事务处于 prepare 状态, 一直没有 commit,MySQL 重启时, 进行崩溃恢复时, 如果 binlog 中没有该事务, 对该事务进行 rollback, 如果有, 则对该事务进行 commit.
XA 两阶段提交满足了事务的 ACID 属性, 原子性: 在 prepare 和 commit 阶段保障了事务的原子性. 隔离性: 通过 MySQL 原生的记录锁, 做到读写隔离. 持久性: 基于 MySQL 单机事务的 redo 实现了持久性. 一致性: 基于 MySQL 单机事务.
如果放弃 prepare 阶段, 只有 commit 阶段, 全局事务的原子性无法保障, 例如这个场景, 全局事务的部分分支事务 commit 成功, 另一部分分支事务 commit 失败, 此时全局事务就处于既不能 commit 成功, 也不能 rollback 成功, 因为已经成功 commit 的分支事务无法 rollback.
即使通过解析 binlog, 生成反向 SQL 进行补偿达到 rollback 的效果, 此时也会多产生一次交互, RPC 次数和两阶段提交是一样的了. 但是此时又引发一个新问题, 全局事务的隔离性难以保障, 因为另一个全局事务 2 可能会修改此时全局事务 1 的已经 commit 了的记录, 而全局事务 1 正在反向补偿同一条已经 commit 了的记录.
即使通过以下方法达到了隔离性, 只满足 Read Commited 隔离级别, Repeated Read 等隔离级别没有实现, 而且隔离的粒度比较大, 记录上的 Xid, 相当于一把记录写锁.
在每个记录上, 增加一个字段全局事务 ID(Xid), 只有满足以下两个条件之一方可访问该记录.
1)记录上 Xid 是本全局事务的 Xid,
2)记录上 Xid 不是本全局事务 ID, 且该 Xid 已经不活跃
总结, TM 和各个 RM 都处于完全正常的情况下, 1PC 的性能比起 2PC 会好, 尤其是 TPS. 但是在 RM 处于异常的场景下, 例如全局事务的部分分支事务 commit 成功, 另一部分分支事务 commit 失败. 1PC 的 TPS 可能跟 2PC 差不多.
5. XA 各个阶段的 MySQL 处理流程
上图为 XA 规范图, 规范中 xa_open 与 xa_close 不会频繁调用, TM 与 RM 要维持数据库长连接, 避免频繁的创建, 销毁数据库连接的开销.
上图 5.2 为 MySQL 内部 Xa 的流程图.
xa_start 与 xa_end 起到标识分支事务的作用, 具体由 MySQL 服务端 Sql_cmd_xa_start::trans_xa_start()函数与 Sql_cmd_xa_end::trans_xa_end()函数实现
Sql_cmd_xa_start::trans_xa_start()把 thd->get_transaction()->xid_state 设置为 XID_STATE::XA_ACTIVE 状态
Sql_cmd_xa_end::trans_xa_end()检查 thd->get_transaction()->xid_state 必须为 XID_STATE::XA_ACTIVE 状态
6. MySQL 源码跟踪
xa_prepare 内部函数调用流程
- mysql_execute_command()
- case SQLCOM_XA_PREPARE:
- res= lex->m_sql_cmd->execute(thd);
- Sql_cmd_xa_prepare::execute(THD *thd)
- Sql_cmd_xa_prepare::trans_xa_prepare(THD *thd)
- ha_prepare(THD *thd)
- innobase_xa_prepare
- trx_prepare_for_mysql(trx_t* trx)
- trx_prepare()
- trx_prepare_low()
trx_undo_set_state_at_prepare() 修改 undolog 状态为 prepare 状态
mlog_write_ulint() 写 redo buffer
mtr_commit(&mtr)将 redo buffer 写入 redo log file, 并将脏页挂载在 buffer pool 的 flushlist, 可以看出写 undo segment 也需要 redo 保护
View Code
xa_commit 内部流程
- mysql_execute_command()
- case SQLCOM_XA_COMMIT:
- res= lex->m_sql_cmd->execute(thd);
- Sql_cmd_xa_commit::execute(THD *thd)
- Sql_cmd_xa_commit::trans_xa_commit(THD *thd)
- MYSQL_BIN_LOG::commit
- ha_commit_low
- innobase_commit
- innobase_commit_low
- trx_commit_for_mysql()
- trx_commit()
- trx_commit_low()
- trx_commit_in_memory()
lock_trx_release_locks() 释放事务的记录锁
trx_flush_log_if_needed() 刷新 redo buffer 到 redo log
log_write_up_to(lsn, flush);
log_write_flush_to_disk_low() 具体刷盘动作
View Code
分支事务 update 处理流程
- mysql_execute_command()
- case SQLCOM_UPDATE:
- res= lex->m_sql_cmd->execute(thd);
- Sql_cmd_update::execute(THD *thd)
- try_single_table_update
- open_tables_for_query(THD *thd, TABLE_LIST *tables, uint flags)
- open_and_process_table
- open_table()
- mysql_update
- table->init_cost_model()
- ha_innobase::info
ha_innobase::info_low 获取统计信息
test_quick_select()根据代价模型, 获取开销最低的表访问方式, 如 range\table scan\index scan
ha_innobase::try_semi_consistent_read(true), 请求存储引擎开启半一致性读, 在 update 或者 delete 的语句中.
init_read_record 设置数据扫描方法, 如 rr_quick,rr_sequential
handler::ha_rnd_init
ha_innobase::rnd_init, 初始化 c
rr_sequential
handler::ha_rnd_next 扫描一条记录
ha_innobase::rnd_next() table scan 读取第一条记录
row_search_mvcc()
sel_set_rec_lock() 在一条记录上加锁
lock_clust_rec_read_check_and_lock 在聚集索引上加记录锁
lock_rec_lock 加记录锁
- handler::ha_update_row
- binlog_log_row
THD::binlog_update_row 记录 row 格式的 binlog
ha_innobase::update_row(old_row,new_row)
row_upd_clust_rec() 更新聚集索引记录
trx_undo_report_row_operation() 记录 undo 信息
trx_undo_assign_undo() 分配回滚段
trx_undo_page_report_modify() 在回滚段中记录聚集索引的更改
row_upd_rec_in_place() 更新操作写入聚集索引
row_upd_rec_in_place_log()更新操作写入 redo buffer
mtr_t::commit() 将 redo buffer 写入 redo 日志文件, 并将脏页挂载在 buffer pool 的 flushlist
View Code
来源: https://www.cnblogs.com/happytech/p/13345795.html