Skip to content

Canal 已经收到 Binlog,为什么 MySQL 主库还查不到数据? ​

在基于 Canal 的数据同步链路里,有一个很反直觉的现象:

text
应用完成一次数据库更新
        ↓
Canal 收到 Binlog 并发送 MQ
        ↓
消费者收到消息后立即回查 MySQL 主库
        ↓
查询不到刚更新的数据,或者仍然读到旧值

第一反应通常是主从延迟、缓存没失效、SQL 路由错了,或者事务隔离级别导致旧读。

但如果已经确认查询走的是主库,没有读缓存,也没有复用旧事务快照,问题就会变得很奇怪:

既然 Canal 已经读到了事务的 Binlog,为什么同一个 MySQL 里的其他会话还看不到数据?

为了确认这件事,我搭了一套只有一个 MySQL 实例的复现环境,并在一台 2 核 2 GiB 的服务器上做了实际验证。

结果是:这个现象确实可以稳定出现。

先说结论 ​

一句话概括:

Binlog 中已经出现完整事务,不代表 InnoDB 的最终提交和数据可见性处理已经完成。

MySQL 的事务提交不是一个瞬间完成的动作,而是由多个阶段组成:

text
InnoDB Prepare
    ↓
写入并同步 Binlog
    ↓
发布新的 Binlog End Position
    ↓
InnoDB 最终 Commit
    ↓
释放锁、对其他会话可见
    ↓
COMMIT 请求返回

Binlog 消费端关注的是 Binlog 已经写到哪里,其他 SQL 会话关注的是事务是否完成了存储引擎提交。

这两个时点并不完全重合。

普通异步消费中的窗口通常非常短;半同步 AFTER_SYNC 会在 Binlog 同步后、InnoDB Commit 前等待 ACK,因此可以把窗口明显放大。

复制模式也需要先统一口径:

  • 异步复制不等待备库
  • 半同步复制只要求至少一个备库确认收到并记录日志,不等于备库已经回放
  • “强同步”不是统一的 MySQL 标准模式,ACK 边界由具体产品定义
  • 云厂商内部主备同步与 Canal 的 Binlog 消费通常是两条独立链路

一、“事务已经提交”到底指什么 ​

这个问题容易产生争议,是因为大家对“提交”的理解不一样。

至少要区分三个时点:

时点代表什么
Binlog 写入事务结束事件Binlog 已形成完整事务边界
Binlog 和 Redo 满足恢复条件崩溃恢复时能够完成一致性恢复
InnoDB 最终 Commit 完成事务状态切换、锁释放,对其他会话可见

Canal 收到 XIDEvent,能够证明 Binlog 已经记录了事务结束,但不能单独证明 InnoDB 已经完成最后一步。

所以,更准确的描述不是:

text
Canal 读取到了一个未提交事务

而是:

text
Binlog 已经记录并发布了事务提交边界,
但 InnoDB 的运行时提交过程可能还没有结束

这两种表述看起来相似,技术含义却完全不同。

二、MySQL 源码里的真实顺序 ​

InnoDB 有自己的 Redo Log,MySQL Server 层还有 Binlog。

一次事务提交时,两套日志必须保持一致:

  • 不能 Redo 已提交,但 Binlog 没有记录
  • 不能 Binlog 记录了事务,但 InnoDB 恢复后却回滚

因此,MySQL 使用两阶段提交协调 Redo Log 和 Binlog:

text
第一阶段:InnoDB Prepare
    ↓
写 Binlog
    ↓
Flush / Sync Binlog
    ↓
第二阶段:InnoDB Commit

MySQL 5.7 的核心事务组提交逻辑位于:

text
sql/binlog.cc
MYSQL_BIN_LOG::ordered_commit()

整个流程又被拆成三个主要阶段:

text
FLUSH_STAGE
    写入 Binlog Cache
    Flush Binlog

SYNC_STAGE
    fsync Binlog
    更新 Binlog End Position
    执行 after_sync Hook

COMMIT_STAGE
    调用 ha_commit_low()
    完成存储引擎 Commit

把关键函数按执行顺序排列,大致是:

text
flush_cache_to_file()
        ↓
sync_binlog_file()
        ↓
update_binlog_end_pos()
        ↓
