从一个线上问题分析binlog与内部XA事务提交过程

1. 问题
业务上新增一条订单记录,用户接收到BinLake拉取的MySQL从库数据消息后,立刻根据消息内的订单号去查询同一个MySQL从库,发现有些时候没法查到该条数据,等待大约500ms~1000ms后再去查询数据库,能够查询到该条数据。
注: BinLake为京东商城数据库技术部自研的一套订阅和消费MySQL数据库binlog的组件,本例所描述的问题是业务方但愿根据订阅的binlog来获取实时订单等业务消息。
2. Binlog与内部XA
2.1. XA的概念
XA(分布式事务)规范主要定义了(全局)事务管理器(TM: Transaction Manager)和(局部)资源管理器(RM: Resource Manager)之间的接口。XA为了实现分布式事务,将事务的提交分红了两个阶段:也就是2PC (tow phase commit),XA协议就是经过将事务的提交分为两个阶段来实现分布式事务。
两阶段
1)prepare 阶段
事务管理器向全部涉及到的数据库服务器发出prepare"准备提交"请求,数据库收到请求后执行数据修改和日志记录等处理,处理完成后只是把事务的状态改为"能够提交",而后把结果返回给事务管理器。即:为prepare阶段,TM向RM发出prepare指令,RM进行操做,而后返回成功与否的信息给TM。
2)commit 阶段
事务管理器收到回应后进入第二阶段,若是在第一阶段内有任何一个数据库的操做发生了错误,或者事务管理器收不到某个数据库的回应,则认为事务失败,回撤全部数据库的事务。数据库服务器收不到第二阶段的确认提交请求,也会把"能够提交"的事务回撤。若是第一阶段中全部数据库都提交成功,那么事务管理器向数据库服务器发出"确认提交"请求,数据库服务器把事务的"能够提交"状态改成"提交完成"状态,而后返回应答。即:为事务提交或者回滚阶段,若是TM收到全部RM的成功消息,则TM向RM发出提交指令;否则则发出回滚指令。
外部与内部XA
MySQL中的XA实现分为:外部XA和内部XA。前者是指咱们一般意义上的分布式事务实现;后者是指单台MySQL服务器中,Server层做为TM(事务协调者,一般由binlog模块担当),而服务器中的多个数据库实例做为RM,而进行的一种分布式事务,也就是MySQL跨库事务;也就是一个事务涉及到同一条MySQL服务器中的两个innodb数据库(目前彷佛只有innodb支持XA)。内部XA也能够用来保证redo和binlog的一致性问题。
2.2. redo与binlog的一致性问题
咱们MySQL为了兼容其它非事务引擎的复制,在server层面引入了 binlog, 它能够记录全部引擎中的修改操做,于是能够对全部的引擎使用复制功能; 然而这种状况会致使redo log与binlog的一致性问题;MySQL经过内部XA机制解决这种一致性的问题。
第一阶段:InnoDB prepare, write/sync redo log;binlog不做任何操做;
第二阶段:包含两步,1> write/sync Binlog; 2> InnoDB commit (commit in memory);
固然在5.6以后引入了组提交的概念,能够在IO性能上进行一些提高,但整体的执行顺序不会改变。
当第二阶段的第1步执行完成以后,binlog已经写入,MySQL会认为事务已经提交并持久化了(在这一步binlog就已经ready而且能够发送给订阅者了)。在这个时刻,就算数据库发生了崩溃,那么重启MySQL以后依然能正确恢复该事务。在这一步以前包含这一步任何操做的失败都会引发事务的rollback。
第二阶段的第2大部分都是内存操做,好比释放锁,释放mvcc相关的read view等等。MySQL认为这一步不会发生任何错误,一旦发生了错误那就是数据库的崩溃,MySQL自身没法处理。这个阶段没有任何致使事务rollback的逻辑。在程序运行层面,只有这一步完成以后,事务致使变动才能经过API或者客户端查询体现出来。
下面的一张图,说明了MySQL在什么时候会将binlog发送给订阅者。
从一个线上问题分析binlog与内部XA事务提交过程
理论上来讲,也能够在commit阶段完成以后再将binlog发送给订阅者,但这样会增大主从延迟的风险。
3. 相关代码数据库

  1. int MYSQL_BIN_LOG::ordered_commit(THD *thd, bool all, bool skip_commit) {
  2. .....
  3. //进入flush stage,
  4. change_stage(thd, Stage_manager::FLUSH_STAGE, thd, NULL, &LOCK_log);
  5. ....
  6. //通知底层存储引擎日志刷盘
  7. process_flush_stage_queue(&total_bytes, &do_rotate, &wait_queue);
  8. .....
  9. //将各个线程的binlog从cache写到文件中
  10. flush_cache_to_file(&flush_end_pos);
  11. ....
  12. //进入到Sync stage
  13. change_stage(thd, Stage_manager::SYNC_STAGE, wait_queue, &LOCK_log,
  14. &LOCK_sync));
  15. //binlog fsync落盘
  16. sync_binlog_file(false)
  17. //通知binlog发送线程,有新的binlog落盘能够发送到订阅者了
  18. update_binlog_end_pos(tmp_thd->get_trans_pos());
  19. //进入commit state
  20. change_stage(thd, Stage_manager::COMMIT_STAGE, final_queue,
  21. leave_mutex_before_commit_stage, &LOCK_commit);
  22. ....
  23. //事务状态提交
  24. process_commit_stage_queue(thd, commit_queue);
  25. ....
    }

