# database-sync **Repository Path**: iQuake/database-sync ## Basic Information - **Project Name**: database-sync - **Description**: 数据库表同步工具、数据仓库工具、数据集市工具,数据抽取。传入一定的参数,即可在相同或不同的数据库间进行表的同步,包括表结构的同步及数据的同步。作业由调试工具进行调度,本项目旨在提供支持各种数据库间表同步的能力 - **Primary Language**: Java - **License**: AGPL-3.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 27 - **Created**: 2024-03-21 - **Last Updated**: 2026-09-29 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # [database-sync](https://gitee.com/somenzz/database-sync) 这是为数据开发人员使用的辅助工具,用于数据库之间的表同步,说同步并不严谨,因为不是实时更新的,更确切的说法是复制,可以方便的从一个数据库复制表到另一个数据库,以下遇到同步的词请理解为复制。 ## 介绍 需求背景: 有很多业务系统,他们的数据库是相互独立的,需要把这些数据归集在一个数据库中(数据仓库),以便做数据统计和分析。希望能有这样的工具,输入数据库名,表名就可以将数据从源数据库拷贝到目标数据库中。具体需求如下:  - 能自动同步表结构,如:源表扩字段,目标表自动扩字段。 - 支持增量或全量同步数据,可以仅同步某个日期之后的数据。 - 支持指定字段同步,只同步关心的那些字段。 - 支持主流的关系型数据库: mysql、db2、postgresql、oracle、sqlserver、clickhouse、doris - 源表和目标表表名可以不同,字段名也可以不同(已存在目标表的情况下) - 支持同一主键多版本的去重:源表按版本列保留最新数据,避免目标合并树留下旧版本 因为自己要用,我就自己写了一个,顺便熟悉下 java 开发(之前一直用 Python),本程序的最大用处就是构建集市或数仓所需要的基础层数据源,欢迎感兴趣的朋友一起加入。 ## 程序的使用方法 ### Docker 方式: 这里用到三个容器: - app 也就是主程序本身,app 容器使用的程序文件就是 release 目录下的文件,已经做了绑定。 - mysql 测试用的数据库,已提前放好了 7000 条测试数据。 - postgres 测试用的数据库,没有数据。 先部署,执行 `docker-compose up -d` 就自动完成了部署: ```sh $ git clone https://github.com/somenzz/database-sync.git $ cd database-sync $ docker-compose up -d Creating database-sync_postgres_1 ... done Creating database-sync_app_1 ... done Creating database-sync_mysql_1 ... done ``` 这样三个容器就启动了,使用 `docker ps -a |grep database-sync` 可以查看到三个正在运行的容器: ![](images/1.jpg) 现在直接使用 `docker exec -i database-sync_app_1 java -jar database-sync-1.3.jar` 来执行程序: ![](images/2.jpg) mysql 容器已有测试数据,`release/config/config.json` 已经配置好了数据库的连接,因此可以直接试用,以下演示的是从 mysql 复制表和数据到 postgres: #### 1. 全量复制,自动建表: ```sh docker exec -i database-sync_app_1 java -jar database-sync-1.3.jar mysql_test testdb somenzz_users postgres_test public users --sync-ddl ``` ![](images/3.jpg) 如果你不想每次都敲 `docker exec -i database-sync_app_1` ,可以进入容器内部执行: ```sh (py38env) ➜ database-sync git:(master) ✗ docker exec -it database-sync_app_1 /bin/bash root@063b1dc76fe1:/app# ls config database-sync-1.3.jar lib logs root@063b1dc76fe1:/app# java -jar database-sync-1.3.jar mysql_test testdb somenzz_users postgres_test public users -sd ``` #### 2. 增量复制: ```sh root@063b1dc76fe1:/app# java -jar database-sync-1.3.jar mysql_test testdb somenzz_users postgres_test public zz_users "create_at >= '2018-01-09'" ``` ![](images/4.jpg) #### 3. 指定字段: ```sh root@063b1dc76fe1:/app# java -jar database-sync-1.3.jar mysql_test testdb somenzz_users postgres_test public zz_users -ff="user_id,name,age" -tf="user_id,name,age" "create_at >= '2018-01-09'" ``` ![](images/5.jpg) ### 普通方式 程序运行前确保已安装 java 1.8 或后续版本,已经安装 maven,然后 clone 源码,打包: ```sh git clone https://gitee.com/somenzz/database-sync.git cd database-sync mvn package ``` 此时你会看到 target 目录,将 target 下的 lib 目录 和 database-sync-1.3.jar 复制出来,放在同一目录下,然后再创建一个 config 目录,在 config 下新建一个 config.json 文件写入配置信息,然后将这个目录压缩,就可以传到服务器运行了,请注意先充分测试,jdk 要求 1.8+ ```sh [aaron@hdp002 /home/aaron/App/Java/database-sync]$ ls -ltr total 48 drwxr-xr-x 2 aaron aaron 4096 Apr 23 2020 lib -rwxrw-r-- 1 aaron aaron 157 Jun 23 2020 run.sh drwxrwxr-x 2 aaron aaron 4096 Jul 3 2020 logs -rw-rw-r-- 1 aaron aaron 24773 Mar 16 2021 database-sync-1.3.jar drwxr-xr-x 7 aaron aaron 4096 Aug 3 2020 jdk1.8.0_231 drwxrwxr-x 2 aaron aaron 4096 Feb 19 17:07 config ``` 你也可以直接下载我打包好的使用。 程序名称叫 database-sync,运行方式是这样的: 不带任何参数、参数不足,或传入 `-h` / `--help` 时,程序会打印以下帮助信息: ```sh (py38env) ➜ target git:(master) ✗ java -jar database-sync-1.3.jar -h Usage: java -jar database-sync-1.3.jar [options] [whereClause] Options: -v, --version Print version then exit -h, --help Print help info then exit -sd, --sync-ddl Automatically synchronize table schema -nf, --no-feature Disable database-specific optimizations (e.g., PostgreSQL COPY) -nt, --no-truncate Do not truncate target table before sync (append mode) -ff=col1,col2,... Source table fields to sync -tf=col1,col2,... Target table fields to sync --latest-by=col When source keeps multiple versions per key (e.g. ClickHouse ReplacingMergeTree), sync only the row with max(col) per key Examples: java -jar database-sync-1.3.jar mysql_test testdb users postgres_test public users -sd java -jar database-sync-1.3.jar mysql_test testdb users postgres_test public users "created_at > '2024-01-01'" java -jar database-sync-1.3.jar mysql_test testdb users postgres_test public users -ff=id,name -tf=user_id,user_name ``` 帮助说明: [] 中括号里的内容表示选填,例如 [options] 表示 options 下的参数不是必须的。 1、其中 options 参数解释如下: - `-h` 或者 `--help` : 打印帮助信息后退出。 - `-v` 或者 `--version` : 打印版本号后退出,版本号取自 jar 清单。 - `--sync-ddl` 或者 `-sd` : 加入该参数会自动同步表结构。 - `--no-feature` 或者 `-nf` : 不使用数据库特有优化,例如 PostgreSQL 的 COPY FROM STDIN。 - `--no-truncate` 或者 `-nt` : 追加模式,全量同步时不先清空目标表。注意:只要带了 where 条件,程序一律按 where 条件执行 `delete from ... where ...`,`-nt` 不会跳过这一步。 - `-ff=col1,col2,...` : 指定原表的字段序列,注意 = 前后不能有空格。 - `-tf=col1,col2,...` : 指定目标表的字段序列,注意 = 前后不能有空格。 - `--latest-by=col` : 同一主键存在多个版本时,只同步该列取值最大的那一行,详见下文「同主键多版本的去重」。 2、whereClause 表示 where 条件,用于增量更新,程序再插入数据前先按照 where 条件进行清理数据,然后按照 where 条件从原表进行读取数据。 whereClause 最好使用双引号包起来,表示一个完整的参数。如:"jyrq='2020-12-31'" {} 大括号里的内容表示必填。 `fromDb` 是指配置在 config.json 的数据库信息的键,假如有以下配置文件: ```json { "postgres":{ "type":"postgres", "driver":"org.postgresql.Driver", "url":"jdbc:postgresql://localhost:5432/apidb", "user": "postgres", "password":"aaron", "encoding": "utf-8" }, "aarondb":{ "type":"mysql", "driver":"com.mysql.cj.jdbc.Driver", "url":"jdbc:mysql://localhost:3306/aarondb?useSSL=false&characterEncoding=utf8&serverTimezone=UTC", "user": "aaron", "password":"aaron" } } ``` fromDb、toDb 可以是 aarondb 或者 postgres。 - `fromSchema` 读取数据的表的模式名,可以填写 "". - `fromTable` 读取数据的表明,必须提供。 - `toSchema` 写入数据表的模式名,可以填写 "",可以和 fromSchema 不同. - `toTable` 写入数据表的表名,必须提供,当写入表不存在时,自动按读取表的表结构创建,可以和 fromTable 不同。 **全量、增量、指定字段的使用样例请参考 Docker 方式。** ## 同主键多版本的去重(--latest-by) ClickHouse 的 `ReplacingMergeTree` 允许同一主键存在多个版本,合并(merge)时**最后插入的行胜出**——注意是插入顺序,不是版本新旧。如果源表和目标表都是这样定义的合并树,直接全量复制会出问题: 源表同一主键有 3 个版本,程序把它们全部发给目标表,那么目标端保留哪一版由写入顺序决定,与版本新旧无关。实测某张 984,154 行、968,974 个唯一键的表,全量复制后有 2,639 个键在目标端可见的是**旧版本**。 加上 `--latest-by` 后,程序按源表主键分组、每个主键只发送版本列取值最大的那一行: ```sh java -jar database-sync-1.3.jar --latest-by=UPDATETIME ch160 db_center FIN_IDX_ANA_NEW ch161 db_center FIN_IDX_ANA_NEW ``` - 主键从源表自动获取,无需额外指定:先读 JDBC 元数据,ClickHouse 读不到时回退到 `system.tables` 的 `primary_key`。 - 版本列取值完全相同的多行,按源表出现顺序保留第一行,保证每个主键恰好输出一行。 - 该列不存在或源表识别不到主键时直接报错,不会静默退化成原样复制。 - 只影响发送的数据,不修改任何表结构。 实测效果(上述表):`skipped 15180 rows`、`insert 968974 rows`,恰好等于唯一键数;目标端重复键 0、旧版本行 0。 两点需要注意: 1. **去重在客户端完成**。服务端 `ORDER BY ... LIMIT 1 BY` 或窗口函数会在输出结果前长时间不发数据,ClickHouse JDBC 驱动的响应流在这个间隙后会错位(见下文「ClickHouse 源的落盘回放」),实测全量读取同样失败。 2. **目标表建议带版本列**。`--latest-by` 保证的是「程序发出的数据是最新版」,但如果还有别的程序直接写目标表(增量任务),目标端同一主键仍会同时存在新旧两行,直到 part 合并。此时不带 `FINAL` 的查询会同时读到被取代的旧行。彻底解决需要把目标表建成 `ReplacingMergeTree(版本列)`,或让查询方改用 `FINAL` / `argMax`。 ## ClickHouse 源的落盘回放 ClickHouse JDBC 的 RowBinary 响应流有个缺陷:**读取端如果长时间不消费数据,流的字节位置会错位**,后续解析出乱七八糟的值并抛出类似下面的错误: ``` java.io.IOException: Failed to read boolean value, expect 0 (false) or 1 (true) but we got: 121 ``` 复制程序是边读边写的,写入耗时必然造成读取端停顿,所以宽表(几十万行以上)几乎必然触发。实测停顿超过约 20 秒就会出错,几秒的停顿则视数据量而定,压缩参数、`socket_timeout`、`buffer-rows` 都无法规避。 程序的做法是:**当源库是 ClickHouse 时,先把源数据全部读到临时文件,再从文件回放写入目标库**。读取阶段连续消费、不会停顿,跨库的慢速写入不再影响响应流。临时文件在同步结束后自动删除。 这条保护逻辑同样会在指定了 `--latest-by` 时启用(去重需要两遍扫描)。 需要注意:`-nf` / `--no-feature` 不会关闭它;PostgreSQL 目标端的 COPY 策略目前还没有这层保护,ClickHouse 源同步到 PostgreSQL 时仍可能触发该缺陷。 ## 配置文件说明 配置文件位于 config/config.json,如下所示: ```json { "sjwb":{ "type":"db2", "driver":"com.ibm.db2.jcc.DB2Driver", "url":"jdbc:db2://192.168.1.*:50000/wbsj", "user": "****", "password":"****", "tbspace_ddl": "/*这里可以放置指定表空间的语句*/", "encoding":"utf-8" }, "dw_test":{ "type":"db2", "driver":"com.ibm.db2.jcc.DB2Driver", "url":"jdbc:db2://192.168.169.*:60990/dwdb", "user": "****", "password":"****", "encoding":"gbk" }, "postgres":{ "type":"postgres", "driver":"org.postgresql.Driver", "url":"jdbc:postgresql://10.99.**.**:5432/apidb", "user": "****", "password":"****", "tbspace_ddl": "WITH (compression=no, orientation=orc, version=0.12)\ntablespace hdfs\n", "encoding":"utf-8" }, "aarondb":{ "type":"mysql", "driver":"com.mysql.cj.jdbc.Driver", "url":"jdbc:mysql://localhost:3306/aarondb?useSSL=false&characterEncoding=utf8&serverTimezone=UTC", "user": "****", "password":"****", "encoding":"utf-8" }, "buffer-rows": 100000 } ``` 配置文件说明: `type` 表示数据库类型,均为小写: - mysql - postgres(也可用 postgresql) - db2(也可用 edw) - oracle - sqlserver - clickhouse - doris `tbspace_ddl` 表示自动建表时指定的表空间,该选项不是必需的,可以删除。 `buffer-rows` 表示读取多少行时一块写入目标数据库,根据服务器内存大小自己做调整,100000 行提交一次满足大多数情况了。 `encoding` 用于表结构同步时确定字段长度,比如说源库的字段是 gbk varchar(10),目标库是 utf-8,那么就应该为 varchar(15),这样字段有中文就不会出现截断或插入失败问题,程序这里 2 倍,也就是 varchar(20) ,这样字段长度不会出现小数位。 ## 编写目的 提高数据库间表的复制效率,如果不需要对源表字段进行转换,就丢掉低效的 datastage 和 kettle 吧。