Canal 已经收到 Binlog,为什么 MySQL 主库还查不到数据?
在基于 Canal 的数据同步链路里,有一个很反直觉的现象:
应用完成一次数据库更新
↓
Canal 收到 Binlog 并发送 MQ
↓
消费者收到消息后立即回查 MySQL 主库
↓
查询不到刚更新的数据,或者仍然读到旧值第一反应通常是主从延迟、缓存没失效、SQL 路由错了,或者事务隔离级别导致旧读。
但如果已经确认查询走的是主库,没有读缓存,也没有复用旧事务快照,问题就会变得很奇怪:
既然 Canal 已经读到了事务的 Binlog,为什么同一个 MySQL 里的其他会话还看不到数据?
为了确认这件事,我搭了一套只有一个 MySQL 实例的复现环境,并在一台 2 核 2 GiB 的服务器上做了实际验证。
结果是:这个现象确实可以稳定出现。
先说结论
一句话概括:
Binlog 中已经出现完整事务,不代表 InnoDB 的最终提交和数据可见性处理已经完成。
MySQL 的事务提交不是一个瞬间完成的动作,而是由多个阶段组成:
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 已经完成最后一步。
所以,更准确的描述不是:
Canal 读取到了一个未提交事务而是:
Binlog 已经记录并发布了事务提交边界,
但 InnoDB 的运行时提交过程可能还没有结束这两种表述看起来相似,技术含义却完全不同。
二、MySQL 源码里的真实顺序
InnoDB 有自己的 Redo Log,MySQL Server 层还有 Binlog。
一次事务提交时,两套日志必须保持一致:
- 不能 Redo 已提交,但 Binlog 没有记录
- 不能 Binlog 记录了事务,但 InnoDB 恢复后却回滚
因此,MySQL 使用两阶段提交协调 Redo Log 和 Binlog:
第一阶段:InnoDB Prepare
↓
写 Binlog
↓
Flush / Sync Binlog
↓
第二阶段:InnoDB CommitMySQL 5.7 的核心事务组提交逻辑位于:
sql/binlog.cc
MYSQL_BIN_LOG::ordered_commit()整个流程又被拆成三个主要阶段:
FLUSH_STAGE
写入 Binlog Cache
Flush Binlog
SYNC_STAGE
fsync Binlog
更新 Binlog End Position
执行 after_sync Hook
COMMIT_STAGE
调用 ha_commit_low()
完成存储引擎 Commit把关键函数按执行顺序排列,大致是:
flush_cache_to_file()
↓
sync_binlog_file()
↓
update_binlog_end_pos()
↓
call_after_sync_hook()
↓
process_commit_stage_queue()
↓
ha_commit_low()最值得注意的是:
update_binlog_end_pos()发生在:
ha_commit_low()之前。
update_binlog_end_pos() 会更新 Binlog 当前可发送的位置,并唤醒等待新事件的 Binlog Dump 线程。
也就是说,Dump 客户端可能已经看到新的 Binlog 位置,而存储引擎 Commit Stage 还没有执行完。
这就是普通异步 Binlog 消费也存在理论竞态窗口的原因。
三、半同步 AFTER_SYNC 如何放大窗口
MySQL 半同步复制有两个重要等待点:
AFTER_SYNC
AFTER_COMMIT两种模式的核心差别是 ACK 等在哪里。
AFTER_COMMIT
Flush / Sync Binlog
↓
InnoDB Commit
↓
事务对其他会话可见
↓
等待半同步 ACKAFTER_SYNC
Flush / Sync Binlog
↓
发送 Binlog
↓
等待半同步 ACK
↓
InnoDB Commit
↓
事务对其他会话可见MySQL 5.7 半同步插件位于:
plugin/semisync/semisync_master_plugin.cc当等待点为 AFTER_SYNC 时,插件会在 after_sync Hook 中调用 commitTrx() 等待 ACK。
而 after_sync Hook 正好位于 Binlog 同步之后、存储引擎 Commit 之前。
只要 ACK 没有返回,提交线程就会停在这里:
Binlog 已经可以被消费
↓
等待 ACK
↓
InnoDB 尚未最终 Commit这也是本次实验能够确定性复现的关键。
四、为什么 MySQL 5.7 默认改成 AFTER_SYNC
MySQL 5.7.2 将半同步等待点的默认行为从 AFTER_COMMIT 改成了 AFTER_SYNC。
这样调整的主要目的是改善故障切换时的数据一致性。
如果使用 AFTER_COMMIT:
主库完成 InnoDB Commit
↓
其他业务会话已经能看到数据
↓
半同步接收端还没确认收到 Binlog
↓
主库突然故障并发生切换业务可能遇到一种很难解释的情况:刚刚查询到的数据,切换到新主库后却不存在了。
AFTER_SYNC 把 ACK 等待移动到引擎 Commit 前:
至少一个接收端确认收到 Binlog
↓
主库完成 InnoDB Commit
↓
事务才对其他会话可见它改善了故障切换语义,但也让“Binlog 已经送出、主库事务还没最终提交”的窗口更容易被观察到。
五、异步、半同步和“同步”到底差在哪
讨论云数据库之前,必须先把“同步”这个词拆开。
一条事务从主库传到备库,至少会经过下面几个阶段:
主库生成并持久化 Binlog
↓
备库接收到 Binlog
↓
备库写入 Relay Log
↓
备库 SQL / Applier 线程执行事务
↓
备库 InnoDB Commit
↓
备库查询能够看到数据不同复制模式的核心差别,不是名字里有没有“同步”,而是:
主库在向应用返回成功之前,要求远端节点走到上面哪一步。
1. 异步复制
异步复制不等待备库 ACK:
主库完成本地提交
↓
立即向应用返回成功
↓
Binlog 后续异步发送给备库它的优点是写入延迟低,备库异常通常不会阻塞主库。
代价是主库故障时,尚未发送或尚未落到备库的事务可能丢失。备库查询也天然存在延迟,只能提供最终一致性。
2. 半同步复制
半同步复制会要求至少一个备库返回 ACK:
主库发送 Binlog
↓
至少一个备库接收并记录到 Relay Log
↓
备库返回 ACK
↓
主库向应用返回成功这里最容易被误解的是:
半同步 ACK 通常只证明备库已经接收并记录了事务日志,不证明备库 SQL 线程已经执行,更不证明备库查询已经可见。
MySQL 原生半同步还存在超时退化机制。主库等待超过 rpl_semi_sync_source_timeout 后,可以退化为异步复制,避免备库故障无限阻塞写入。MySQL 5.7 对应的旧变量名是 rpl_semi_sync_master_timeout。
因此,半同步是在数据安全和写入可用性之间做折中,不是严格意义上的全同步提交。
3. “强同步”或“全同步”
传统定义中的同步复制,是主库必须等待指定远端节点到达约定的提交屏障,才能向客户端返回成功。
但云厂商所说的“强同步”并不是一个统一的 MySQL 标准术语。不同产品的 ACK 可能分别代表:
收到 Binlog
写入 Relay Log
多数节点收到事务
多数存储副本完成写入
备库已经执行并 Commit这些语义并不等价。
有些厂商所谓的强同步,本质上是:
半同步 ACK
+
不允许自动退化为异步
+
等待一个或多个备节点它可以提高故障切换时的数据安全,但仍不必然代表备库已经完成事务回放。
Oracle MySQL 传统 InnoDB 主从复制原生提供的是异步和半同步机制,并没有一个“等待所有普通 Replica 完成 InnoDB Commit 后再返回”的经典复制模式。MySQL NDB Cluster 的同步数据节点复制属于另一套存储引擎和集群架构,也不能与普通 InnoDB 主从直接类比。
所以看到“强同步”三个字时,至少要继续确认:
- ACK 代表收到日志、持久化日志,还是已经执行事务
- 主库需要等待一个节点、全部节点,还是多数派节点
- 备节点异常时会阻塞写入,还是自动退化为异步
- 同步发生在 Binlog 层、数据库引擎层,还是分布式存储层
4. 三种模式放在一起比较
| 模式 | 主库是否等待远端 | ACK 通常代表什么 | 备端是否已经可查询 | 故障时的典型行为 |
|---|---|---|---|---|
| 异步复制 | 不等待 | 没有远端 ACK 要求 | 不保证 | 主库继续写入,切换时可能丢失未复制事务 |
| 半同步复制 | 等待至少一个备端 | 收到并记录事务日志 | 不保证 | 超时后通常可退化为异步 |
| 强同步 / 同步复制 | 取决于产品定义 | 可能是日志接收、多数派确认或事务执行完成 | 必须查产品定义 | 通常不静默退化,可能阻塞写入或失去多数派后拒绝写入 |
还有两个容易混淆的参数:
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 还可以控制读写需要等待到什么一致性边界。
例如:
EVENTUAL
BEFORE_ON_PRIMARY_FAILOVER
BEFORE
AFTER
BEFORE_AND_AFTER所以 MGR 的“多数派已经接受事务”和“所有节点已经执行完成”仍然是两个不同概念。
如果业务要求从任意节点立即读到刚提交的数据,不能只确认开启了 MGR,还需要检查具体一致性等级和读取节点。
六、不同云厂商支持的复制模式有什么差异
下面对比的是云厂商托管实例内部的高可用复制模式,不是 Canal 自身的消费模式。
云数据库产品更新较快,以下信息以 2026 年 7 月 22 日能够查到的官方文档为准。不同地域、实例系列、存储类型和内核小版本仍可能存在差异,最终应以购买页和实例控制台为准。
| 云厂商及产品 | 官方公开的复制方式 | 主要限制或语义 |
|---|---|---|
| 阿里云 RDS MySQL 高可用系列 | 半同步、异步 | 半同步只等待备实例收到日志,不等待执行;异常时会退化为异步 |
| 阿里云 RDS MySQL 集群系列 | 半同步、异步、MGR | MGR 等待超过半数节点收到事务;MySQL 8.4 当前不支持 MGR |
| 腾讯云数据库 MySQL | 异步、半同步、强同步 | MySQL 5.6、5.7、8.0 支持三种模式,5.5 只支持异步;强同步受实例架构限制 |
| 华为云 RDS for MySQL 主备或集群实例 | 异步、半同步 | 主备实例默认半同步;备库异常且等待超时后自动切换为异步 |
| AWS RDS MySQL Multi-AZ DB instance | AWS 定义的同步 Standby | Standby 用于故障切换,不提供读流量;底层同步实现由 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
阿里云公开文档把复制模式分成:
异步
半同步
MGR高可用系列支持异步和半同步,集群系列额外支持 MGR。
阿里云对半同步的描述非常明确:
备实例收到日志
↓
主实例认为复制条件满足
↓
不等待备实例执行日志备实例不可用或网络异常时,半同步会退化为异步。
MGR 则要求超过半数节点收到事务后,主节点才能提交。它只适用于集群系列,并有节点数量、内存、存储引擎和内核小版本要求。
截至上述日期,阿里云文档明确说明 MySQL 8.4 不支持 MGR,只支持半同步和异步;其 MGR 使用说明要求 MySQL 8.0 对应内核版本、至少 3 个且为奇数的节点,以及至少 8 GiB 内存。
2. 腾讯云数据库 MySQL
腾讯云提供三个名称:
异步复制
半同步复制
强同步复制官方支持矩阵显示:
| 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 主备实例公开支持:
异步
半同步默认选择半同步。主库需要等待备库收到日志后才向应用返回成功。
当备库异常时,主库会等待数秒:
备库在等待窗口内恢复
→ 继续半同步
备库没有恢复
→ 自动切换为异步华为云公开 API 文档中,RDS for MySQL 的 replication_mode 取值也是 async 或 semisync,没有面向 RDS for MySQL 主备实例暴露 sync 模式。
只读实例与主实例之间则采用异步复制,因此通过只读地址查询仍然可能遇到复制延迟。
4. AWS RDS MySQL
AWS 需要按部署形态分别讨论。
普通 Multi-AZ DB instance 会在另一个可用区维护一个同步 Standby:
Primary
↓ 同步复制
Standby这个 Standby 只负责高可用切换,不能承接只读流量。AWS 将其描述为同步复制,但没有把它公开定义成 MySQL 原生半同步插件的某个 wait_point,因此不能直接套用 AFTER_SYNC 或 AFTER_COMMIT 推断内部实现。
Multi-AZ DB cluster 则明确使用数据库引擎的半同步复制:
一个 Writer
↓
两个 Reader
↓
至少一个 Reader 返回 ACK
↓
Writer 提交AWS 官方同时强调,这个 ACK 不要求事件已经在所有副本上执行和提交。因此 Reader 仍然可能存在 ReplicaLag。
单独创建的 RDS MySQL Read Replica 使用异步复制,主要解决读扩展,不提供提交时的同步屏障。
5. Aurora MySQL 是另一种模型
Aurora 将计算和存储分离。
事务写入 Primary 后,数据会同步复制到跨 3 个可用区的 6 个存储节点:
Aurora Writer
↓
共享集群存储
↓
3 个 AZ / 6 个存储副本一次写入需要获得 6 个存储副本中至少 4 个的确认,而不是等待 6 个副本全部完成。
Aurora Replica 读取的是同一个集群存储卷,而不是依靠传统 MySQL Binlog 把完整数据复制到另一套独立存储。
所以 Aurora 的“同步”描述的是存储层高可用,不应该与 MySQL 半同步 ACK 或腾讯云强同步放在同一层直接比较。
6. 云厂商的主备模式不等于 Canal 的 ACK 模式
这是和本文问题关系最直接的一点。
云厂商内部高可用链路通常是:
云数据库主节点
↓
云厂商托管备节点Canal 链路则是:
云数据库主节点
↓
Binlog Dump 协议
↓
Canal两条链路不是同一个复制成员关系。
即使控制台显示“半同步”或“强同步”,也只能说明主节点与云厂商托管备节点之间的复制方式,不能自动推出:
- Canal 是半同步 ACK 客户端
- Canal 的 ACK 会阻塞主库 Commit
- 本文单库实验中的 8 秒窗口会原样出现在该云产品上
- 云厂商没有修改 MySQL 内核提交顺序
反过来也一样。控制台显示异步,并不能证明普通 Binlog Consumer 与 InnoDB Commit 之间绝对不存在极短竞态窗口。
在云数据库上定位问题时,建议向厂商确认下面四个问题:
ACK 到底代表收到、落盘,还是执行完成
等待点在存储引擎 Commit 之前还是之后
异常时是否退化为异步
外部 Binlog Consumer 是否会被计入 ACK 客户端如果实例允许查看半同步变量,还可以检查:
SHOW VARIABLES LIKE 'rpl_semi_sync%';
SHOW STATUS LIKE 'Rpl_semi_sync%';但托管数据库可能隐藏变量、修改变量名称或者使用自研内核,最终仍要以当前实例的产品文档、参数模板和厂商确认结果为准。
七、如何只用一个 MySQL 实例复现
常见复现方式是准备一个 Master 和一个 Slave,再用 GDB 把 Master 卡在半同步等待位置。
但如果只是为了验证 MySQL 提交顺序,不需要真的启动第二个 MySQL。
本次测试只使用三个组件:
写入会话
负责 INSERT 并 COMMIT
单个 MySQL 5.7.44
开启 ROW Binlog 和半同步 AFTER_SYNC
Binlog Observer
模拟 Canal 消费 Binlog
注册成半同步 ACK 接收端
收到 XID 后回查同一个 MySQL整体过程如下:
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 只是一个外部客户端,同时承担:
- Canal 风格的 Binlog Consumer
- 测试用半同步 ACK 接收端
- 收到消息后回查源库的业务应用
所以这是一个单 MySQL 实例复现。
八、关键配置与实测结果
MySQL 使用的主要参数:
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: 30000Observer 基于 go-mysql 实现,并显式开启:
SemiSyncEnabled: true收到目标事务的 XIDEvent 后立即回查,然后故意延迟事件处理函数返回:
visible, err := sourceVisible(db, id, marker)
time.Sleep(8 * time.Second)服务器配置:
| 项目 | 配置 |
|---|---|
| CPU | 2 vCPU |
| 内存 | 约 1.8 GiB |
| 系统 | Alibaba Cloud Linux 3 |
| Docker | 26.1.3 |
| Docker Compose | 2.27.0 |
| Swap | 2 GiB |
第一次运行:
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第二次独立运行:
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 会话可能仍看不到事务结果。
证据链如下:
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,就能明确形成:
Binlog 已经发送
↓
等待 ACK
↓
InnoDB CommitMySQL 8.0 仍然保留 after_sync Hook → Commit Stage 的整体顺序。从 MySQL 8.0.26 开始,官方逐步使用 source/replica 替代 master/slave,相关变量名称可能变为:
rpl_semi_sync_source_wait_point所以真正需要检查的是半同步配置和消费方式,而不只是版本号。
十一、线上怎么排查和治理
1. 先排除更常见的原因
确认:
- 查询是否真的走主库
- 是否经过读写分离中间件
- 是否命中本地缓存或 Redis
- 是否在旧事务里复用了 RR Read View
- 消息主键、分库分表键是否正确
这些问题出现的概率通常比 MySQL 提交窗口更高。
2. 检查半同步状态
MySQL 5.7 可以执行:
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';重点关注:
rpl_semi_sync_master_wait_point = AFTER_SYNC
Rpl_semi_sync_master_status = ON3. 建立统一时间线
至少记录:
业务 COMMIT 开始时间
业务 COMMIT 返回时间
Canal 收到 Binlog 时间
MQ 发送和消费时间
回查 SQL 开始与结束时间
查询目标数据库地址如果各系统机器时间没有同步,毫秒级时间线没有参考价值。
4. 不要把 CDC 消息当成源库可见性屏障
错误假设:
收到 Canal 消息
=
现在查询源库一定能看到数据更准确的契约是:
收到 Canal 消息
=
Binlog 已经记录并发布了该事务5. 消息尽量携带完整业务数据
如果消费者收到消息后还必须立即回查源库,链路就天然依赖数据库当前可见性。
更稳的做法是让消息直接携带:
- 业务主键
- 变更后的关键字段
- 事件版本
- 发生时间
- 幂等键
消费者能够直接处理时,就不需要用一次新的数据库查询重新确认消息内容。
6. 使用有界重试,不要只靠固定延迟
第一次查询为空时,可以使用短暂退避:
20ms
50ms
100ms
200ms
500ms同时保证幂等,并限制最大重试次数。
固定延迟 1 秒能够降低命中概率,但它不是严格保证。遇到数据库抖动、磁盘阻塞或网络异常时,固定值没有稳定边界。
更可靠的是:
条件判断 + 有界重试 + 幂等处理7. 谨慎切换 AFTER_COMMIT
把半同步等待点改为 AFTER_COMMIT,会让引擎先提交,再等待 ACK,但这也会改变故障切换语义。
在 AFTER_COMMIT 下,事务可能已经对主库其他会话可见,但半同步接收端还没确认收到 Binlog。如果此时主库故障并切换,新主库可能缺少业务已经观察到的事务。
所以它不是一个可以由应用开发单独决定的“问题修复开关”,必须结合 RPO、切换策略和一致性要求由 DBA 统一评估。
总结
回到最开始的问题:
Canal 已经收到 Binlog,为什么 MySQL 主库还查不到数据?
根本原因是:
Binlog 事务结束事件到达消费端和:
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 服务器上连续两次复现成功:
收到 XID 时:数据不可见
延迟 ACK 8 秒后:数据可见这件事给业务系统最重要的提醒是:
收到 CDC 消息,只能说明 Binlog 已经记录并发布了事务,不能把它当作源库数据当前一定可见的严格保证。
生产设计上,更应该依赖完整事件数据、幂等重试和明确的一致性模型,而不是一次立即回查或者一个拍脑袋的固定延迟。
参考资料
- MySQL 5.7 Semisynchronous Replication
- MySQL 5.7.2 Release Notes
- MySQL 5.7 sql/binlog.cc
- MySQL 5.7 semisync_master_plugin.cc
- MySQL 5.7 trx0trx.cc
- MySQL 5.6.51 sql/binlog.cc
- MySQL 8.0 sql/binlog.cc
- MySQL 8.4 Semisynchronous Replication
- MySQL 8.4 Replication Solutions
- MySQL 8.4 Group Replication Consistency Guarantees
- 阿里云 RDS MySQL:查询和修改数据复制方式
- 阿里云 RDS MySQL:组复制简介
- 阿里云 RDS MySQL:使用组复制
- 腾讯云数据库 MySQL:数据复制方式
- 腾讯云数据库 MySQL:修改数据复制方式
- 华为云 RDS for MySQL:修改数据同步方式
- 华为云 RDS for MySQL:数据库实例类型
- AWS RDS:Multi-AZ DB instance
- AWS RDS:Multi-AZ DB cluster
- AWS RDS:Read Replicas
- Amazon Aurora:High Availability
- AWS Database Blog:Amazon Aurora Under the Hood - Quorum and Correlated Failure