欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

详解MySQL-5.7.18多线程复制可能会导致主从数据不一致的bug

程序员文章站 2024-03-21 08:58:46
...

bug概述

从MySQL-5.7.19的Release Notes 中可以看到其修复了如下这个bug

  • Replication:
    In certain cases, the master could write to the binary log a last_committed value which was smaller than it should have been. This could cause the slave to execute in parallel transactions which should not have been, leading to inconsistencies or other errors. (Bug #84471, Bug #25379659)

意思是,在某些场景中,master在记录二进制日志时,某个事务记录的last_commited值比实际应该记录的值要小,这会导致从机在回放时,将本来不应该并行回放的事务,进行并行回放,因此可能会导致数据不一致,或者其他的错误.(bug编号:84471。)

bug详述

关于MySQL5.7基于逻辑时间戳的多线程回放的并行回放原理在此不多做解释。
从Release Notes中可以看到bug编号,所以可以到bug系统中查询这个bug的详细反馈信息,链接如下https://bugs.mysql.com/bug.php?id=84471,但是发现是个私有链接,不能访问。但是详细的信息我们可以在github上找到。可以在mysql-server这个项目中搜索25379659,可以搜到RD进行代码提交时的一些记录,并且可以找到针对这个bug做出了哪些代码上的修复。
如下
https://github.com/mysql/mysql-server/commit/da46a8987ac52e3cbb3d4eb6918c822513165bc9
官方解释如下:

  • ANALYSIS
    When committing a transaction, its last_commited is set to the value of
    max_committed_transaction. max_committed_transaction is the maximum
    sequence_number of committed transactions. It is maintained just before
    committing each transaction to engine. If its sequence_number is not
    SEQ_UNINIT then updates max_committed_transaction accordingly.
    However, it checked wrong sequence_number(the sequence_number of the
    leader thread’s transaction instead of the sequence_number of the transaction).
    That caused that max_committed_transaction was only updated in time for leader
    thread’s transaction. The update for following transactions were delay to
    finish_commit() which was after the transaction commited to engine.

意思为,在group commit的commit阶段,commit线程队列的head线程负责整个队列的commit操作,commit操作主要操作为更新全局的max_committed_transaction值,此值会影响新的事务在执行数据写入时获取到的last_commit值,也就是binlog中记录的last_commit值,然而,head线程在进行整个队列的commit操作中的如下代码中,出现了差错,如下:

 for (THD *head= first ; head ; head = head->next_to_commit)
  {
    DBUG_PRINT("debug", ("Thread ID: %u, commit_error: %d, flags.pending: %s",
                         head->thread_id(), head->commit_error,
                         YESNO(head->get_transaction()->m_flags.pending)));
    /*
      If flushing failed, set commit_error for the session, skip the
      transaction and proceed with the next transaction instead. This
      will mark all threads as failed, since the flush failed.

      If flush succeeded, attach to the session and commit it in the
      enginesbgc_after_enrolling_for_commit_stagebgc_after_enrolling_for_commit_stage.
    */
#ifndef DBUG_OFF
    stage_manager.clear_preempt_status(head);
#endif
    if (thd->get_transaction()->sequence_number != SEQ_UNINIT)
      update_max_committed(head);

问题点在

if (thd->get_transaction()->sequence_number != SEQ_UNINIT)
      update_max_committed(head);

thd是队首(当前)线程,首先检测此线程中事物的sequence_number是否为初始值,如果不是,则需要去执行update_max_committed(head),用来更新max_committed_transaction,其代码如下:

void MYSQL_BIN_LOG::update_max_committed(THD *thd)
{
  Transaction_ctx *trn_ctx= thd->get_transaction();
  max_committed_transaction.set_if_greater(trn_ctx->sequence_number);
  /*
    sequence_number timestamp is unneeded anymore, so it's cleared off.
  */
  trn_ctx->sequence_number= SEQ_UNINIT;

  DBUG_ASSERT(trn_ctx->last_committed == SEQ_UNINIT ||
              thd->commit_error == THD::CE_FLUSH_ERROR);
}

注意其中的trn_ctx->sequence_number= SEQ_UNINIT; 这表明,在第一次for循环过程中,将head线程中的trn_ctx->sequence_number做了初始化。并且更新了max_committed_transaction,但是在之后的循环中,在处理队列中的其他线程时,依然是拿当前线程在进行判断是否是初始化值,所以并不会去更新max_committed_transaction。而随后会进行存储引擎的提交操作

 /*
        storage engine commit
       */
      if (ha_commit_low(head, all, false))

这表明在存储引擎层进行提交,释放锁资源,释放undo等操作,行锁用于控制并发,释放行锁后,其他线程如果更新线程队列的非head线程写入的行数据,则其他线程获取到的last_commit值会小于非head线程的sequence_number,这表明对于相同数据行的写入操作在从库上可以并发的去执行,则有可能导致主从数据不一致,或者复制报错。

官方修复

起始修复起来非常简单,就是判断每一个线程的sequence_number,而不是当前线程。
详解MySQL-5.7.18多线程复制可能会导致主从数据不一致的bug

对于线上的影响

虽然存在这个问题,但是并不一定会带来数据不一致的风险。来假设场景

先insert,后update场景

这种情况下,如果因为如上bug,导致update先执行,则复制报错,不会出现数据不一致。

先update,再update

这种情况下,复制不会报错,update数据有可能被覆盖。