标签 CDC 下的文章

作者:Prabakaran Thirumalai,MySQL 服务器运行时咨询成员技术人员。

原文:https://blogs.oracle.com/mysql/no-more-hidden-changes-how-mys...,Jan 30, 2026

爱可生开源社区翻译,本文约 2700 字,预计阅读需要 9 分钟。

640 (87).webp

MySQL 通过重新思考外键约束和级联的管理方式,迈出了重要一步。MySQL 9.6 开始,外键检查和级联操作将由 SQL 引擎 直接处理,而非 InnoDB 存储引擎。这一改进解决了长期存在的变更跟踪、二进制日志复制和数据一致性方面的挑战,使 MySQL 在异构环境、变更数据捕获(CDC)管道和分析工作负载方面更加稳健。

1. InnoDB 中外键的先前工作方式

历史上,MySQL 在存储引擎层(特别是 InnoDB 数据库)强制执行外键约束和级联。其工作原理如下:

  • 外键级联:当对父表执行 DELETE 或 UPDATE 等语句时,InnoDB 会检查外键约束。如果定义了级联操作(例如 ON DELETE CASCADE ),InnoDB 会处理子表中相应行的更新或删除操作。
  • InnoDB 内部执行:所有级联操作均由 InnoDB 内部执行。SQL 引擎仅发起父级操作;所有对子表的依赖操作均由 InnoDB 管理。

    重要的是,这些子行更改对 SQL 层是不可见的。因此,在基于行的复制 (RBR) 模式下,InnoDB 内部执行的级联操作不会出现在 MySQL 二进制日志中。

  • 运行影响:由于这些变更对 SQL 引擎和二进制日志隐藏,下游系统(例如 CDC 管道和分析平台)可能无法检测到这些变更。这可能导致数据不一致、分析结果不可靠以及复制问题。

基于 InnoDB 的外键的局限性

随着 MySQL 部署规模和复杂性的增长,这种传统方法暴露出以下局限性:

  • 隐藏的数据更改:在 InnoDB 内部执行的级联父子更改对 SQL 层是不可见的,并且没有在更高级别上被捕获。
  • 系统日志不完整:二进制日志中经常缺少子行更改,导致复制和审计不完整。
  • 数据捕获差距:依赖二进制日志或完整变更历史记录的数据工具和下游系统无法始终跟踪与外键相关的每个更新或删除。
  • 复制风险: 在复杂的复制设置中,这些静默的更改可能会导致主服务器和副本之间的数据出现差异,从而导致操作上的挑战。

2. 新模型:SQL 引擎管理的外键强制执行

为了解决这些问题,MySQL 现在强制执行外键,并在 SQL 引擎内部管理级联操作。通过这项更改,父表和子表上的所有外键操作对 SQL 层都是完全可见的。

640 (88).webp

主要优势:

  • 完整日志记录:所有更改(包括级联更改)现在都可见、可审计,并完整记录在二进制日志中。
  • 可靠的复制:不再有隐藏的数据更改;复制现在更加值得信赖和准确。
  • 更佳的分析:数据采集和分析工具现在可以获得所有数据变化的完整、实时视图。
  • 创新基础:这种架构使得跨存储引擎扩展外键支持以及未来的复制和可观测性功能变得更加容易。

注意:对于除 InnoDB 之外的其他支持外键的存储引擎,强制执行和级联操作仍由相应的存储引擎管理。

性能比较

我们理解,对于考虑将外键强制执行机制从 InnoDB 迁移到 SQL 引擎的 MySQL 用户而言,性能是首要考虑因素。针对常见事务工作负载的大量基准测试证实,基于 SQL 引擎的外键强制执行和级联机制的性能与 InnoDB 方法 几乎完全相同。外键检查和级联的成本基本保持不变,因此 吞吐量和延迟方面没有出现任何可观察到的下降。 这使得即使在高吞吐量和关键任务部署中,采用新的实现方案也是安全的。

向后兼容性

