Spring Boot 集成 Debezium 实现 MySQL 指定表数据实时变更监听



摘要

在日常开发中,我们经常需要监听 MySQL 数据库的变更,例如:记录审计日志、同步数据到 ES、实时通知业务模块等。传统方式通常依赖轮询,效率低且容易遗漏数据。本文介绍如何在 Spring Boot 环境下使用 Debezium Embedded 模式 来监听 MySQL 指定数据库与数据表的变化,实现高性能、低延迟的数据捕获(CDC)。

文章将从 依赖配置 → 参数说明 → Java 监听实现 → 变更事件解析 全流程进行讲解,让你快速掌握 Debezium 在本地应用中的使用方法,构建轻量级的数据监听能力。

一、Debezium 介绍

Debezium 是一个开源的 CDC(Change Data Capture)框架,用于实时捕获数据库的变更事件。它通过读取 MySQL Binlog 的方式获取数据表的 insert、update、delete 等操作,并以结构化的事件格式输出。

在典型架构中,Debezium 依赖 Kafka。但本文使用的是 Embedded Mode(嵌入式模式)

・不需要 Kafka

・不需要 Zookeeper

・直接在 Spring Boot 应用内部运行

・更适合轻量级、单体项目、本地服务

二、依赖配置

<properties>
    <debezium.version>3.3.1.Final</debezium.version>
</properties>

<dependencies>
    <!-- debezium connector -->
    <dependency>
        <groupId>io.debezium</groupId>
        <artifactId>debezium-connector-mysql</artifactId>
        <version>${debezium.version}</version>
    </dependency>
    <!-- debezium api -->
    <dependency>
        <groupId>io.debezium</groupId>
        <artifactId>debezium-api</artifactId>
        <version>${debezium.version}</version>
    </dependency>
    <!-- debezium embedded -->
    <dependency>
        <groupId>io.debezium</groupId>
        <artifactId>debezium-embedded</artifactId>
        <version>${debezium.version}</version>
</dependency>



三、核心参数说明

下面对代码中的几个关键配置项做详细说明:

参数说明
database.include.list指定监听的数据库
table.include.list指定监听的数据表
snapshot.mode=no_data不执行快照,只监听后续变更
offset.storage.file.filename偏移量记录文件
schema.history.internal.file.filename表结构变更历史记录文件
database.server.id该监听器在 MySQL 中的伪服务器 ID,必须唯一
topic.prefix用于事件分类的前缀(本地模式也需要)

💡 注意:必须启用 MySQL Binlog 且为 ROW 格式

[mysqld]
log_bin=mysql-bin
binlog_format=ROW
server_id=223344



四、代码实现:监听 MySQL 指定表变更

import com.fasterxml.jackson.databind.JsonNode;
import io.debezium.connector.mysql.MySqlConnector;
import io.debezium.engine.ChangeEvent;
import io.debezium.engine.DebeziumEngine;
import io.debezium.engine.format.Json;
import io.debezium.storage.file.history.FileSchemaHistory;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.apache.kafka.connect.storage.FileOffsetBackingStore;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.nio.file.Paths;
import java.util.List;
import java.util.Objects;
import java.util.Properties;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;

/**
 * @author TheTsing
 * @since 2025-11-12 15:30
 */
@Slf4j
@Component
public class StuLeaveTableListener {

    @PostConstruct
    public void start() {
        final Properties props = new Properties();
        // offset config
        props.setProperty("offset.storage", FileOffsetBackingStore.class.getName());
        props.setProperty("offset.storage.file.filename", Paths.get(System.getProperty("user.dir"), "logs", "mysql_test_offsets.dat").toString());
        props.setProperty("offset.flush.interval.ms", "60000");
        // connector config
        props.setProperty("name", "jwxt-connector");
        props.setProperty("connector.class", MySqlConnector.class.getName());
        props.setProperty("database.connectionTimeZone", "GMT+8");
        props.setProperty("database.hostname", "127.0.0.1");
        props.setProperty("database.port", "3306");
        props.setProperty("database.user", "root");
        props.setProperty("database.password", "root");
        props.setProperty("database.include.list", "test");
        props.setProperty("table.include.list", "test.user");
        props.setProperty("snapshot.mode", "no_data");
        props.setProperty("database.server.id", "3456");
        props.setProperty("topic.prefix", "observer3456");
        props.setProperty("include.schema.changes", "false");
        props.setProperty("schema.history.internal.store.only.captured.databases.ddl", "true");
        props.setProperty("schema.history.internal.store.only.captured.tables.ddl", "true");
        props.setProperty("schema.history.internal", FileSchemaHistory.class.getName());
        props.setProperty("schema.history.internal.file.filename", Paths.get(System.getProperty("user.dir"), "logs", "mysql_test_schemahistory.dat").toString());
        ExecutorService executor = Executors.newSingleThreadExecutor();
        DebeziumEngine<ChangeEvent<String, String>> engine = DebeziumEngine.create(Json.class).using(props).notifying(this::handlePayload).build();
        executor.submit(engine);
        registerShutdownHook(engine, executor);
    }

    private void registerShutdownHook(DebeziumEngine<ChangeEvent<String, String>> engine, ExecutorService executor) {
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            try {
                engine.close();
                executor.shutdown();
                while (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            } catch (IOException ignored) {
            }
        }));
    }

    private void handlePayload(List<ChangeEvent<String, String>> records, DebeziumEngine.RecordCommitter<ChangeEvent<String, String>> committer) {
        Consumer<ChangeEvent<String, String>> consumer = record -> {
            try {
                if (record.value() == null) {
                    // Tombstone event, skip
                    return;
                }
                // ObjectMapper 的 readTree 方法
                JsonNode payload = JsonUtils.getJsonNode(record.value()).get("payload");
                if (Objects.isNull(payload)) return;
                switch (payload.get("op").asText()) {
                    case "c" -> log.info("Insert data: {}", payload.get("after").toString());
                    case "u" ->
                            log.info("Update data:\nbefore = {}\nafter = {}", payload.get("before").toString(), payload.get("after").toString());
                    case "d" -> log.info("Delete data: {}", payload.get("before").toString());
                    default -> log.warn("Unknown event type: {}", payload.get("op").asText());
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        };
        try {
            for (ChangeEvent<String, String> record : records) {
                consumer.accept(record);
                committer.markProcessed(record);
            }
            committer.markBatchFinished();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}



五、变更事件结构解析

Debezium 输出的 JSON 数据格式如下(示例):

{
  "payload": {
    "before": null,
    "after": {
      "id": 1,
      "name": "Tom"
    },
    "op": "c",
    "ts_ms": 1700000000000
  }
}
字段含义
before更新前数据,insert 时为 null
after更新后的数据,delete 时为 null
op动作类型:c=insert, u=update, d=delete
ts_ms时间戳

总结

本文展示了如何通过 Debezium Embedded + Spring Boot 的方式,实现轻量级、高性能的 MySQL 表级变更监听功能。适合使用在:

・实时日志审计

・数据同步到缓存或 ES

・数据库变更通知

・轻量级的 CDC 服务

如果这篇博客对你有帮助的话,记得给我点个赞,你的鼓励是我最大的动力 (。◕‿◕。)

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值