call_after_sync_hook()
        ↓
process_commit_stage_queue()
        ↓
ha_commit_low()

最值得注意的是:

text
update_binlog_end_pos()

发生在:

text
ha_commit_low()

之前。

update_binlog_end_pos() 会更新 Binlog 当前可发送的位置,并唤醒等待新事件的 Binlog Dump 线程。

也就是说,Dump 客户端可能已经看到新的 Binlog 位置,而存储引擎 Commit Stage 还没有执行完。

这就是普通异步 Binlog 消费也存在理论竞态窗口的原因。

三、半同步 AFTER_SYNC 如何放大窗口 ​

MySQL 半同步复制有两个重要等待点:

text
AFTER_SYNC
AFTER_COMMIT

两种模式的核心差别是 ACK 等在哪里。

AFTER_COMMIT ​

text
Flush / Sync Binlog
        ↓
InnoDB Commit
        ↓
事务对其他会话可见
        ↓
等待半同步 ACK

AFTER_SYNC ​

text
Flush / Sync Binlog
        ↓
发送 Binlog
        ↓
等待半同步 ACK
        ↓
InnoDB Commit
        ↓
事务对其他会话可见

MySQL 5.7 半同步插件位于:

text
plugin/semisync/semisync_master_plugin.cc

当等待点为 AFTER_SYNC 时,插件会在 after_sync Hook 中调用 commitTrx() 等待 ACK。

而 after_sync Hook 正好位于 Binlog 同步之后、存储引擎 Commit 之前。

只要 ACK 没有返回,提交线程就会停在这里:

text
Binlog 已经可以被消费
        ↓
等待 ACK
        ↓
InnoDB 尚未最终 Commit

这也是本次实验能够确定性复现的关键。

四、为什么 MySQL 5.7 默认改成 AFTER_SYNC ​

MySQL 5.7.2 将半同步等待点的默认行为从 AFTER_COMMIT 改成了 AFTER_SYNC。

这样调整的主要目的是改善故障切换时的数据一致性。

如果使用 AFTER_COMMIT:

text
主库完成 InnoDB Commit
        ↓
其他业务会话已经能看到数据
        ↓
半同步接收端还没确认收到 Binlog
        ↓
主库突然故障并发生切换

业务可能遇到一种很难解释的情况:刚刚查询到的数据,切换到新主库后却不存在了。

AFTER_SYNC 把 ACK 等待移动到引擎 Commit 前:

text
至少一个接收端确认收到 Binlog
        ↓
主库完成 InnoDB Commit
        ↓
事务才对其他会话可见

它改善了故障切换语义,但也让“Binlog 已经送出、主库事务还没最终提交”的窗口更容易被观察到。

五、异步、半同步和“同步”到底差在哪 ​

讨论云数据库之前,必须先把“同步”这个词拆开。

一条事务从主库传到备库,至少会经过下面几个阶段:

text
主库生成并持久化 Binlog
        ↓
备库接收到 Binlog
        ↓
备库写入 Relay Log
        ↓
备库 SQL / Applier 线程执行事务
        ↓
备库 InnoDB Commit
        ↓
备库查询能够看到数据

不同复制模式的核心差别,不是名字里有没有“同步”,而是:

主库在向应用返回成功之前,要求远端节点走到上面哪一步。

1. 异步复制 ​

异步复制不等待备库 ACK:

text
主库完成本地提交
        ↓
立即向应用返回成功
        ↓
Binlog 后续异步发送给备库

它的优点是写入延迟低,备库异常通常不会阻塞主库。

代价是主库故障时,尚未发送或尚未落到备库的事务可能丢失。备库查询也天然存在延迟,只能提供最终一致性。

2. 半同步复制 ​

半同步复制会要求至少一个备库返回 ACK:

text
主库发送 Binlog
        ↓
至少一个备库接收并记录到 Relay Log
        ↓
备库返回 ACK
        ↓
主库向应用返回成功

这里最容易被误解的是:

半同步 ACK 通常只证明备库已经接收并记录了事务日志,不证明备库 SQL 线程已经执行,更不证明备库查询已经可见。