SQL 引擎的外键强制执行和级联机制旨在 完全向后兼容,保留 InnoDB 外键强制执行的语义和行为。虽然整体用户体验保持不变,但仍有一些值得注意的改进和细微的行为差异:

  • 错误信息:虽然错误代码与以前的版本一致,但由于检查执行顺序不同,具体的错误信息文本(包括外键名称)可能会有所不同。
  • 自增间隙:如果外键约束失败,任何尝试插入操作都会增加自增计数器,这可能会导致值出现间隙,符合 MySQL 的标准行为。
  • 针对级联行更新统计信息:行级统计信息(例如 delete_rows )已更新,以包含受级联外键操作影响的行。这确保系统统计信息能够准确反映外键强制执行所执行的所有数据更改。
  • 更严格的排序规则验证:如果外键级联跨越不兼容的排序规则,则会引发显式错误,防止出现 静默数据问题,并提高用户的数据完整性。

3. 安全采用并内置备用方案

为了实现可控的升级,MySQL 引入了一个只读的启动变量 innodb_native_foreign_keys。这提供了平滑的升级路径,并最大限度地减少了版本过渡期间的意外变更。默认情况下,此变量设置为 FALSE ,这意味着默认行为是基于 SQL 引擎的外键强制执行 。在测试环境或早期生产部署期间,您可以将此变量设置为 TRUE ,以暂时恢复到 InnoDB 的原生外键处理方式。这在验证新的 SQL 引擎行为时提供了一个清晰的操作回退方案。

注意: 此系统变量旨在帮助简化迁移,随着 MySQL 社区全面采用基于 SQL 引擎的外键,该变量将在未来的版本中移除。

4. 总结:为什么这项改变至关重要?

通过将外键强制执行移至 SQL 引擎,MySQL 弥补了长期存在的架构缺陷。这一改进确保数据变更始终可见、被记录和被复制,使 MySQL 成为更强大的平台,适用于现代化的分布式合规数据环境。

总的来说,对于 MySQL 用户而言,这意味着更好的数据一致性、更可靠的复制,以及在分析和合规工作流程中更少的意外情况,而不会牺牲性能。

介绍
发布和订阅使用了 pg 的逻辑复制功能,通过发布端创建 publication 与表绑定,订阅端创建 subscription 同时会在发布端创建逻辑复制槽实现逻辑复制功能

逻辑复制基于 发布(Publication) 与 订阅(Subscription)模型

一个 发布者(Publisher) 上可以有多个发布,一个 订阅者(Subscriber) 上可以有多个 订阅 。
一个发布可被多个订阅者订阅,一个订阅只能订阅一个发布者,但可订阅同发布者上的多个不同发布。

典型用法

迁移,跨 PostgreSQL 大版本,跨操作系统平台进行复制。
CDC,收集数据库(或数据库的一个子集)中的增量变更,在订阅者上为增量变更触发触发器执行定制逻辑。
分拆,将多个数据库集成为一个,或者将一个数据库拆分为多个,进行精细的分拆集成与访问控制。

复制标识

一个被纳入发布中的表,必须带有复制标识(Replica Identity),只有这样才可以在订阅者一侧定位到需要更新的行,完成 UPDATE 与 DELETE 操作的复制。
默认情况下,主键 (Primary Key)是表的复制标识,非空列上的唯一索引 (UNIQUE NOT NULL)也可以用作复制标识。
如果没有任何复制标识,可以显式将复制标识设置为 FULL,也就是把整个行当作复制标识
使用 FULL 模式的复制标识效率很低(因为每一行修改都需要在订阅者上执行全表扫描,很容易把订阅者拖垮),所以这种配置只能是保底方案。
使用 FULL 模式的复制标识还有一个限制,订阅端的表上的复制身份所包含的列,要么与发布者一致,要么比发布者更少

创建发布订阅

环境介绍

version: pg16
发布端: 5433
订阅端: 5434
主机 IP: 192.168.28.11

创建发布订阅

在发布者端,wal_level 必须被设置为 logical,而 max_replication_slots 中设置的值必须至少是预期要连接的订阅数加上保留给表同步的连接数。max_wal_senders 应该至少被设置为 max_replication_slots 加上同时连接的物理复制体的数量。
订阅者必须配置 max_replication_slots。它必须设置为至少是订阅者数,加上一些用于表同步的预留。max_logical_replication_workers 必须至少被设置为订阅数加上保留给表同步的连接数。此外,可能需要调整 max_worker_processes 以容纳复制工作者,至少为 (max_logical_replication_workers + 1)。注意,一些扩展和并行查询也会从 max_worker_processes 中取得工作者槽。

