增量同步

增量同步是一种只同步“发生变化的数据”(新增和更新),而不搬运全部数据的机制。相比每次全量覆盖,它能大幅减少数据传输量,适合数据量大或对实时性有要求的场景。

核心原理与局限

它只抓取自上次同步以来产生变动的数据。不过,单纯的增量同步通常无法捕获删除操作(因为被删的数据在源端已不存在),删除往往需要依赖全量同步或特殊的日志解析来补充。

常见实现方式

根据源系统的能力和架构,主要有三种主流做法:

1. 基于时间戳或增量字段(最简单)
在源表中找一个能标识更新时间的字段(如 update_time、gmt_modify)。同步任务每次只查询该字段大于“上次同步时间点”的数据。

  • 优点:实现简单,无需改源库结构。
  • 缺点:无法捕获物理删除;若源表没有更新时间字段则无法使用;高并发下可能存在微小的时间误差导致漏数。

2. 基于触发器(对源库有侵入)
在源库建立数据库触发器,当发生增删改时,自动将变更写入一张专门的日志表,同步工具再去读日志表。

  • 优点:能捕获删除和更新。
  • 缺点:对源库性能有影响,且需要修改源库 schema,很多业务场景不允许这样做。

3. 基于日志解析(CDC,最可靠)
这是目前生产环境主流的高阶方案。通过读取数据库自身的日志(如 MySQL 的 binlog、Oracle 的 redo log),解析出所有的增删改操作并应用到目标端。

  • 优点:对源库几乎无侵入,能完整捕获增删改,且支持断点续传(记录位点,重启后接着读)。
  • 缺点:架构复杂,通常需要借助 Debezium、Canal 等专业工具或云服务。

关键实现细节:全量与增量的衔接

在初次搭建时,通常需要先做一次全量同步,然后无缝切换到增量。
严谨的做法是:在全量快照开始的那一刻,记录下日志的位点或时间戳;等全量搬完后,从那个记录点开始补增量数据,避免中间出现数据断层。

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>incremental-sync</artifactId>
    <version>1.0.0</version>

    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <!-- SQLite JDBC -->
        <dependency>
            <groupId>org.xerial</groupId>
            <artifactId>sqlite-jdbc</artifactId>
            <version>3.45.3.0</version>
        </dependency>
        <!-- Jackson 用于位点 JSON 读写 -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.17.1</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <!-- 打成可执行 jar,方便直接 java -jar 运行 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>3.5.1</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals><goal>shade</goal></goals>
                        <configuration>
                            <transformers>
                                <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                    <mainClass>com.example.sync.IncrementalSyncDemo</mainClass>
                                </transformer>
                            </transformers>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

IncrementalSyncDemo.java

package com.example.sync;

