开源数据同步中间件实战:CDC增量、断点续传与多源接入指南

📅 发布时间:2026/10/22 4:07:02
开源数据同步中间件实战:CDC增量、断点续传与多源接入指南
简介这是一款开源数据同步中间件DBSyncer的完整资源包面向需要处理异构数据库间数据同步的开发者、运维工程师及架构师。它覆盖MySQL、Oracle、SqlServer、PostgreSQL、Elasticsearch、Kafka、File、SQL等多种同步场景支持上传插件自定义同步转换业务并自带全量/增量数据监控统计图与应用性能预警能有效降低多源数据集成与实时同步的落地门槛。压缩包内含737个文件大小2.07MB主要由472个Java源码文件、81个PNG图片、59个CSS样式、42个HTML页面及36个JS脚本构成同时包含SQL脚本、XML配置、Shell/Bat启动脚本、Proto定义等便于二次开发或直接部署调试。目前已有747人学习下载。通过该包可获取完整项目源码、监控看板静态资源、启动与构建脚本以及核心配置示例适合需要研读中间件实现原理、快速搭建同步环境或基于DBSyncer进行扩展开发的技术人员使用。1. 为什么说开源数据同步中间件解决的是“库到库”的最后一公里数据同步这件事业务侧看是“把表搬过去”工程侧看是“搬过去之后还能不能跟上、丢没丢、重复没重复”。我们团队最早自己写同步任务MySQL到PostgreSQL白天看起来正常一到大促轮询逻辑把源库压得CPU飘红凌晨爬起来补数据是常态。后来把增量改成读binlog全量用分片并发拉这其实就是开源数据同步中间件标准做的事。它把MySQL、Oracle、SqlServer、PostgreSQL、File、Kafka、SQL这些读写场景抽象成一个个可插拔的连接器让你用一套配置就完成“源端-目标端”的同步链。这篇笔记适合正在做数据平台、实时数仓或者被跨库同步逼到想自己写框架的同学我会从原理、最小配置、多源接入一直讲到真正会遇到的坑。2. 同步中间件的核心机制增量、全量、映射是怎么被拆开的2.1 为什么基于日志的CDC是增量同步的最优解先澄清一个点同步中间件的“增量”不一定是CDC。有些轻量工具走的是基于时间戳的轮询原理是定时发起SELECT ... WHERE update_time ?把新变更的记录拉出来。这套方案在表不大、更新不频繁、又能容忍删除漏同步时实现成本最低很多中间件也提供了这个选项。但真正上了生产环境我通常不会选它。轮询有几个绕不开的问题。第一要求业务表必须有可靠的修改时间字段不少业务表压根没有。第二删除操作如果走的是物理删除轮询看不到任何变化。第三高频更新时时间戳可能重复用还是会导致丢数据或重复数据需要在应用层做额外去重。所以主流开源中间件更倾向于基于日志的CDC。MySQL走binlogOracle走redo logSQLServer走事务日志的CDCPostgreSQL靠WAL和逻辑复制。中间件把自己伪装成一个从库按主从协议请求日志流然后解析出INSERT、UPDATE、DELETE事件。这样拿到的不是“某张表的某个瞬间快照”而是一串连续、有序、带事务边界的变更事件流。以下几个维度直接决定选哪个方案方案侵入性删除事件源库性能影响适用场景时间戳轮询高依赖业务加字段不能感知有额外查询压力小表、可容忍分钟级延迟日志CDC低只需开启日志保留能感知极小大表、实时、高频写、删除敏感除了选型还有一点容易被忽略CDC对源库日志格式有要求。MySQL要设置binlog_formatROW因为只有ROW格式才记录每一行的字段变化前后值STATEMENT格式只记录执行语句解析出来后字段值是不完整的。很多同步任务“能跑但数据不对”问题往往就出在这里。选CDC之后第一件事是确认源库日志配置合标。2.2 全量初始化与增量续跑断点续传是怎么衔接的用CDC做增量之前得先把存量数据搬过去这就涉及“全量增量”的衔接。不少中间件的启动模式里都有一项叫initial含义是首次启动先做一致性快照再自动切换到增量。衔接过程大致是四步任务启动时先向源库发起一个一致性快照读同时在后台记录当前的日志位置MySQL里就是mysql-bin.000013: 4412367这样一组坐标。对目标表按主键范围做切分分成多个分片用多个线程并发select全量数据并写入目标端。全量跑完后回到第1步记下的坐标开始消费增量日志。从快照产生到增量切换之间发生的变更不会丢。后续每消费一批日志并写完目标端就把坐标持久化到本地或第三方存储作为检查点。这个流程里有两处边界要特别上心。第一快照必须基于“可重复读”如果全量读到一半源库又改了数据全量数据和增量数据衔接时就会出现空洞。MySQL依赖的是事务隔离级别和一致性快照读中间件通常会在内部处理但我们在选型时要确认它确实做了。第二全量阶段失败重试不需要整表重来只需从最近成功的一个分片处继续。我之前见过一个表全量同步跑了两小时因为某个分片偶发超时就整表重新拉结果永远跑不完这就是断点续传设计不到位。断点续传还有一层意思进程重启、网络抖动、目标端写失败后任务恢复时是从记录的坐标继续读而不是从最早的位点重新来。这保证了“不丢”但也意味着一部分日志会被重复读到。所以目标端写入采用at-least-once语义允许重复不能丢。中间件解决的是“不丢”幂等则要靠写端实现。2.3 同步任务的三层模型读端、写端、映射有了增量能力和断点续传接下来看中间件如何组织任务。绝大多数开源实现都沿用同一套抽象读端reader、写端writer、映射transform。读端连接器负责连接数据源把源端的行数据统一转成中间格式。比如MySQL读出一行后会转成类似{id: 1001, amount: 99.90, create_time: 2024-06-01 12:00:00}的标准结构。Oracle、SQLServer、PostgreSQL也各有对应读端连接器转换后的结构完全一致。写端连接器把标准事件写到目标端。写到Kafka是发送消息写到MySQL是执行INSERT/UPDATE写到文件是追加一行CSV。由于读和写被拆开“MySQL到Oracle”、“MySQL到Kafka”、“File到MySQL”本质上是同一套引擎只是换了连接器组合。映射层做中间加工。常见的有字段改名、字段裁剪、常量注入、类型转换以及时间格式化有些中间件还允许用SQL对数据流做复杂处理。映射在配置里通常是一张表或一条规则写明源字段、目标字段和转换逻辑。用YAML描述的最小同步任务骨架大概是这样的job: name: demo_sync reader: connector: mysql-cdc transform: - rename: { order_id: id } writer: connector: kafka骨架虽短但代表了所有同步中间件的核心数据源被抽象成连接器同步过程被拆成“读—转—写”三个阶段。理解这一点之后后续新增一种数据源接入本质就是多接一个读端连接器任务模型本身不会变。3. 用中间件跑通第一条链路MySQL到Kafka的最小可运行作业3.1 部署形态一个Java进程几条初始化命令大多数开源数据同步中间件是Java实现原因是JDBC生态和Kafka客户端生态最成熟。部署多数都不复杂一个安装包、一个Java环境、几条启动命令。下面按最常见的目录结构做说明。# 解压安装包到指定目录例如 /opt/sync unzip sync-middleware-bin.zip -d /opt/sync cd /opt/sync # 确认Java环境8或11都能跑 java -version # 检查内置驱动MySQL驱动一般自带 ls lib/ | grep -i mysql解压后目录里通常会有bin/启停脚本、lib/依赖jar包、conf/全局配置、logs/运行日志几个子目录。第二个命令确认JDK版本版本过低会直接启动失败。第三个命令确认MySQL驱动在lib下如果用Oracle或SQLServer这一步就要特别注意驱动jar包经常需要手动放进目录。注意如果lib/下看不到MySQL驱动先不要启动任务把对应版本的驱动放进去再启动否则任务会停在加载连接器阶段。3.2 最小可用配置MySQL到Kafka的YAML部署就绪后最核心的是提交一个同步任务配置。下面是MySQL到Kafka的最小配置采用目前开源中间件里最常见的YAML风格不同项目在键名上略有差异但整体结构相通job: name: mysql_orders_to_kafka mode: streaming reader: connector: mysql-cdc hostname: 127.0.0.1 port: 3306 username: sync_user password: SyncPass2024 database: shop tables: [orders, order_items] server-id: 5400-5404 startup: mode: initial writer: connector: kafka bootstrap.servers: 10.0.0.5:9092,10.0.0.6:9092 topic: ods_orders semantics: at-least-once transform: - rename: order_id: id参数逐项说明mode: streaming表示任务持续运行而不是跑一次退出。server-id: 5400-5404是范围值CDC读取binlog时每个读线程要模拟成一个独立的从库实例范围里的每个ID代表一个可并行线程。如果配置了多个表这个范围可以适当放大但不能与源库上真实从库的ID冲突。startup.mode: initial的含义已经在上一章说过首次运行先做全量全量结束后自动切换增量。semantics: at-least-once表示写入Kafka时允许重复但不会丢失这是最常用的默认语义。在源库侧还要把同步账号和日志准备好。下面这段SQL是通常的初始化动作-- 源库开启binlog在my.cnf中设置log-binON后重启然后创建同步账号 CREATE USER sync_user% IDENTIFIED BY SyncPass2024; GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO sync_user%; FLUSH PRIVILEGES; -- 确认binlog开启并查看当前位点 SHOW VARIABLES LIKE log_bin; SHOW MASTER STATUS;权限上最容易漏的是REPLICATION SLAVE和REPLICATION CLIENT。漏掉任何一个任务一启动就会报没有读取binlog的权限。SHOW MASTER STATUS的输出会被中间件用来确认位点格式排查问题时也经常要看这里。3.3 启动任务并验证数据是否真的到了Kafka配置就绪后执行启动脚本然后观察日志# 启动任务--job 指定任务名 bin/sync.sh start --job mysql_orders_to_kafka # 实时看日志确认全量阶段和增量阶段的状态 tail -f logs/sync.log日志里出现类似snapshot finished, switching to binlog position的字样时说明全量阶段结束已经切到增量。为验证同步效果可以在源库插一条数据再去Kafka消费确认# 造一条测试数据 mysql -h127.0.0.1 -usync_user -p -e INSERT INTO shop.orders(id, amount, user_name) VALUES(1001, 99.90, tester); # 消费topic确认同步结果 kafka-console-consumer.sh \ --bootstrap-server 10.0.0.5:9092 \ --topic ods_orders \ --from-beginning消费端如果能看到包含id: 1001的JSON消息这条链路就算通了。注意--from-beginning会从头消费如果只想看增量效果去掉这个参数直接监听即可。我第一次跑这种链路时遇到最多的两类启动失败一是账号没有REPLICATION权限二是server-id和源库已有从库冲突。第二类报错通常带着“slave is connecting”或“server-id already in use”字样解决方法是给同步任务分配一段不冲突的ID范围比如5400-5404避开常见的3306、3307号段。4. 把Oracle、SQLServer、PostgreSQL接进来驱动、连接配置与类型映射4.1 驱动包放对位置驱动类名别写错MySQL驱动通常随安装包自带Oracle、SQLServer、PostgreSQL的驱动则常需要手动放。以Oracle为例把ojdbc8.jar放进lib/目录即可。驱动放错位置的表现很迷惑同步任务能创建一建立连接就抛ClassNotFoundException或No suitable driver found。# Oracle驱动放进lib目录后确认jar包在 ls -l lib/ojdbc8.jar # 重启中间件进程确保驱动被重新加载 bin/sync.sh restartOracle连接串也有讲究。一种是老式SID格式一种是新式服务名格式# SID方式适合非容器数据库 jdbc:oracle:thin:10.0.0.8:1521:orcl # 服务名方式适合CDB/PDB容器数据库 jdbc:oracle:thin://10.0.0.8:1521/ORCLPDB1如果源库是容器数据库PDB用SID方式连接会报ORA-12505改成服务名方式即可。SQLServer侧则要留意encrypt参数新版驱动默认校验证书测试环境常常直接握手失败关闭加密即可jdbc:sqlserver://10.0.0.9:1433;databaseNamesales;encryptfalse;trustServerCertificatetrue4.2 各源端连接参数对照表把几个常用数据库的连接要素放一起差异就很直观源数据库驱动类连接串示例增量方案重要前置配置MySQLcom.mysql.cj.jdbc.Driverjdbc:mysql://host:3306/shop?useSSLfalsebinloglog-binON, binlog_formatROWOracleoracle.jdbc.OracleDriverjdbc:oracle:thin://host:1521/serviceredo log开启补充日志SQLServercom.microsoft.sqlserver.jdbc.SQLServerDriverjdbc:sqlserver://host:1433;databaseNamesales;encryptfalse事务日志CDC开启变更捕获PostgreSQLorg.postgresql.Driverjdbc:postgresql://host:5432/dbWAL逻辑复制wal_levellogicalOracle的redo log在开启补充日志后才能解析出字段级前后镜像。最小补充日志可以用下面这条SQL开启-- 开启最小补充日志 ALTER DATABASE ADD SUPPLEMENTAL LOG DATA; -- 对关键表开启完整行级日志 ALTER TABLE ORDERS ADD SUPPLEMENTAL LOG DATA (ALL) COLUMNS;提示Oracle增补日志的开启命令要由DBA配合执行开启后redo日志量会上升磁盘空间要为可能的增长留出余量。PostgreSQL走逻辑复制前要修改postgresql.conf并重启实例wal_level logical max_replication_slots 10 max_wal_senders 10这三个参数不调整逻辑复制槽无法创建任务日志多半会出现 “replication slot creation failed”。4.3 字段类型映射类型不同才是同步里真正麻烦的连接能建立只是第一步数据真正搬过去后字段类型不匹配的问题才暴露。以下是我在同步任务里经常遇到的映射问题源库类型中间件转换后的类型目标端常见问题Oracle NUMBER(18,2)decimal目标DECIMAL精度正常若落成浮点类型则精度丢失SQLServer datetime2timestamp精确到100nsMySQL datetime只到秒需按需求取舍PostgreSQL UUIDstring目标MySQL无UUID类型通常转字符串MySQL JSONstring写Oracle时可能超varchar长度需确认目标字段长度处理思路就是“类型映射表”。每个同步配置里都会有一张默认映射把源端类型转成中间类型再转成目标类型。默认映射不满足时就得在任务配置里显式覆盖。一个实际案例Oracle的NUMBER类型没有标度时中间件可能推断成BIGINT但字段实际有小数目标端就会精度丢失。全量阶段这种问题完全看不出来到增量阶段某条带小数的记录写入时就失败。规避办法是同步前先核对源库字段元数据再在映射规则里写明目标类型。4.4 多源接入时的验证习惯接一个新源不要直接上大表。我一般先把连接、驱动、权限验证通再挑几张数据量在几百行的表试运行。试运行重点看三类现象全量完成后是否报错增量阶段插入、更新、删除三类事件是否都能到达目标端字段长度和精度是否被隐性截断。确认这三点没问题再放大到生产表。5. 数据同步中间件避坑这些坑我基本都踩过先说一句下面这些都不是罕见的边角错误而是接了多种数据库之后几乎都会遇到的通病。我用血泪经验换来的结论每一条都可以直接抄去排查。5.1 时区偏移同步过来的时间凭空少了8小时现象源库MySQL存的是2024-06-01 12:00:00同步到目标端变成2024-06-01 04:00:00。原因中间件解析日志时把DATETIME作为UTC时间处理再按中间件所在时区转成目标时区。源库连接串里的时区参数与JVM默认时区不一致时就会出现“本地时间被当成UTC再转一次”的两段转换。解决统一三处的时区。MySQL连接串显式加serverTimezoneAsia/Shanghai中间件进程用TZAsia/Shanghai启动任务配置里也写明时区参数。三处对齐后8小时偏移基本消失。TZAsia/Shanghai bin/sync.sh start --job mysql_orders_to_kafka5.2 大事务把同步延迟打满现象源库一个事务更新了20万行同步任务没崩但目标端消息突然堆积延迟从秒级变成几十分钟。原因日志解析以事务为边界一个事务内的所有变更必须按顺序落地。目标端是事务库时一个事务写20万行必然花费大量时间目标端是Kafka时会逐条发送这条事务的消息。大事务天然拉长整条链路的延迟并形成明显尖峰。解决在同步侧可以增加写端并行度让多个事务并发落地但单个事务内部仍然保序。也可以缩短checkpoint间隔让单次提交的数据量下降。最兜底的做法是配一个延迟告警源库执行大事务时同步延迟超过阈值就通知值班人员至少不用等业务侧反馈才知道。5.3 DDL变更导致同步断流现象源库执行ALTER TABLE orders ADD COLUMN coupon_amount DECIMAL(10,2)之后同步任务进入异常状态日志反复报解析失败。原因日志流里除了DML事件还有DDL事件。中间件维护的表结构元数据如果没有同步刷新解析出来的新字段会与已有映射不匹配导致整条管道停摆。解决多数中间件支持DDL同步或忽略需要提前配置。能忽略DDL的任务会跳过结构变更继续增量能同步DDL的工具会把结构变更同步到目标端但要求目标端也支持同类DDL。更稳妥的方式是建立结构变更流程源库表结构变更前先在同步平台暂停任务、更新映射、再恢复。人工介入虽然麻烦但能避免大量不可控的解析异常。5.4 无主键或唯一索引的表目标端出现重复记录现象源端历史表没有主键目标表开始出现重复记录且UPDATE事件被错误地变成了INSERT。原因写端默认依赖主键判断“更新还是插入”。没有主键时写入没有定位基准只能走追加插入或全表更新。解决在同步配置里给表指定“逻辑主键”即从字段里挑一个或多个业务上唯一的列组合作为定位键。如果整张表连一个可靠业务键都没有就需要先回源库补主键否则同步只是“追加日志”没有业务正确性。5.5 重复消费导致目标端幂等失效现象网络抖动Kafka消费者触发重平衡重新消费了部分消息目标库又是MySQL结果部分记录插入两次主键冲突报错。原因同步语义是at-least-once允许重复投递但目标端写入必须是幂等的。Kafka到MySQL链路如果用的是普通INSERT而不是“存在即更新”重复消息就会变成重复数据。解决在写端开启幂等写入。常见写法是INSERT ... ON DUPLICATE KEY UPDATE或在写端配置里把write-mode设为upsert。目标端是文件时重复无法避免就需要在文件名或文件内容里加批次唯一标识配合下游去重。6. 再进一步把File和SQL同步做成数据管道的一部分6.1 从CSV文件同步到MySQL的定时任务中间件的File连接器很适合处理“离线导出文件”这个场景。数据文件放到指定目录后同步任务按周期扫描把CSV内容写到目标MySQL表。配置大体是这样的reader: connector: file path: /data/sync/input file-format: csv skip-header: true encoding: utf-8 fields: [id, user_name, amount, create_time] writer: connector: jdbc url: jdbc:mysql://127.0.0.1:3306/ods table: ods_user_amount write-mode: upsert用文件同步时我一般额外处理两件事一是在文件名里带批次号防止重复扫描同一个文件二是同步完成后把文件移到备份目录避免二次消费。6.2 SQL同步的轻量聚合技巧另一种常被低估的用法是SQL同步即把“源表到目标表”变成“SQL查询结果到目标表”适合做轻量级聚合并行清洗。比如把源端多张明细表聚合成汇总表或者按时间做过滤后入仓SELECT order_date, SUM(amount) AS total_amount, COUNT(*) AS order_count FROM source_orders WHERE status PAID GROUP BY order_dateSQL同步要守边界复杂的JOIN和子查询会大幅增加排查难度建议只处理单表过滤、字段裁剪、简单聚合。需要做复杂拆分的交给专门的数据处理引擎更合适。说一个我自己养成的小习惯每个SQL转换任务我都会在目标表上建一个sync_version字段写入时带一个固定批次号。之后排查数据对不上时按sync_version一查哪些批次刷过、哪些是脏记录一目了然。这个习惯帮我少翻了不知道多少次车。如果你正在评估是否引入这一类的数据同步中间件别只看它支持的连接器列表重点看断点续传、幂等写入和结构变更应对这三项能力它们才是生产环境真正决定成败的地方。希望帮到你。本文还有配套的精品资源点击获取