MySQL 原生半同步还存在超时退化机制。主库等待超过 rpl_semi_sync_source_timeout 后,可以退化为异步复制,避免备库故障无限阻塞写入。MySQL 5.7 对应的旧变量名是 rpl_semi_sync_master_timeout。

因此,半同步是在数据安全和写入可用性之间做折中,不是严格意义上的全同步提交。

3. “强同步”或“全同步” ​

传统定义中的同步复制,是主库必须等待指定远端节点到达约定的提交屏障,才能向客户端返回成功。

但云厂商所说的“强同步”并不是一个统一的 MySQL 标准术语。不同产品的 ACK 可能分别代表:

text
收到 Binlog
写入 Relay Log
多数节点收到事务
多数存储副本完成写入
备库已经执行并 Commit

这些语义并不等价。

有些厂商所谓的强同步,本质上是:

text
半同步 ACK
    +
不允许自动退化为异步
    +
等待一个或多个备节点

它可以提高故障切换时的数据安全,但仍不必然代表备库已经完成事务回放。

Oracle MySQL 传统 InnoDB 主从复制原生提供的是异步和半同步机制,并没有一个“等待所有普通 Replica 完成 InnoDB Commit 后再返回”的经典复制模式。MySQL NDB Cluster 的同步数据节点复制属于另一套存储引擎和集群架构,也不能与普通 InnoDB 主从直接类比。

所以看到“强同步”三个字时,至少要继续确认:

  1. ACK 代表收到日志、持久化日志,还是已经执行事务
  2. 主库需要等待一个节点、全部节点,还是多数派节点
  3. 备节点异常时会阻塞写入,还是自动退化为异步
  4. 同步发生在 Binlog 层、数据库引擎层,还是分布式存储层

4. 三种模式放在一起比较 ​

模式主库是否等待远端ACK 通常代表什么备端是否已经可查询故障时的典型行为
异步复制不等待没有远端 ACK 要求不保证主库继续写入,切换时可能丢失未复制事务
半同步复制等待至少一个备端收到并记录事务日志不保证超时后通常可退化为异步
强同步 / 同步复制取决于产品定义可能是日志接收、多数派确认或事务执行完成必须查产品定义通常不静默退化,可能阻塞写入或失去多数派后拒绝写入

还有两个容易混淆的参数:

text
sync_binlog = 1
innodb_flush_log_at_trx_commit = 1

它们解决的是主库本机 Binlog 和 Redo Log 的持久化问题,不会等待远端节点,因此不能把它们称为同步复制。

AFTER_SYNC 中的 SYNC 也不是“主备已经完全同步”的意思。这里主要指主库完成 Binlog Sync 后选择半同步 ACK 等待点。

5. MGR 也不能直接等同于传统半同步 ​

MySQL Group Replication(MGR)不是传统的一主一从 Binlog Dump 模式。

它会通过组通信和事务认证机制,让事务经过多数派协调后再提交。group_replication_consistency 还可以控制读写需要等待到什么一致性边界。

例如:

text
EVENTUAL
BEFORE_ON_PRIMARY_FAILOVER
BEFORE
AFTER
BEFORE_AND_AFTER

所以 MGR 的“多数派已经接受事务”和“所有节点已经执行完成”仍然是两个不同概念。

如果业务要求从任意节点立即读到刚提交的数据,不能只确认开启了 MGR,还需要检查具体一致性等级和读取节点。

六、不同云厂商支持的复制模式有什么差异 ​

下面对比的是云厂商托管实例内部的高可用复制模式,不是 Canal 自身的消费模式。

云数据库产品更新较快,以下信息以 2026 年 7 月 22 日能够查到的官方文档为准。不同地域、实例系列、存储类型和内核小版本仍可能存在差异,最终应以购买页和实例控制台为准。

