MySQL 外键约束引起的异构数据同步问题详解

MySQL 外键约束引起的异构数据同步问题详解

2 年前

前言

在 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 有以下几种:

  1. CASCADE : 在主表对应行被删除, 或者对应字段被更新之后, 删除自身所在行
  2. SET NULL : 在主表对应行被删除, 或者对应字段被更新之后, 将自身值设置为 NULL
  3. RESTRICT / NO ACTION : 默认行为 , 不允许主表删除对应行, 或者更新对应字段

支持一张表创建多个外键, 逐个申明即可

支持级联外键, 约束级联生效

同步方案

问题描述

在同步带有外键的子表时, 会遇到以下问题:

  1. 任务只同步子表, 不同步主表, 正常情况下, 除非用户手动在目标端建立主表, 否则子表数据无法写入
  2. 子表同步过程中, 主表发生一些导致级联的操作, 无法被正常同步到子表中

前置准备

首先, 在同步前, 应当根据同步的子表, 向上树状查找, 找出全部外键关联的层级主表, 并按照层级与 action , 分析出级联操作规则

处理过程

对于问题 1, 在建表时:

  1. 对同构数据目标, 或者支持主键约束的目标:
    1. 如果所有关联表都在同步任务中, 自动创建约束并正常同步, 同步时需要保证数据写入顺序, 并将级联操作规则关闭
    2. 如果任务中有缺失的关联表, 提示用户多选一:
      1. 将缺失的表加入到同步任务中
      2. 忽略此外键关联
      3. 用户自行保证缺失的表存在, 并自行保证数据准确性
  2. 对于不支持主键约束的目标:
    1. 提示用户此表中的约束将会被自动清理
    2. 提示用户是否需要开启主键约束处理, 如果开启, 将影响同步性能
    3. 自动将关联主表增加到监听过滤器中, 将同步表的所有上级表, 其中选中表正常同步, 所有上级表, 按照 主键为 key, 关联键为二级缓存, 自子而主逐级构建全量依赖结构缓存
    4. 同步完存量数据之后, 在增量阶段:
      1. 按照从顶层主表, 到子表的顺序, 逐个处理事件
      2. 监听到关联主表的操作, 首先更新自身缓存, 之后按照已有关联操作规则, 逐级生成子表层级关联操作, 最终落到同步目标表中, 如需操作, 新生成一个 DML 操作事件到事件流中

简化处理过程

作为 2 的特例, 如果关联外键主表, 有且只有一个, 可以省略此主表的结构缓存, 只使用逻辑操作, 在主表数据 >> 子表数据时, 可能会造成较多针对子表缓存的无效计算操作

作为 2 的特例, 子表可不使用关联缓存, 在上游表较多时, 可能造成较多无效的被同步子表数据库操作

为完成精准的前后值获取, 作为子表的特殊场景, 应将子表的全部字段存储下来, 并模拟生产 CDC 事件推送到目标

此方案最常见的情况为单级外键关联, 或者被同步的目标全在任务里, 但是目标不支持外键约束时: 此时整个方案可简化为:

  1. 对于所有主表, 不构建缓存
  2. 对于主表发生的事件, 在子表目标内做反查, 构建完整的前后值, 并生成操作同步到下游, 此方案适用于全部计算场景, 缺点是有一些既有流程的打破(源端需要感知目标端数据)
  3. 对于主表发生的事件, 不加处理全部以关联键为过滤条件, 更新/删除事件推送到目标, 缺点是无法构建完整的前后值, 且造成目标数据库有一些额外的写压力

在实际工程中, 可按照复杂度, 从最简单开始逐个实现, 满足不同层级的外键同步需求

多米诺骨牌: 万恶的循环外键

整理出分级操作时, 有一种特殊情况是出现环状外键, 这种情况需要在数据部分写入数据表之后再构建, 针对这种情况的同步, 需要做一点特殊处理:

  1. 对支持外键的目标:
    1. 在全量同步阶段开始前, 不构建外键
    2. 全量结束后, 构建外键, 并正常同步
  2. 对不支持外键的目标: 计算级联操作时, 需要循环查找, 直到找到没有新操作的事件为止

在出现循环外键时, 一个操作影响的表行数是不可知的, 两张数千万的数据表会因为一条数据的删除引发多米诺骨牌效应, 导致全部的数据被删除

演示如下:

## 创建两个无关联表 
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) 
## 查看两张表的数据