发布端
  1. 修改配置文件
vim postgres.conf
wal_level = logical 
max_wal_senders = 100 #max_wal_senders大于max_replication_slots 
vim pg_hba.conf
host    all    repl    订阅者IP/32    md5

重启 pgsql ,配置生效

su postgres
pg_ctl restart -D /data/postgresql/pgdata/pg-5433/ 
  1. 发布者节点创建用户,赋权
su postgres
psql -P 5433
psql (16.9 (Ubuntu 16.9-0ubuntu0.24.10.1))
Type "help" for help.
#创建用户
postgres=# create user repuser password 'repuser' replication;
CREATE ROLE
#在postgres库中创建表,并插入一条数据
postgres=# create table test1(id int primary key,name varchar(10) );
postgres=# insert into test1 values(1,'a'); # 赋予复制用户select表权限
postgres=# grant select on table test1 to repuser; 
  1. 创建表 test1 的发布
create publication pub_test01 for table test1;
订阅端
  1. 修改 posetgres.conf
vim postgres.conf
max_replication_slots=8
max_logical_replication_workers=10
max_worker_processes=8

max_logical_replication_workers 逻辑复制进程数,应大于订阅节点的数量,并且给表同步预留一些进程数量
2. 重启 pgsql

pg_ctl restart -D /data/postgresql/pgdata/pg-5434/ 
  1. 创建订阅
CREATE SUBSCRIPTION subs_test01  CONNECTION 'host=192.168.28.11 dbname=postgres port=5433 user=repuser password=repuser' PUBLICATION pub_test01;
  1. 查看表是否同步
    在发布端执行 test1 表的 insert、delete、update、truncate 操作,看订阅端是否能同步

新加表同步

发布端
  1. 新建表
    create table test3 (id int,name varchar(10));
  2. 设置复制标识
    alter table test3 REPLICA IDENTITY full;
  3. 将表加入发布
    ALTER PUBLICATION pub1 ADD TABLE test3;
订阅端

在订阅端执行刷新
ALTER SUBSCRIPTION sub1 REFRESH PUBLICATION ;
在发布端执行 test3 表的 insert、delete、update、truncate 操作,看订阅端是否能同步

发布订阅查询状态语句

发布端
select * from pg_replication_slots; --查看所有复制槽 select * from pg_stat_replication; --查看复制槽的同步状态 select * from pg_publication; --查看所有发布 select * from pg_publication_tables; --查看所有发布对应的表 select usename,a.pubname,c.*,pubinsert,pubupdate,pubdelete,pubtruncate from pg_publication a,pg_user b,pg_publication_tables c where a.pubowner=b.usesysid and c.pubname=a.pubname;
订阅端
select * from pg_subscription; --查看所有订阅 select srrelid::regclass from pg_subscription_rel; --查看订阅的所有表 select usename,datname,subname,srrelid::regclass,srsublsn,subconninfo from pg_subscription a,pg_user b,pg_subscription_rel c ,pg_database d where a.oid=c.srsubid and a.subowner=b.usesysid and a.subdbid=d.oid;

删除发布订阅

删除订阅

订阅在运行的情况下,需要一下操作:

ALTER SUBSCRIPTION pub_test01;
##首先需要停止当前的订阅 ALTER SUBSCRIPTION subs_test01 (slot_name =NONE);
##然后将订阅的复制槽设置成空
drop subscription subs_test01;
##最后就可以删除订阅了。 ##另外一个问题,复制订阅中,如果订阅的服务停止,或无法再次连接的情况下,需要关注 发布端的数据wal log 无法清理以及膨胀的问题。 ##所以在复制订阅中的订阅停止后,如果确认订阅无法再次恢复,或者不确认多长时间恢复,则需要删除复制槽 select * from pg_replication_slots;

select pg_drop_replication_slot(slot_name) from pg_replication_slots where slot_name = 'pub_test01';

📌 转载信息
原作者:
xxdtb
转载时间:
2026/1/19 17:52:23