云厂商及产品官方公开的复制方式主要限制或语义
阿里云 RDS MySQL 高可用系列半同步、异步半同步只等待备实例收到日志,不等待执行;异常时会退化为异步
阿里云 RDS MySQL 集群系列半同步、异步、MGRMGR 等待超过半数节点收到事务;MySQL 8.4 当前不支持 MGR
腾讯云数据库 MySQL异步、半同步、强同步MySQL 5.6、5.7、8.0 支持三种模式,5.5 只支持异步;强同步受实例架构限制
华为云 RDS for MySQL 主备或集群实例异步、半同步主备实例默认半同步;备库异常且等待超时后自动切换为异步
AWS RDS MySQL Multi-AZ DB instanceAWS 定义的同步 StandbyStandby 用于故障切换,不提供读流量;底层同步实现由 AWS 管理
AWS RDS Multi-AZ DB cluster半同步Writer 向两个 Reader 复制,至少一个 Reader ACK 即可提交,不要求所有 Reader 已执行
AWS RDS MySQL Read Replica异步用于读扩展,允许出现复制延迟
Amazon Aurora MySQL存储层同步复制数据同步写入跨 3 个 AZ 的 6 个存储节点;不是传统 Binlog 主备同步

1. 阿里云 RDS MySQL ​

阿里云公开文档把复制模式分成:

text
异步
半同步
MGR

高可用系列支持异步和半同步,集群系列额外支持 MGR。

阿里云对半同步的描述非常明确:

text
备实例收到日志
        ↓
主实例认为复制条件满足
        ↓
不等待备实例执行日志

备实例不可用或网络异常时,半同步会退化为异步。

MGR 则要求超过半数节点收到事务后,主节点才能提交。它只适用于集群系列,并有节点数量、内存、存储引擎和内核小版本要求。

截至上述日期,阿里云文档明确说明 MySQL 8.4 不支持 MGR,只支持半同步和异步;其 MGR 使用说明要求 MySQL 8.0 对应内核版本、至少 3 个且为奇数的节点,以及至少 8 GiB 内存。

2. 腾讯云数据库 MySQL ​

腾讯云提供三个名称:

text
异步复制
半同步复制
强同步复制

官方支持矩阵显示:

MySQL 版本支持模式
MySQL 5.5异步
MySQL 5.6 / 5.7 / 8.0异步、半同步、强同步

架构也会限制可选模式:

  • 双节点、集群版和云盘版支持异步、半同步
  • 三节点、四节点支持异步、半同步、强同步
  • 三节点、四节点可以配置强同步或半同步需要等待的 ACK 节点数

腾讯云半同步在异常时会退化为异步。

强同步的主要区别是不会自动退化:当不能满足 ACK 数量时,主节点会暂停向应用返回成功,直到同步条件恢复。

需要特别注意,腾讯云当前产品文档对 ACK 的解释仍然是确认备节点已经接收 Binlog。另一份复制原理文档把强同步描述为写入 Relay Log,并出现“执行完成”的措辞。

因此,如果业务要把“强同步”进一步解释为“备库 SQL 已经回放并可以立即查询”,应该结合当前实例架构、TXSQL 内核版本和腾讯云技术支持确认,不能只根据模式名称推断。

3. 华为云 RDS for MySQL ​

华为云 RDS for MySQL 主备实例公开支持:

text
异步
半同步

默认选择半同步。主库需要等待备库收到日志后才向应用返回成功。

当备库异常时,主库会等待数秒:

text
备库在等待窗口内恢复
    → 继续半同步

备库没有恢复
    → 自动切换为异步

华为云公开 API 文档中,RDS for MySQL 的 replication_mode 取值也是 async 或 semisync,没有面向 RDS for MySQL 主备实例暴露 sync 模式。

只读实例与主实例之间则采用异步复制,因此通过只读地址查询仍然可能遇到复制延迟。

4. AWS RDS MySQL ​

AWS 需要按部署形态分别讨论。

普通 Multi-AZ DB instance 会在另一个可用区维护一个同步 Standby:

text
Primary
    ↓ 同步复制
Standby

这个 Standby 只负责高可用切换,不能承接只读流量。AWS 将其描述为同步复制,但没有把它公开定义成 MySQL 原生半同步插件的某个 wait_point,因此不能直接套用 AFTER_SYNC 或 AFTER_COMMIT 推断内部实现。

Multi-AZ DB cluster 则明确使用数据库引擎的半同步复制:

text
一个 Writer
    ↓
两个 Reader
    ↓
至少一个 Reader 返回 ACK
    ↓
Writer 提交

AWS 官方同时强调,这个 ACK 不要求事件已经在所有副本上执行和提交。因此 Reader 仍然可能存在 ReplicaLag。

