做数据同步这件事,最怕的不是技术难点,而是方案选错之后反复推倒重来。我之前负责的一个报表项目就是这样:业务库在 MySQL,搜索和报表要做到 Elasticsearch 里,一开始图省事用定时任务全量同步,白天业务高峰期不敢跑,只能凌晨跑一次,结果第二天早上数据还是昨天的,被业务方追着问。后来换成 Flink CDC 做实时同步,一套链路同时解决全量初始化、增量捕获、数据一致性这三个问题,算是把这个老痛点彻底摁住了。这篇实战文章把我从环境准备到上线调优的完整过程梳理出来,给正在做 MySQL 到 ES 同步的朋友一个可以直接抄作业的参考。
这次实战会用到 Flink CDC 2.4 版本配合 Flink 1.17,如果你对 Flink 的基础概念不算熟也没关系,我会把每个选择背后的原因讲清楚。你不需要提前精通 Flink,只要会写基本的 Java Maven 项目,跟着步骤走就能把一个能跑的同步任务搭起来。
1. 为什么是 Flink CDC?同步方案的对比与选型思考
1.1 我踩过的同步方案对比
在做 Flink CDC 之前,我把网上能搜到的同步方案差不多都试了一圈。定时全量同步最暴力也最好理解,就是把 MySQL 表数据定期导出来,清空 ES 索引再灌进去,但问题非常明显:数据延迟至少一个周期,全量导出时还会占用业务库的 IO 资源。我试过半小时跑一次,业务方还是觉得不够实时,而且数据量一大,全量重建索引的时间越来越长,凌晨跑不完的风险也随之增加。
后来我试过监听 Binlog 的方式,就是自己写一个消费者去订阅 MySQL 的 Binlog,解析出变更事件后写入 ES。这条路能拿到实时的增量数据,但坑很多:Binlog 事件格式需要自己解析,位点要自己记录和维护,重启后从哪继续读需要自己做持久化,多线程消费时的顺序问题也要处理。说白了,这套逻辑自己写一遍的成本不低,而且写出来还不一定比现成方案健壮。
还考虑过基于 Canal 加 Kafka 的架构,让 Canal 监听 Binlog 写入 Kafka,再写一个 Flink 任务消费 Kafka 写入 ES。这个架构在腾讯阿里内部确实很成熟,但对我们这个规模来说太重了,多了一套 Kafka 集群要运维,Canal 服务本身也要保证高可用,光部署调优就得花不少时间。如果团队没有专门的中间件运维人力,这套方案的上手成本并不低。
1.2 Flink CDC 的优势和适用边界
Flink CDC 让我眼前一亮的地方在于,它把全量同步和增量同步融合成了一个任务。全量同步阶段会先做一次一致性快照,把当前 MySQL 表里的数据全部读出来;快照做完之后,无缝切换到 Binlog 增量消费,把快照期间产生的新变更继续同步过来。整个过程不需要你手动停业务、不需要停机维护,读的是带锁或者无锁的快照,对业务的影响控制在比较小的范围内。
相比自己写 Binlog 消费者,Flink CDC 自带 Checkpoint 机制,位点会跟着 Flink 的检查点持久化到状态后端。任务挂了重启之后,可以从上次成功的 Checkpoint 恢复,不会重复消费也不会丢数据。这个能力对于生产环境来说太重要了,靠手写代码要做到同样的可靠程度,工程量完全不是一个量级。
再说回 ES 场景,Flink CDC 输出的是结构化的事件流,每条变更都带操作类型,包括插入、更新、删除。而 ES 正好需要根据这些操作类型来决定是写入文档还是删除文档,语义完全对得上。如果你的下游是 Kafka 或者 Hudi,Flink CDC 同样用得起来,但配合 ES 的加索引、删文档操作,是最常见的生产组合之一。
不过 Flink CDC 也不是万能的。它更适合有主键的表,无主键表虽然能同步,但更新和删除事件的语义会受影响,这点我会在后面的坑位清单里详细展开。另外,如果业务库本身没有开启 Binlog,那 Flink CDC 直接就没有数据来源,环境准备阶段需要先确认这一点。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 同步链路的关键原理:全量快照与增量日志的衔接
2.1 两大阶段:Snapshot 与 Binlog 消费
Flink CDC 的 MySQL Connector 底层用的是 Debezium,一个开源的变化数据捕获框架。Debezium 最核心的做法是把自己伪装成 MySQL 的从库,向主库发起 Binlog 复制请求,主库就会把 Binlog 事件持续推给它。因为是复制协议的合法订阅者,所以 Flink CDC 读取 Binlog 时不需要侵入业务代码,也不用在 MySQL 里装插件。
整个同步过程分成两个阶段。第一个阶段是快照阶段,也就是 Snapshot,任务启动时会把满足条件的表数据按主键分片,每个分片用一条 SELECT 语句读出来。分片机制意味着可以并行读取,大表的速度比单条全表扫描快不少。第二个阶段是增量阶段,快照读完之后,连接器自动切换为实时消费 Binlog 事件,后续的每次插入、更新、删除都会变成事件流往下游发。
这两个阶段是自动衔接的,对使用方来说是透明的。你只需要在配置里指定启动模式是 initial,Flink CDC 就会从当前时间点先做全量再自动进入增量;如果指定 latest-offset,则跳过全量,直接从当前 Binlog 位置开始消费增量。
2.2 全量与增量如何无缝衔接
这里有个关键问题:快照阶段读取的是某一时刻的数据,但快照执行期间业务库可能一直在写入,那这部分写入的数据怎么保证不丢?Flink CDC 的解决思路是给快照分段记录全局位点。
具体来说,连接器在快照开始前,会先记录一个 Binlog 位点作为起始点,然后开始读取快照数据。快照读完后,连接器不会直接跳到最新的 Binlog 位点,而是从之前记录的起始点开始消费。这样的话,快照期间产生的增量变更都会在 Binlog 中被重新读取一遍,再和快照数据合并。这种机制保证数据在语义上是不重不漏的,这也是 Flink CDC 能给出精确一次语义的基础。
在实际体验上,这个设计最直观的体会就是:你不需要关心全量同步和增量同步的边界在哪里。任务启动后,该搬的历史数据自动搬完,新产生的变更实时跟进,两边无缝衔接。不需要手动触发什么切换动作,也不需要往队列里塞边界标记。
2.3 数据长什么样子:SourceRecord 的事件解析
如果你用过 Flink CDC,一定会接触到一个概念叫 SourceRecord,这是 Debezium 定义的事件格式。每条 SourceRecord 里会包含变更前后的数据快照、操作类型、来源库表信息、Binlog 文件位置和时间戳等元数据。
在 Flink CDC 里,SourceRecord 会通过反序列化器转换成你需要的类型。最省事的方式是直接用官方提供的 JsonDebeziumDeserializationSchema,输出的是 JSON 字符串,结构大致长这样:
json复制{
"before": null,
"after": {
"id": 1,
"name": "张三",
"age": 28
},
"source": {
"db": "demo",
"table": "users"
},
"op": "c",
"ts_ms": 1690000000000
}
其中 op 字段是关键,c 表示新增,u 表示更新,d 表示删除,r 表示快照阶段读取。我们在写 ES Sink 的时候,就是根据这个字段来决定调用 ES 的 IndexRequest 还是 DeleteRequest。可以说,理解了这条 JSON 的结构,整个同步逻辑就完成了一半。
3. 环境准备:版本搭配、MySQL Binlog 与同步账号
3.1 版本搭配建议
Flink CDC 和 Flink 主版本、MySQL 版本、ES 版本之间都有兼容性要求,尤其是 Flink CDC 的不同大版本,包名都有变化。我这次用的是 Flink 1.17.1 配合 Flink CDC 2.4.2,这是目前生产环境里比较稳的组合。
| 组件 | 推荐版本 | 说明 |
|---|---|---|
| JDK | 1.8 或 11 | Flink 1.17 对 JDK 8 支持很好 |
| Flink | 1.17.1 | 1.16 也可以,但 1.17 更稳定 |
| Flink CDC | 2.4.2 | 包名是 com.ververica,网上资料最多 |
| MySQL | 5.7 或 8.0 | 必须开启 Binlog,格式为 ROW |
| Elasticsearch | 7.17.x | Java High Level REST Client 兼容性最好 |
有一点要特别提醒:Flink CDC 3.0 之后包名从 com.ververica 改成了 org.apache.flink.cdc,用法也有变化,网上搜到的资料很容易混。如果你用的是 3.x,不要直接复制 2.4 的依赖和代码,先确认版本号。
3.2 开启 MySQL Binlog
MySQL 默认是不开 Binlog 的,这是新手最容易忽略的一步。以 MySQL 8.0 为例,编辑 MySQL 配置文件 my.cnf 或 my.ini,在 [mysqld] 段下面加:
ini复制[mysqld]
server-id=1
log_bin=mysql-bin
binlog_format=ROW
binlog_row_image=FULL
expire_logs_days=7
max_binlog_size=128M
这里每个参数都有讲究。server-id 是 MySQL 实例的唯一标识,不能和 Flink CDC 连接器指定的 server-id 冲突,否则会被 MySQL 判定为重复的连接而踢掉。binlog_format 必须设置为 ROW,只有行级日志才能拿到每条变更前后的完整数据。binlog_row_image=FULL 表示记录整行数据,不是只记变更的字段,这样在更新场景下还能拿到变更前的旧值。
配置改完后重启 MySQL,然后执行:
sql复制SHOW VARIABLES LIKE 'log_bin';
SHOW VARIABLES LIKE 'binlog_format';
SHOW MASTER STATUS;
看到 log_bin 为 ON、binlog_format 为 ROW,就说明配置生效了。SHOW MASTER STATUS 会展示当前 Binlog 文件名和位点,这个可以用于后续验证。
3.3 创建同步账号与演示表
Flink CDC 读取 Binlog 需要一个有复制权限的 MySQL 账号。生产环境不建议直接用 root,单独建一个最小权限的账号:
sql复制CREATE USER 'cdc'@'%' IDENTIFIED BY 'Cdc@2024';
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'cdc'@'%';
FLUSH PRIVILEGES;
SELECT 权限用于全量快照阶段读取表数据,REPLICATION SLAVE 和 REPLICATION CLIENT 权限用于读取 Binlog。如果你用的是 MySQL 5.7,创建账号的语法略有不同,用一条 GRANT ... IDENTIFIED BY 就行。
然后建一张演示表,后面所有的同步测试都靠它:
sql复制CREATE DATABASE IF NOT EXISTS demo DEFAULT CHARSET utf8mb4;
USE demo;
CREATE TABLE users (
id BIGINT PRIMARY KEY,
name VARCHAR(100),
age INT,
email VARCHAR(255),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
INSERT INTO users(id, name, age, email) VALUES (1, '张三', 28, 'zhangsan@example.com');
4. 核心代码实现:从 Binlog 到 ES 的完整链路
4.1 Maven 依赖
新建一个 Maven 项目,Java 版本设为 1.8。核心依赖如下:
xml复制<properties>
<flink.version>1.17.1</flink.version>
<cdc.version>2.4.2</cdc.version>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-java</artifactId>
<version>${flink.version}</version>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-streaming-java</artifactId>
<version>${flink.version}</version>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-clients</artifactId>
<version>${flink.version}</version>
</dependency>
<dependency>
<groupId>com.ververica</groupId>
<artifactId>flink-connector-mysql-cdc</artifactId>
<version>${cdc.version}</version>
</dependency>
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>7.17.8</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
</dependencies>
这里提醒一下,flink-connector-mysql-cdc 会传递引入很多依赖,比如 flink-table-api-java 和 flink-table-runtime。这些是 CDC 正常工作需要的,不要用 exclusions 排除掉。如果你是在本地 IDE 里跑,flink-clients 是必须的,它负责把任务提交到本地环境并启动执行。
4.2 构建 MySQL CDC Source
写一个主类 MysqlCdcToEs,在 main 方法里构建数据源:
java复制import com.ververica.cdc.connectors.mysql.source.MySqlSource;
import com.ververica.cdc.connectors.mysql.table.StartupOptions;
import com.ververica.cdc.debezium.json.JsonDebeziumDeserializationSchema;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
public class MysqlCdcToEs {
public static void main(String[] args) throws Exception {
MySqlSource<String> source = MySqlSource.<String>builder()
.hostname("localhost")
.port(3306)
.databaseList("demo")
.tableList("demo.users")
.username("cdc")
.password("Cdc@2024")
.serverTimeZone("Asia/Shanghai")
.deserializer(new JsonDebeziumDeserializationSchema())
.startupOptions(StartupOptions.initial())
.serverId("5400-5404")
.build();
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.enableCheckpointing(5000L);
DataStream<String> stream = env.fromSource(source, WatermarkStrategy.noWatermarks(), "mysql-cdc-source");
stream.print();
stream.addSink(new EsSink("users_index"));
env.execute("mysql-cdc-to-es-users");
}
}
这里说几个关键配置的含义。databaseList 和 tableList 是白名单列表,格式是 库名.表名,支持正则。我指定的是 demo.users,这意味着只同步这一张表,不会影响其他库表。startupOptions(StartupOptions.initial()) 表示先从全量快照开始,再自动进入增量消费;如果你只需要增量数据,可以改为 StartupOptions.latestOffset()。
serverId 这里我写的是 "5400-5404" 范围。Flink CDC 的并行度如果是 5,就需要给每个并行子任务分配一个独立的 server-id。如果设置成了范围,连接器会自动分配。很多人在单机测试时只写一个固定值,任务也能跑起来,但一旦并行度大于 1 就会报错,这个后面坑位部分还会细说。
4.3 自定义反序列化器
官方提供的 JsonDebeziumDeserializationSchema 可以直接用,输出是 JSON 字符串,字段名和 Debezium 默认格式一致。如果你希望输出更简洁、更贴合下游需求,可以自己写一个反序列化器。
我建议一开始先用官方的,跑通之后再按需自定义。官方输出的数据结构在上文已经展示过,足够支撑 ES Sink 的开发。等你理解透了这个 JSON 结构,再根据自己的场景改写反序列化器会容易很多。
4.4 ES Sink 与批量写入
ES Sink 我选择自定义一个 RichSinkFunction,原因有两个。一是官方 ES Connector 对增删改的区分不够直观,尤其是 DELETE 事件处理比较绕;二是自定义 Sink 可以精确控制文档 ID、批量大小和 flush 间隔,在实际生产里更灵活。
先看核心实现:
java复制import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.functions.sink.RichSinkFunction;
import org.apache.http.HttpHost;
import org.elasticsearch.action.bulk.BulkProcessor;
import org.elasticsearch.action.bulk.BulkRequest;
import org.elasticsearch.action.bulk.BulkResponse;
import org.elasticsearch.action.delete.DeleteRequest;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestClientBuilder;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.unit.ByteSizeUnit;
import org.elasticsearch.common.unit.ByteSizeValue;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.common.xcontent.XContentType;
public class EsSink extends RichSinkFunction<String> {
private final String indexName;
private RestHighLevelClient client;
private BulkProcessor bulkProcessor;
public EsSink(String indexName) {
this.indexName = indexName;
}
@Override
public void open(Configuration parameters) {
RestClientBuilder builder = RestClient.builder(new HttpHost("localhost", 9200, "http"));
client = new RestHighLevelClient(builder);
BulkProcessor.Listener listener = new BulkProcessor.Listener() {
@Override
public void beforeBulk(long executionId, BulkRequest request) {
}
@Override
public void afterBulk(long executionId, BulkRequest request, BulkResponse response) {
if (response.hasFailures()) {
System.out.println("Bulk has failures: " + response.buildFailureMessage());
}
}
@Override
public void afterBulk(long executionId, BulkRequest request, Throwable failure) {
System.out.println("Bulk failed: " + failure.getMessage());
}
};
BulkProcessor.Builder bpBuilder = BulkProcessor.builder(
(request, bulkListener) -> client.bulkAsync(request, RequestOptions.DEFAULT, bulkListener),
listener);
bpBuilder.setBulkActions(1000);
bpBuilder.setBulkSize(new ByteSizeValue(5, ByteSizeUnit.MB));
bpBuilder.setFlushInterval(TimeValue.timeValueSeconds(5));
bpBuilder.setConcurrentRequests(1);
bulkProcessor = bpBuilder.build();
}
@Override
public void invoke(String value, Context context) {
JSONObject obj = JSON.parseObject(value);
String op = obj.getString("op");
JSONObject after = obj.getJSONObject("after");
if (after == null) {
after = obj.getJSONObject("before");
}
if (after == null) {
return;
}
String docId = String.valueOf(after.get("id"));
if ("d".equals(op) || "t".equals(op)) {
bulkProcessor.add(new DeleteRequest(indexName).id(docId));
} else {
bulkProcessor.add(new IndexRequest(indexName)
.id(docId)
.source(after.toJSONString(), XContentType.JSON));
}
}
@Override
public void close() throws Exception {
if (bulkProcessor != null) {
bulkProcessor.flush();
bulkProcessor.close();
}
if (client != null) {
client.close();
}
}
}
这个 Sink 最关键的设计是使用 BulkProcessor。ES 批量写入比逐条写入要高效得多,BulkProcessor 会自动攒批,攒满 1000 条或 5MB 就自动提交一次,同时每 5 秒强制 flush 一次,避免数据积压太久。concurrentRequests(1) 表示并发提交的批次数,稍微降低并发可以避免打爆 ES 的写入线程池。
文档 ID 我直接用业务表的主键 id 来映射 ES 文档的 _id。这样做的好处是,MySQL 里同一行数据的更新和删除都能精确对应到同一个 ES 文档,不会产生重复文档。如果你不指定 _id,ES 会自动生成随机 ID,那更新删除就会完全乱掉。
4.5 任务提交与验证
本地跑的时候,直接运行 main 方法。任务启动后,控制台会打印两条关键日志:一条是 Starting MySqlSource,另一条是 Snapshot 相关的 log,说明全量快照已经开始读取。
启动后去 ES 查询一下:
bash复制curl -XGET 'http://localhost:9200/users_index/_search?pretty'
应该能看到刚才插进去的那条用户数据。然后再往 MySQL 里插入一条新数据:
sql复制INSERT INTO users(id, name, age, email) VALUES (2, '李四', 30, 'lisi@example.com');
几秒钟后再查 ES,新数据已经出现在索引里了。接着试一下更新和删除:
sql复制UPDATE users SET age = 31 WHERE id = 2;
DELETE FROM users WHERE id = 2;
删除后 ES 里的文档也应该随之消失。这一套操作跑通,说明从 Binlog 到 ES 的整条链路已经正常工作。
5. 上线前必须知道的 9 个坑位清单
5.1 坑一:Binlog 没开但任务不报错
这是最容易踩的坑。MySQL 如果没有开启 Binlog,Flink CDC 任务启动后不会直接报错,因为全量快照阶段读取的是表数据,不依赖 Binlog。等全量读完了,任务会一直卡在增量消费阶段,看起来就像“停止同步”了一样,但print() 还在打印快照读到的数据。
排查方法很简单:先确认 MySQL 的 binlog_format 是否为 ROW,再看任务日志里是否出现 Binlog 相关的偏移信息。如果设置了 latestOffset() 启动模式,而 Binlog 又没开,任务启动后会一直等 Binlog 位点,表现和上面一样。
解决方式没有捷径,就是开启 Binlog 并重启 MySQL,然后重启 Flink 任务。所以环境准备阶段一定要先执行 SHOW VARIABLES LIKE 'binlog_format' 确认一遍再开始写代码。
5.2 坑二:server-id 冲突导致连接被 Kill
Flink CDC 连接器会模拟 MySQL 从库去拉 Binlog,每个并行子任务需要一个独立的 server-id。如果你在多个任务里使用了相同的 server-id,或者在并行度大于 1 时只写了一个固定值,MySQL 会判定为 slave 冲突,主动断开连接。
典型报错是 ERROR 1064 或者 The slave is connecting using CHANGE MASTER TO MASTER_AUTO_POSITION = 1, but the master has purged binary logs。前者是权限和复制协议问题,后者是 Binlog 被清理了。
我的建议是:如果有多个 CDC 任务,给每个任务划分不同的 server-id 段。比如任务 A 用 5400-5404,任务 B 用 5405-5409,避免互相冲突。这个配置看着不起眼,但上线后因为并发引起的数据中断,排查起来非常头疼。
5.3 坑三:时区差 8 小时
Flink CDC 读取 MySQL 的 TIMESTAMP 字段时,如果时区设置不对,写入 ES 的时间会比实际少 8 小时。这个问题的根因是 MySQL 的 JDBC 连接时区默认是服务器本地时区,而 Flink CDC 在解析时间字段时会按 Debezium 配置的时区来转。
解决办法有两个。一是在构建 MySqlSource 时显式指定 serverTimeZone("Asia/Shanghai");二是给 JDBC URL 配上 serverTimezone=Asia/Shanghai。我推荐第一种,因为 Flink CDC 的 builder 直接预留了这个方法,语义更清晰。
这里有个细节:如果你用 Docker 启动的 MySQL,容器里默认时区可能是 UTC,即使服务器时区是东八区,也会出现时间偏移。所以启动 MySQL 容器的时候最好加上 -e TZ=Asia/Shanghai,从源头保持一致。
5.4 坑四:BigInt Unsigned 变成二进制
MySQL 里 BIGINT UNSIGNED 字段在 Debezium 的默认映射下会被解析成 BIGINT_UNSIGNED,在 Java 里拿到的可能是字节数组,直接写进 ES 后变成了一串乱码或空值。
我踩过一次之后,总结了两种处理方式。第一种是在自定义反序列化器里对 BIGINT_UNSIGNED 做特殊转换,用 after.get(fieldName) 之后判断类型,如果是字节数组就通过 new BigDecimal((byte[]) value) 还原成数字。第二种更省事,直接把字段类型改成 DECIMAL(20,0),避免触发无符号大整数的特殊处理。
如果业务表已经用了 BIGINT UNSIGNED,那就只能走第一种方式。这个问题在高并发订单表里很常见,因为雪花 ID 往往用 BIGINT UNSIGNED,同步到 ES 后如果你用必须精确匹配的字段做查询,就会踩到。
5.5 坑五:DELETE 事件没处理
默认的 ES Sink 通常只处理 INSERT 和 UPDATE,对 DELETE 事件要么忽略,要么当成空值更新。如果不处理 DELETE,MySQL 里删掉的数据在 ES 里一直存在,搜索的时候能搜到已删除的内容,这在生产环境是非常严重的。
我在前面的 EsSink 代码里专门判断了 op 为 d 或 t 的情况,调用 DeleteRequest。这里有两个点要注意:一是删除操作必须指定文档 ID,否则 ES 不知道删哪个文档;二是有些场景下 after 字段在删除事件里是 null,要回退到 before 字段取值。我写的代码里 after == null 时就取 before,就是干这个用的。
如果你用官方 ES Connector 的 SQL 方式,删除事件的处理会麻烦一些,需要额外配置主键映射。所以从这个角度说,自定义 Sink 在 CDC 场景下反而更可控。
5.6 坑六:无主键表的同步限制
Flink CDC 对无主键表的支持是有条件的。全量快照阶段需要按照主键分片读取,没有主键时分片就没法做,只能全表扫;增量阶段的 UPDATE 和 DELETE 事件默认只支持某种特定模式,而且操作前值和操作后值的关联能力弱。最简单的表现就是:无主键表同步插入没问题,但更新和删除到了 ES 里对不上号。
如果业务上必须同步无主键表,我的建议是在 MySQL 里给表加一个自增主键,哪怕这个主键业务上用不到,只作为 CDC 的分片和文档 ID 使用也行。如果改不了表结构,那就要接受“只做插入同步、更新删除以全量重建兜底”的方案,用定时任务定期重建索引来修正。
5.7 坑七:全量阶段 MySQL 压力飙升
Flink CDC 全量快照默认会并发读取表数据,如果表的行数特别大,同时并发度过高,MySQL 的 CPU 和 IO 会被打高。这在白天业务高峰期启动迁移任务时尤其明显,可能会影响线上业务的写入性能。
解决思路