其中,在update_binlog_end_pos以后,binlog发送线程就已经能够读取最新的binlog发送给订阅者了。当订阅者收到这些binlog以后若是process_commit_stage_queue由于系统调度等缘由还未执行完成,那么订阅者碰巧在此时发起问题中所描述的查询,就会发生查询不到的状况。
下面咱们看一下process_commit_stage_queue都作了什么。
在process_commit_stage_queue会分别调用到binlog的commit方法binlog_commit和innodb的commit函数trx_commit_in_memory。服务器

  1. static int binlog_commit(handlerton , THD , bool) {
  2. DBUG_ENTER("binlog_commit");
  3. /*
  4. Nothing to do (any more) on commit.
  5. */
  6. DBUG_RETURN(0);
  7. }
    在binlog_commit中什么也不作,由于跟binlog有关的操做前面都已经作完了。
    最后看一下存储引擎innodb的trx_commit_in_memory都干了什么。
  8. static void trx_commit_in_memory(
  9. trx_t trx, /!< in/out: transaction */
  10. const mtr_t mtr, /!< in: mini-transaction of
  11. trx_write_serialisation_history(), or NULL if
  12. the transaction did not modify anything */
  13. bool serialised)
  14. /*!< in: true if serialisation log was
  15. written */
  16. {
  17. ....
  18. //释放锁
  19. lock_trx_release_locks(trx);
  20. ut_ad(trx_state_eq(trx, TRX_STATE_COMMITTED_IN_MEMORY));
  21. .....
  22. //释放mvcc相关的read view
  23. if (trx->read_only || trx->rsegs.m_redo.rseg == NULL) {
  24. MONITOR_INC(MONITOR_TRX_RO_COMMIT);
  25. if (trx->read_view != NULL) {
  26. trx_sys->mvcc->view_close(trx->read_view, false);
  27. }
  28. } else {
  29. ut_ad(trx->id > 0);
  30. MONITOR_INC(MONITOR_TRX_RW_COMMIT);
  31. }
  32. }
  33. ....
  34. //清理insert操做相关的undo log(注意,此时只有insert的undo须要清理)
  35. if (mtr != NULL) {
  36. if (trx->rsegs.m_redo.insert_undo != NULL) {
  37. trx_undo_insert_cleanup(&trx->rsegs.m_redo, false);
  38. }
  39. if (trx->rsegs.m_noredo.insert_undo != NULL) {
  40. trx_undo_insert_cleanup(&trx->rsegs.m_noredo, true);
  41. }
  42. }

这一步完成以后,在运行时刻事务的变动才能被查询到。但须要记住,MySQL在binlog落盘成功后就认为事务的持久化已经完成。
30. 总结
在binlog落盘以后,MySQL就会认为事务的持久化已经完成(在这个时刻以后,就算数据库发生了崩溃均可以在重启后正确的恢复该事务)。可是该事务产生的数据变动被别的客户端查询出来还须要在commit所有完成以后。MySQL会在binlog落盘以后会当即将新增的binlog发送给订阅者以尽量的下降主从延迟。但因为多线程时序等缘由,当订阅者在收到该binlog以后当即发起一个查询操做,可能不会查询到任何该事务产生的数据变动(由于此时该事务所处线程可能还没有完成最后的commit步骤)。
若是应用须要根据binlog做为一些业务逻辑的触发点,仍是须要考虑引入一些延时重试机制或者从新考虑合适的实现架构。多线程

本文由京东商城数据库技术部王治提供。架构

相关文章
相关标签/搜索