import com.fasterxml.jackson.databind.ObjectMapper;

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.sql.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class IncrementalSyncDemo {
private static final String SOURCE_DB = "source.db";
private static final String TARGET_DB = "target.db";
private static final Path CHECKPOINT_FILE = Paths.get("checkpoint.json");
private static final String INITIAL_TS = "1970-01-01 00:00:00";

private static final DateTimeFormatter FMT =
        DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private static final ObjectMapper MAPPER = new ObjectMapper();

// ---------- 初始化 ----------

private static void initSource() throws SQLException {
    try (Connection conn = DriverManager.getConnection("jdbc:sqlite:" + SOURCE_DB);
         Statement st = conn.createStatement()) {
        st.execute("""
            CREATE TABLE IF NOT EXISTS source_users (
                id INTEGER PRIMARY KEY,
                name TEXT,
                update_time TEXT
            )
        """);
    }
}

private static void initTarget() throws SQLException {
    try (Connection conn = DriverManager.getConnection("jdbc:sqlite:" + TARGET_DB);
         Statement st = conn.createStatement()) {
        st.execute("""
            CREATE TABLE IF NOT EXISTS target_users (
                id INTEGER PRIMARY KEY,
                name TEXT,
                update_time TEXT
            )
        """);
    }
}

// ---------- 位点读写 ----------

@SuppressWarnings("unchecked")
private static String loadCheckpoint() {
    try {
        if (Files.exists(CHECKPOINT_FILE)) {
            Map<String, String> data = MAPPER.readValue(CHECKPOINT_FILE.toFile(), Map.class);
            String ts = data.get("last_update_time");
            if (ts != null && !ts.isEmpty()) return ts;
        }
    } catch (Exception e) {
        System.err.println("[checkpoint] 读取失败,使用初始位点: " + e.getMessage());
    }
    return INITIAL_TS;
}

private static void saveCheckpoint(String ts) {
    try {
        Map<String, String> data = new HashMap<>();
        data.put("last_update_time", ts);
        MAPPER.writeValue(CHECKPOINT_FILE.toFile(), data);
    } catch (Exception e) {
        throw new RuntimeException("保存位点失败", e);
    }
}

// ---------- 核心:增量同步 ----------

public static void incrementalSync() throws SQLException {
    String lastTs = loadCheckpoint();
    System.out.println("[sync] 上次同步位点: " + lastTs);

    List<Object[]> rows = new ArrayList<>();

    // 1. 从源库按位点拉增量
    try (Connection src = DriverManager.getConnection("jdbc:sqlite:" + SOURCE_DB);
         PreparedStatement ps = src.prepareStatement(
                 "SELECT id, name, update_time FROM source_users " +
                 "WHERE update_time > ? ORDER BY update_time")) {
        ps.setString(1, lastTs);
        try (ResultSet rs = ps.executeQuery()) {
            while (rs.next()) {
                rows.add(new Object[]{
                        rs.getInt("id"),
                        rs.getString("name"),
                        rs.getString("update_time")
                });
            }
        }
    }

    if (rows.isEmpty()) {
        System.out.println("[sync] 无新增数据");
        return;
    }

    // 2. 写入目标库(UPSERT),同时推进位点
    String maxTs = lastTs;
    try (Connection tgt = DriverManager.getConnection("jdbc:sqlite:" + TARGET_DB);
         PreparedStatement ps = tgt.prepareStatement(
                 "INSERT INTO target_users (id, name, update_time) VALUES (?, ?, ?) " +
                 "ON CONFLICT(id) DO UPDATE SET " +
                 "name = excluded.name, update_time = excluded.update_time")) {

        for (Object[] row : rows) {
            ps.setInt(1, (Integer) row[0]);
            ps.setString(2, (String) row[1]);
            ps.setString(3, (String) row[2]);
            ps.addBatch();

            String ts = (String) row[2];
            if (ts.compareTo(maxTs) > 0) maxTs = ts;
        }
        ps.executeBatch();
    }

    saveCheckpoint(maxTs);
    System.out.println("[sync] 同步 " + rows.size() + " 条,新位点: " + maxTs);
}

// ---------- 模拟源库变化 ----------

private static void simulateSourceChanges() throws SQLException {
    String now = LocalDateTime.now().format(FMT);
    try (Connection conn = DriverManager.getConnection("jdbc:sqlite:" + SOURCE_DB);
         PreparedStatement ps = conn.prepareStatement(
                 "INSERT OR REPLACE INTO source_users (id, name, update_time) VALUES (?, ?, ?)")) {
        ps.setInt(1, 1);
        ps.setString(2, "Alice");
        ps.setString(3, now);
        ps.addBatch();

        ps.setInt(1, 2);
        ps.setString(2, "Bob");
        ps.setString(3, now);
        ps.addBatch();

        ps.executeBatch();
    }
    System.out.println("[source] 写入/更新 2 条,时间: " + now);
}

private static void updateOneInSource() throws SQLException {
    String now = LocalDateTime.now().format(FMT);
    try (Connection conn = DriverManager.getConnection("jdbc:sqlite:" + SOURCE_DB);
         PreparedStatement ps = conn.prepareStatement(
                 "UPDATE source_users SET name = ?, update_time = ? WHERE id = 1")) {
        ps.setString(1, "Alice-Updated");
        ps.setString(2, now);
        ps.executeUpdate();
    }
    System.out.println("[source] 更新 id=1,时间: " + now);
}

private static void printTarget() throws SQLException {
    try (Connection conn = DriverManager.getConnection("jdbc:sqlite:" + TARGET_DB);
         Statement st = conn.createStatement();
         ResultSet rs = st.executeQuery("SELECT id, name, update_time FROM target_users ORDER BY id")) {
        while (rs.next()) {
            System.out.printf("  id=%d, name=%s, update_time=%s%n",
                    rs.getInt("id"), rs.getString("name"), rs.getString("update_time"));
        }
    }
}

// ---------- main ----------

public static void main(String[] args) throws Exception {
    initSource();
    initTarget();

    System.out.println("=== 第一次:模拟源库有数据后同步 ===");
    simulateSourceChanges();
    incrementalSync();

    System.out.println("\n=== 第二次:源库无变化,再同步 ===");
    incrementalSync();

    System.out.println("\n=== 第三次:源库更新一条后再同步 ===");
    // 保证 update_time 比上次大(SQLite 时间字符串按字典序比较)
    Thread.sleep(1100);
    updateOneInSource();
    incrementalSync();

    System.out.println("\n=== 目标库最终数据 ===");
    printTarget();
}
}

文末附加内容
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