单独创建的 RDS MySQL Read Replica 使用异步复制,主要解决读扩展,不提供提交时的同步屏障。

5. Aurora MySQL 是另一种模型 ​

Aurora 将计算和存储分离。

事务写入 Primary 后,数据会同步复制到跨 3 个可用区的 6 个存储节点:

text
Aurora Writer
        ↓
共享集群存储
        ↓
3 个 AZ / 6 个存储副本

一次写入需要获得 6 个存储副本中至少 4 个的确认,而不是等待 6 个副本全部完成。

Aurora Replica 读取的是同一个集群存储卷,而不是依靠传统 MySQL Binlog 把完整数据复制到另一套独立存储。

所以 Aurora 的“同步”描述的是存储层高可用,不应该与 MySQL 半同步 ACK 或腾讯云强同步放在同一层直接比较。

6. 云厂商的主备模式不等于 Canal 的 ACK 模式 ​

这是和本文问题关系最直接的一点。

云厂商内部高可用链路通常是:

text
云数据库主节点
        ↓
云厂商托管备节点

Canal 链路则是:

text
云数据库主节点
        ↓
Binlog Dump 协议
        ↓
Canal

两条链路不是同一个复制成员关系。

即使控制台显示“半同步”或“强同步”,也只能说明主节点与云厂商托管备节点之间的复制方式,不能自动推出:

  • Canal 是半同步 ACK 客户端
  • Canal 的 ACK 会阻塞主库 Commit
  • 本文单库实验中的 8 秒窗口会原样出现在该云产品上
  • 云厂商没有修改 MySQL 内核提交顺序

反过来也一样。控制台显示异步,并不能证明普通 Binlog Consumer 与 InnoDB Commit 之间绝对不存在极短竞态窗口。

在云数据库上定位问题时,建议向厂商确认下面四个问题:

text
ACK 到底代表收到、落盘,还是执行完成
等待点在存储引擎 Commit 之前还是之后
异常时是否退化为异步
外部 Binlog Consumer 是否会被计入 ACK 客户端

如果实例允许查看半同步变量,还可以检查:

sql
SHOW VARIABLES LIKE 'rpl_semi_sync%';
SHOW STATUS LIKE 'Rpl_semi_sync%';

但托管数据库可能隐藏变量、修改变量名称或者使用自研内核,最终仍要以当前实例的产品文档、参数模板和厂商确认结果为准。

七、如何只用一个 MySQL 实例复现 ​

常见复现方式是准备一个 Master 和一个 Slave,再用 GDB 把 Master 卡在半同步等待位置。

但如果只是为了验证 MySQL 提交顺序,不需要真的启动第二个 MySQL。

本次测试只使用三个组件:

text
写入会话
    负责 INSERT 并 COMMIT

单个 MySQL 5.7.44
    开启 ROW Binlog 和半同步 AFTER_SYNC

Binlog Observer
    模拟 Canal 消费 Binlog
    注册成半同步 ACK 接收端
    收到 XID 后回查同一个 MySQL

整体过程如下:

text
Observer 连接 MySQL,开始读取 Binlog
        ↓
写入会话 INSERT 一条带唯一 Marker 的记录
        ↓
MySQL Prepare 并同步 Binlog
        ↓
Observer 收到 RowsEvent 和 XIDEvent
        ↓
Observer 立即通过独立 SQL 连接查询同一个 MySQL
        ↓
此时故意不返回 ACK,延迟 8 秒
        ↓
查询结果为空
        ↓
8 秒后 Observer 返回 ACK
        ↓
MySQL 完成 InnoDB Commit
        ↓
Observer 再次查询,记录可见

这里没有 MySQL Slave。

Observer 只是一个外部客户端,同时承担:

  1. Canal 风格的 Binlog Consumer
  2. 测试用半同步 ACK 接收端
  3. 收到消息后回查源库的业务应用

所以这是一个单 MySQL 实例复现。

八、关键配置与实测结果 ​

MySQL 使用的主要参数:

text
MySQL:                           5.7.44
binlog_format:                   ROW
sync_binlog:                     1
innodb_flush_log_at_trx_commit:  1
binlog_order_commits:            ON
rpl_semi_sync_master_enabled:    ON
rpl_semi_sync_master_wait_point: AFTER_SYNC
rpl_semi_sync_master_timeout:    30000

