CDC实时同步如何构建UK冲突感知的并发模型

背景

在线数据实时同步是数据集成软件需要处理的一个重要场景。有别于构建实时数仓、容灾等场景,在线数据实时同步对于数据本身端到端的延迟、可靠性会比其他实时同步场景要求高得多。这个核心原因是源对端的数据都应用在在线业务。这种场景下源对端的数据库往往是需要ACID的TP数据库,如果产生延迟和数据问题对企业的业务影响会非常大。数据软件在处理这种在线数据实时同步的场景时会面临很多挑战,其中的一点也是今天需要分享的——在线数据实时同步中UK冲突的高效处理。
image.png

问题说明

在线数据同步一个显著特征就是源对端的关系表都是需要用于在线业务的,表结构包含UK是非常普遍的情况。用户一次性会订阅非常多的表的实时变更,如何高效实时同步这些数据到对端数据库关键是设计一个高效的并发模型,可以并行化地实时同步这些数据,同时保证这些数据写入对端后是最终一致的。

比较容易想到的几个并发模型主要是:

  • 表级并行:数据按照其所属的表做分区,然后并行。优点是同一个表内的数据完全串行化,可以规避由于同表内数据并发导致的UK冲突或数据库死锁问题,缺点是订阅表的数量较小时无法发挥多核的性能,整体并发能力不行。
  • pk hash: 按照数据行的pk进行hash。一般而言pk均匀分布的话,这种方式不会有太大的数据倾斜问题,同时并发性能也会比较好。相同pk上的变更,都是串行化执行的,数据是最终一致的。但是这种方式的缺点是没有考虑uk,也就是并发执行会有概率导致写入时UK冲突或者数据库死锁。

以上两个方案,第一个性能在一些场景下不太行,第二种则是无法解并发执行时UK引发的问题。在介绍本文讨论的新并发模型之前,我们先来理解下并发同步UK列时产生的问题。

UK冲突

我们假设同步时按照PK值对行数据进行分桶。考虑以下的执行序列,按照默认pk hash的方式并行则会产生UK冲突问题导致数据无法写入。订阅的表结构假设如下:

1
2
3
4
5
6
7
CREATE TABLE IF NOT EXISTS `test_table`(
`id` INT UNSIGNED AUTO_INCREMENT,
`name` VARCHAR(32) NOT NULL,
`c_uk` varchar(64) not null ,
PRIMARY KEY ( `id` ),
UNIQUE KEY (`c_uk`)
)ENGINE=InnoDB DEFAULT CHARSET=utf8;

我们初始化2条数据,按照id、name、c_uk这三个列的顺序,初始化的第一个值为 [1,wanshao,‘1’],我们后续用该表达方式表示一行数据。

1
2
insert into test_table(`id`,`name`,`c_uk`) values (1,'wanshao','1');
insert into test_table(`id`,`name`,`c_uk`) values (2,'wanshao','2');

我们构造一个DML执行的序列,模拟binlog的产生。下图的DML执行序列只是将id=1和id=2的行中UK的值进行了交换,过程如下。
image.png

  1. [1,‘wanshao’,‘1’]
  2. [2,‘wanshao’,‘1’]
  3. [1,‘wanshao’,‘2’]

如果将以上变更按照pk hash并行化写入对端,假设并行度设置为2,则写入时的过程如下。我们将3个写入对端的SQL编号,在pk hash的并发模型下我们只能保证②肯定比①先执行,但是③和①②之间的先后顺序是不可控的。只有执行序列是①③②才可以正常写入,如果是其他情形,都会导致UK冲突。
image.png

数据库死锁

假设我们还是按照pk hash的默认策略来做写入,除了UK冲突导致无法正常写入还可能在写入时引发对端数据库产生死锁。一张表有联合UK,并在其上进行并发UPDATE在某些数据负载情况下会产生死锁,接下来我们会构造这样的场景。我们首先准备一张表和一些数据:

1
2
3
4
5
6
7
8
CREATE TABLE IF NOT EXISTS `test_table`
(
`id` INT UNSIGNED AUTO_INCREMENT,
`uk1` int not null,
PRIMARY KEY (`id`),
UNIQUE KEY (`uk1`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;

假设有一个正常的执行序列:

1
[1,1] --> [1,2] --> [1,5] --> [2,2] --> [2,1] --> [2,3]

如果按照PK HASH去分区并行执行,分区数为2,2个线程分别处理pk=1和pk=2行上的变更,写入对端时则实际为2个事务进行写入。实际写入场景可能产生如下的事务和写入序列。以下A和B两个事务,在uk1列的变更上会互相等待获取锁导致产生死锁异常,然后导致写入失败。

1
2
3
A [1,1] --> [1,2] --> [1,5]
B [2,2] --> [2,1] --> [2,3]

设计方案

总体思路

为了解决UK冲突以及死锁问题,我们可以设计一个感知UK冲突的并发模型。根据前文的问题描述,我们可以了解产生这些问题的本质原因是并行时没有考虑UK导致相同UK值的行变更执行顺序不可预测,引发死锁或者UK冲突。理解本质问题产生原因后我们可以想到设计UK冲突并发模型的总体思路:

  • 对变更的数据流进行探测,识别冲突
  • 识别冲突后根据UK值的情况对并发序列进行调整,使得并发时相同UK值上的变更也是串行化执行的

总体流程

整体方案的工作流程参考如下:

  1. 有序的change data数据流进入到数据集成软件的内存中
  2. 针对数据流开始冲突检测,识别UK冲突的行,记录这些行所有的主键值集合,记为S。将当前数据流中主键值在集合S中的行全部按照其原本的顺序剥离到一个独立的队列,称之为冲突队列。该队列的数据按照设置的并行度和表数量做二次分区和并行。如果检测不到冲突,则冲突队列为空,不冲突的数据可以按照默认的pk hash做并行。这里一定要注意在不破坏第一次pk hash的分区基础上对冲突队列做uk感知的二级分区,否则会破坏pk上的执行顺序导致数据不一致。

image.png

之所以采用这样的设计有以下考虑:

  • 在整体PK hash的并发模型下做二级分区,有较好的数据均衡和性能:采用PK HASH的并发模型,整体上数据分区相对比较均衡,即使出现热点更新的场景,也可以通过热点数据合并的技术来使得总体的更新不会有过于严重的数据倾斜问题。数据均衡的情况下则可以更好的发挥并行处理的优势。
  • 冲突队列二级分区提升并行能力:为避免大量UK冲突导致彻底退化成单线程串行执行。冲突队列按照数据所属表做二次分区,进一步提升冲突下的并行能力。

如何探测UK冲突

探测UK冲突是实现感知UK冲突并发模型的关键。实现UK冲突探测时需要考虑的问题包括:

  • 联合UK的情况
  • 多个UK约束的情况

总体上UK探测的执行步骤可以归纳为:

  1. 加载数据关联的表的结构信息,获取关键的UK约束信息。这里需要做好schema信息的缓存以及一致性。
  2. 遍历所有UK约束,每个UK约束约定了UK列的集合,称之为U。针对需要写入对端的数据,探测其中集合U中列的值,如果存在相同值的情况,则视为探测到冲突。将这些行对应的PK值相同的其他行一并放入冲突队列等待后续二级分区。注意整个UK遍历过程中,探测到的冲突行放入冲突队列时依然需要做好保序,确保其不改变原有的执行次序。

总结

在线数据实时同步,在感知约束冲突的情况下做好高性能的写入是非常重要的课题。类似的问题还有PK约束冲突时如何避免由于约束冲突带来的性能劣化问题。这个后续在于大家分享相关内容。