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 服务
如果这篇博客对你有帮助的话,记得给我点个赞,你的鼓励是我最大的动力 (。◕‿◕。)
1585

被折叠的 条评论
为什么被折叠?



