MySQL 外键约束引起的异构数据同步问题详解
前言
在 MySQL 中允许对表字段设置一些外键, 外键的存在可以保证多个表之间的数据一致性, 也可以实现一些级联操作, 但是级联操作为 CDC 同步带来一个麻烦:
被级联的操作不会被记录在 binlog 里
, MySQL 自身的主从同步是依赖结构之间的关系自动执行的级联操作, 不需要被 binlog 触发, 但是在数据表部分同步, 或者异构同步时, 不能保证目标端也有同样的外键逻辑, 无法使用目标约束来处理级联操作, 因此需要在获取增量事件, 传递到下游时自动对事件进行补全, 做到
级联操作对下游无感
, 保证数据同步的准确性
MySQL 外键简述
创建外键的语法为:
CONSTRAINT constraint_name ## 约束名字
FOREIGN KEY [foreign_key_name] (columns) ## [约束的名字] (字段名)
REFERENCES parent_table(columns) ## 外部主表的名字(字段名)
[ON DELETE action] ## 外部主表对应行被删除后触发的级联操作
[ON UPDATE action] ## 外部主表对应行对应字段, 被更新后触发的级联操作
支持跨库主键, 跨库时,
parent_table
格式调整为
parent_db.parent_table
action
有以下几种:
-
CASCADE
: 在主表对应行被删除, 或者对应字段被更新之后, 删除自身所在行 -
SET NULL
: 在主表对应行被删除, 或者对应字段被更新之后, 将自身值设置为 NULL -
RESTRICT / NO ACTION
:默认行为
, 不允许主表删除对应行, 或者更新对应字段
支持一张表创建多个外键, 逐个申明即可
支持级联外键, 约束级联生效
同步方案
问题描述
在同步带有外键的子表时, 会遇到以下问题:
- 任务只同步子表, 不同步主表, 正常情况下, 除非用户手动在目标端建立主表, 否则子表数据无法写入
- 子表同步过程中, 主表发生一些导致级联的操作, 无法被正常同步到子表中
前置准备
首先, 在同步前, 应当根据同步的子表, 向上树状查找, 找出全部外键关联的层级主表, 并按照层级与
action
, 分析出级联操作规则
处理过程
对于问题 1, 在建表时:
- 对同构数据目标, 或者支持主键约束的目标:
- 如果所有关联表都在同步任务中, 自动创建约束并正常同步, 同步时需要保证数据写入顺序, 并将级联操作规则关闭
- 如果任务中有缺失的关联表, 提示用户多选一:
- 将缺失的表加入到同步任务中
- 忽略此外键关联
- 用户自行保证缺失的表存在, 并自行保证数据准确性
- 对于不支持主键约束的目标:
- 提示用户此表中的约束将会被自动清理
- 提示用户是否需要开启主键约束处理, 如果开启, 将影响同步性能
- 自动将关联主表增加到监听过滤器中, 将同步表的所有上级表, 其中选中表正常同步, 所有上级表, 按照 主键为 key, 关联键为二级缓存, 自子而主逐级构建全量依赖结构缓存
- 同步完存量数据之后, 在增量阶段:
- 按照从顶层主表, 到子表的顺序, 逐个处理事件
- 监听到关联主表的操作, 首先更新自身缓存, 之后按照已有关联操作规则, 逐级生成子表层级关联操作, 最终落到同步目标表中, 如需操作, 新生成一个 DML 操作事件到事件流中
简化处理过程
作为 2 的特例, 如果关联外键主表, 有且只有一个, 可以省略此主表的结构缓存, 只使用逻辑操作, 在主表数据 >> 子表数据时, 可能会造成较多针对子表缓存的无效计算操作
作为 2 的特例, 子表可不使用关联缓存, 在上游表较多时, 可能造成较多无效的被同步子表数据库操作
为完成精准的前后值获取, 作为子表的特殊场景, 应将子表的全部字段存储下来, 并模拟生产 CDC 事件推送到目标
此方案最常见的情况为单级外键关联, 或者被同步的目标全在任务里, 但是目标不支持外键约束时: 此时整个方案可简化为:
- 对于所有主表, 不构建缓存
- 对于主表发生的事件, 在子表目标内做反查, 构建完整的前后值, 并生成操作同步到下游, 此方案适用于全部计算场景, 缺点是有一些既有流程的打破(源端需要感知目标端数据)
- 对于主表发生的事件, 不加处理全部以关联键为过滤条件, 更新/删除事件推送到目标, 缺点是无法构建完整的前后值, 且造成目标数据库有一些额外的写压力
在实际工程中, 可按照复杂度, 从最简单开始逐个实现, 满足不同层级的外键同步需求
多米诺骨牌: 万恶的循环外键
整理出分级操作时, 有一种特殊情况是出现环状外键, 这种情况需要在数据部分写入数据表之后再构建, 针对这种情况的同步, 需要做一点特殊处理:
- 对支持外键的目标:
- 在全量同步阶段开始前, 不构建外键
- 全量结束后, 构建外键, 并正常同步
- 对不支持外键的目标: 计算级联操作时, 需要循环查找, 直到找到没有新操作的事件为止
在出现循环外键时, 一个操作影响的表行数是不可知的, 两张数千万的数据表会因为一条数据的删除引发多米诺骨牌效应, 导致全部的数据被删除
演示如下:
## 创建两个无关联表
mysql> create table l1 (l1id int primary key, l2id int);
Query OK, 0 rows affected (0.16 sec)
mysql> create table l2 (l2id int primary key, l1id int);
Query OK, 0 rows affected (0.13 sec)
## 表一插入五条数据
mysql> insert into l1 values(1, 11);
Query OK, 1 row affected (0.37 sec)
mysql> insert into l1 values(2, 12);
Query OK, 1 row affected (0.11 sec)
mysql> insert into l1 values(3, 13);
Query OK, 1 row affected (0.11 sec)
mysql> insert into l1 values(4, 14);
Query OK, 1 row affected (0.11 sec)
mysql> insert into l1 values(5, 15);
Query OK, 1 row affected (0.11 sec)
## 表二插入五条数据
mysql> insert into l2 values(11, 2);
Query OK, 1 row affected (0.11 sec)
mysql> insert into l2 values(12, 3);
Query OK, 1 row affected (0.11 sec)
mysql> insert into l2 values(13, 4);
Query OK, 1 row affected (0.10 sec)
mysql> insert into l2 values(14, 5);
Query OK, 1 row affected (0.20 sec)
mysql> insert into l2 values(15, 1);
Query OK, 1 row affected (0.11 sec)
## 查看两张表的数据
mysql> select * from l1;
+------+------+
| l1id | l2id |
+------+------+
| 1 | 11 |
| 2 | 12 |
| 3 | 13 |
| 4 | 14 |
| 5 | 15 |
+------+------+
5 rows in set (0.05 sec)
mysql> select * from l2;
+------+------+
| l2id | l1id |
+------+------+
| 11 | 2 |
| 12 | 3 |
| 13 | 4 |
| 14 | 5 |
| 15 | 1 |
+------+------+
5 rows in set (0.05 sec)
## 为两张表增加外键
mysql> alter table l1 add CONSTRAINT foreign key (l2id) REFERENCES l2(l2id) ON DELETE CASCADE;
Query OK, 5 rows affected (0.14 sec)
Records: 5 Duplicates: 0 Warnings: 0
mysql> alter table l2 add CONSTRAINT foreign key (l1id) REFERENCES l1(l1id) ON DELETE CASCADE;
Query OK, 5 rows affected (0.19 sec)
Records: 5 Duplicates: 0 Warnings: 0
## 从表一中删除 l1id=1 的数据
mysql> delete from l1 where l1id=1;
Query OK, 1 row affected (0.10 sec)
## 查看两张表的数据