Observer 基于 go-mysql 实现,并显式开启:

go
SemiSyncEnabled: true

收到目标事务的 XIDEvent 后立即回查,然后故意延迟事件处理函数返回:

go
visible, err := sourceVisible(db, id, marker)
time.Sleep(8 * time.Second)

服务器配置:

项目配置
CPU2 vCPU
内存约 1.8 GiB
系统Alibaba Cloud Linux 3
Docker26.1.3
Docker Compose2.27.0
Swap2 GiB

第一次运行:

text
READY file=mysql-bin.000003 position=194
BINLOG_COMMIT_RECEIVED marker=canal-window-178470447052216
SOURCE_VISIBLE_AT_BINLOG_COMMIT=false
SOURCE_VISIBLE_AFTER_ACK_MS=8005

Reproduced successfully.
Rpl_semi_sync_master_yes_tx  1
Rpl_semi_sync_master_no_tx   0

第二次独立运行:

text
READY file=mysql-bin.000003 position=500
BINLOG_COMMIT_RECEIVED marker=canal-window-178470450452843
SOURCE_VISIBLE_AT_BINLOG_COMMIT=false
SOURCE_VISIBLE_AFTER_ACK_MS=8007

Reproduced successfully.
Rpl_semi_sync_master_yes_tx  2
Rpl_semi_sync_master_no_tx   0

两次测试中,数据分别在 8005ms 和 8007ms 后变得可见,与人为设置的 8000ms ACK 延迟基本一致。

测试完成后,MySQL 容器内存约 211 MiB,系统仍有约 1.1 GiB 可用内存。

因此,2 核 2 GiB 的服务器可以完成部署和稳定复现,建议保留 Swap 以提高首次构建的容错余量。

九、这能证明什么,又不能证明什么 ​

这套实验能够直接证明:

在 MySQL 5.7.44、半同步 AFTER_SYNC 模式下,Binlog Consumer 收到目标事务的 XID Event 时,同一个 MySQL 中的其他 SQL 会话可能仍看不到事务结果。

证据链如下:

text
Observer 匹配到唯一 Marker 的 RowsEvent
        ↓
Observer 收到对应 XIDEvent
        ↓
MySQL 半同步状态为 ON
        ↓
提交线程正在等待 Observer ACK
        ↓
独立 SQL 连接查询结果为空
        ↓
ACK 返回后约 8 秒,记录变为可见

但它不能直接证明:

  • 所有 Canal 版本都会参与 MySQL 半同步 ACK
  • 所有线上旧读问题都是这个原因
  • 未启用半同步时,窗口一定能被普通脚本稳定捕获
  • 生产环境中的真实窗口会达到 8 秒

本次测试中的 8 秒是人为放大的观察窗口。

对于普通异步 Canal,MySQL 源码顺序说明竞态窗口理论上存在,但通常更短,也更难稳定捕获。

十、这和 MySQL 版本有什么关系 ​

版本关系不能简单回答成“有”或者“没有”。

版本异步 Dump 理论窗口半同步默认行为
MySQL 5.6存在等价于 AFTER_COMMIT
MySQL 5.7.2+存在AFTER_SYNC
MySQL 8.0存在AFTER_SYNC

MySQL 5.6 的 Ordered Commit 中,同样会在存储引擎 Commit Stage 前更新并通知 Binlog End Position,所以普通异步窗口不是 MySQL 5.7 才首次出现。

MySQL 5.7.2+ 默认使用 AFTER_SYNC。如果消费端参与半同步 ACK,就能明确形成:

text
Binlog 已经发送
        ↓
等待 ACK
        ↓
InnoDB Commit

MySQL 8.0 仍然保留 after_sync Hook → Commit Stage 的整体顺序。从 MySQL 8.0.26 开始,官方逐步使用 source/replica 替代 master/slave,相关变量名称可能变为:

text
rpl_semi_sync_source_wait_point

所以真正需要检查的是半同步配置和消费方式,而不只是版本号。

十一、线上怎么排查和治理 ​

1. 先排除更常见的原因 ​

确认:

  • 查询是否真的走主库
  • 是否经过读写分离中间件
  • 是否命中本地缓存或 Redis
  • 是否在旧事务里复用了 RR Read View
  • 消息主键、分库分表键是否正确

这些问题出现的概率通常比 MySQL 提交窗口更高。

2. 检查半同步状态 ​

MySQL 5.7 可以执行:

sql
SHOW VARIABLES LIKE 'rpl_semi_sync_master_enabled';
SHOW VARIABLES LIKE 'rpl_semi_sync_master_wait_point';
SHOW STATUS LIKE 'Rpl_semi_sync_master_status';
SHOW STATUS LIKE 'Rpl_semi_sync_master_clients';

重点关注:

text
rpl_semi_sync_master_wait_point = AFTER_SYNC
Rpl_semi_sync_master_status = ON

3. 建立统一时间线 ​

至少记录:

text
业务 COMMIT 开始时间
业务 COMMIT 返回时间
Canal 收到 Binlog 时间
MQ 发送和消费时间
回查 SQL 开始与结束时间
查询目标数据库地址

如果各系统机器时间没有同步,毫秒级时间线没有参考价值。

4. 不要把 CDC 消息当成源库可见性屏障 ​

错误假设:

text
收到 Canal 消息
    =
现在查询源库一定能看到数据

更准确的契约是:

text
收到 Canal 消息
    =
Binlog 已经记录并发布了该事务

5. 消息尽量携带完整业务数据 ​

如果消费者收到消息后还必须立即回查源库,链路就天然依赖数据库当前可见性。

更稳的做法是让消息直接携带:

  • 业务主键
  • 变更后的关键字段
  • 事件版本
  • 发生时间
  • 幂等键

消费者能够直接处理时,就不需要用一次新的数据库查询重新确认消息内容。

6. 使用有界重试,不要只靠固定延迟 ​

第一次查询为空时,可以使用短暂退避:

text
20ms
50ms
100ms
200ms
500ms

同时保证幂等,并限制最大重试次数。

固定延迟 1 秒能够降低命中概率,但它不是严格保证。遇到数据库抖动、磁盘阻塞或网络异常时,固定值没有稳定边界。

更可靠的是:

text
条件判断 + 有界重试 + 幂等处理

7. 谨慎切换 AFTER_COMMIT ​

把半同步等待点改为 AFTER_COMMIT,会让引擎先提交,再等待 ACK,但这也会改变故障切换语义。

在 AFTER_COMMIT 下,事务可能已经对主库其他会话可见,但半同步接收端还没确认收到 Binlog。如果此时主库故障并切换,新主库可能缺少业务已经观察到的事务。

所以它不是一个可以由应用开发单独决定的“问题修复开关”,必须结合 RPO、切换策略和一致性要求由 DBA 统一评估。

总结 ​

回到最开始的问题:

Canal 已经收到 Binlog,为什么 MySQL 主库还查不到数据?

根本原因是:

text
Binlog 事务结束事件到达消费端

和:

text
InnoDB 最终 Commit 完成并对其他会话可见

不是同一个时点。

MySQL Ordered Commit 会先完成 Binlog Flush/Sync 和 End Position 更新,再进入存储引擎 Commit Stage。

普通异步消费中,两个阶段之间存在很短的理论窗口;半同步 AFTER_SYNC 的 ACK 等待正好发生在两个阶段之间,因此窗口可以被稳定放大。

云数据库控制台中的“异步”“半同步”“强同步”或“同步 Standby”,描述的通常是厂商内部高可用链路。它们可能分别工作在 Binlog、共识协议或分布式存储层,不能仅凭名称推断 ACK 是否代表备库已经执行事务,也不能推断 Canal 是否参与 ACK。

本次使用单个 MySQL 5.7.44 实例,在 2 核 2 GiB 服务器上连续两次复现成功:

text
收到 XID 时:数据不可见
延迟 ACK 8 秒后:数据可见

这件事给业务系统最重要的提醒是:

收到 CDC 消息,只能说明 Binlog 已经记录并发布了事务,不能把它当作源库数据当前一定可见的严格保证。

生产设计上,更应该依赖完整事件数据、幂等重试和明确的一致性模型,而不是一次立即回查或者一个拍脑袋的固定延迟。

参考资料 ​

Last updated: