CDC 之 MySQL binlog订阅 二
一、订阅逻辑实现
mysql-binlog-connector-java 使用详解
mysql-binlog-connector-java 是一个用于读取和解析 MySQL 二进制日志(binlog)的 Java 库,它允许开发者实时监听数据库的变更事件,如数据的插入、更新和删除等操作。以下是该库的详细使用指南:
一、核心功能
- 实时监听 MySQL 复制流:通过监听 binlog,开发者可以实时获取数据库的变更信息。
- 支持断线重连:在网络不稳定或程序异常时,能够自动重新连接 MySQL 服务器。
- GTID 处理:支持全局事务标识符(GTID),便于在分布式环境中跟踪事务。
- 安全的 TLS 通信:支持通过 TLS 加密通信,确保数据传输的安全性。
- JMX 监控:提供 JMX 接口,便于监控和管理 binlog 监听过程。
二、使用条件
-
MySQL 配置:
- 确保 MySQL 服务器已开启 binlog 功能。可以通过执行
SHOW VARIABLES LIKE 'log_bin';命令检查。 - 配置 MySQL 用户具有
REPLICATION SLAVE权限,以便能够读取 binlog。
- 确保 MySQL 服务器已开启 binlog 功能。可以通过执行
-
Java 环境:
- 确保项目中已引入
mysql-binlog-connector-java库。可以通过 Maven 依赖管理工具添加依赖:<dependency> <groupId>com.zendesk</groupId> <artifactId>mysql-binlog-connector-java</artifactId> <version>0.29.2</version> <!-- 使用最新版本 --> </dependency>
- 确保项目中已引入
三、使用步骤
-
创建 BinaryLogClient 客户端对象:
- 初始化时需要传入 MySQL 服务器的连接信息,包括主机名、端口、用户名和密码。
- 示例代码:
BinaryLogClient client = new BinaryLogClient("hostname", 3306, "username", "password");
-
注册事件监听器:
- 通过实现
BinaryLogClient.EventListener接口或继承AbstractBinaryLogEventListener类,来定义对 binlog 事件的监听和处理逻辑。 - 示例代码:
client.registerEventListener(new BinaryLogClient.EventListener() { @Override public void onEvent(Event event) { // 处理事件 EventData data = event.getData(); if (data instanceof WriteRowsEventData) { // 处理插入事件 WriteRowsEventData writeData = (WriteRowsEventData) data; System.out.println("Insert event: " + writeData.getRows()); } else if (data instanceof UpdateRowsEventData) { // 处理更新事件 UpdateRowsEventData updateData = (UpdateRowsEventData) data; System.out.println("Update event: " + updateData.getRows()); } else if (data instanceof DeleteRowsEventData) { // 处理删除事件 DeleteRowsEventData deleteData = (DeleteRowsEventData) data; System.out.println("Delete event: " + deleteData.getRows()); } } });
- 通过实现
-
设置 binlog 文件名和位置(可选):
- 如果需要从特定的 binlog 文件和位置开始监听,可以设置
binlogFilename和binlogPosition属性。 - 示例代码:
client.setBinlogFilename("mysql-bin.000001"); client.setBinlogPosition(11); // 从第11个字节开始监听
- 如果需要从特定的 binlog 文件和位置开始监听,可以设置
-
启动监听:
- 调用
client.connect()方法启动监听过程。 - 示例代码:
client.connect();
- 调用
-
处理断开连接和重连:
mysql-binlog-connector-java支持断线重连功能。当连接断开时,库会自动尝试重新连接。- 开发者也可以实现自定义的重连逻辑,通过监听
onConnectFailed事件来处理连接失败的情况。
四、高级功能
-
断点续传:
- 当程序宕机后重启时,可以从上次宕机的位置继续监听 binlog,而不是从最新的位置开始。
- 实现思路:将上次监听的 binlog 文件名和位置保存到持久化存储中(如数据库或文件),重启时读取这些信息并设置到
BinaryLogClient对象中。
-
自定义事件反序列化:
- 通过设置不同的
EventDeserializer,可以控制事件的反序列化方式,以满足不同的业务需求。
- 通过设置不同的
-
监控和管理:
- 利用 JMX 接口,可以监控 binlog 监听过程的状态和性能指标,如连接状态、事件处理速率等。
五、示例代码(完整流程)
import com.github.shyiko.mysql.binlog.BinaryLogClient;
import com.github.shyiko.mysql.binlog.event.*;
import com.github.shyiko.mysql.binlog.event.deserialization.EventDeserializer;
public class BinlogListenerExample {
public static void main(String[] args) {
BinaryLogClient client = new BinaryLogClient("hostname", 3306, "username", "password");
// 可选:设置自定义的EventDeserializer
EventDeserializer eventDeserializer = new EventDeserializer();
// eventDeserializer.setCompatibilityMode(...); // 根据需要设置兼容性模式
client.setEventDeserializer(eventDeserializer);
// 注册事件监听器
client.registerEventListener(new BinaryLogClient.EventListener() {
@Override
public void onEvent(Event event) {
EventHeader header = event.getHeader();
EventType eventType = header.getEventType();
switch (eventType) {
case TABLE_MAP:
// 处理表映射事件(可选)
break;
case WRITE_ROWS:
// 处理插入事件
WriteRowsEventData writeData = (WriteRowsEventData) event.getData();
System.out.println("Insert event: " + writeData.getRows());
break;
case UPDATE_ROWS:
// 处理更新事件
UpdateRowsEventData updateData = (UpdateRowsEventData) event.getData();
System.out.println("Update event: " + updateData.getRows());
break;
case DELETE_ROWS:
// 处理删除事件
DeleteRowsEventData deleteData = (DeleteRowsEventData) event.getData();
System.out.println("Delete event: " + deleteData.getRows());
break;
default:
// 处理其他类型的事件(如DDL事件)
break;
}
}
});
try {
// 启动监听
client.connect();
} catch (Exception e) {
e.printStackTrace();
}
}
}
二、订阅位置记录
mysql-binlog-connector-java 记录消费位置实现示例
在使用 mysql-binlog-connector-java 监听 MySQL binlog 时,记录消费位置(即记录已经处理到的 binlog 文件和位置)非常重要,这可以实现断点续传功能,避免程序重启后从头开始消费。
实现方案
以下是几种常见的记录消费位置的实现方式:
1. 基于文件存储的消费位置记录
import com.github.shyiko.mysql.binlog.BinaryLogClient;
import com.github.shyiko.mysql.binlog.event.*;
import java.io.*;
public class FilePositionStorageExample {
private static final String POSITION_FILE = "binlog_position.txt";
public static void main(String[] args) {
BinaryLogClient client = new BinaryLogClient("hostname", 3306, "username", "password");
// 从文件读取上次记录的位置
loadPosition(client);
client.registerEventListener(event -> {
EventHeader header = event.getHeader();
// 处理事件逻辑...
System.out.println("Event: " + header.getEventType());
// 每次处理完事件后记录位置
savePosition(header);
});
// 注册生命周期监听器,在连接断开时保存位置
client.registerLifecycleListener(new BinaryLogClient.LifecycleListener() {
@Override
public void onDisconnect(BinaryLogClient client) {
// 连接断开时保存当前位置
savePosition(client.getBinlogFilename(), client.getBinlogPosition());
}
});
try {
client.connect();
} catch (Exception e) {
e.printStackTrace();
}
}
private static void loadPosition(BinaryLogClient client) {
try (BufferedReader reader = new BufferedReader(new FileReader(POSITION_FILE))) {
String filename = reader.readLine();
long position = Long.parseLong(reader.readLine());
if (filename != null && position > 0) {
client.setBinlogFilename(filename);
client.setBinlogPosition(position);
System.out.println("Resuming from position: " + filename + "/" + position);
}
} catch (FileNotFoundException e) {
System.out.println("No position file found, starting from beginning");
} catch (Exception e) {
System.err.println("Error reading position file: " + e.getMessage());
}
}
private static void savePosition(EventHeader header) {
savePosition(header.getNextBinlogFilename(), header.getNextBinlogPosition());
}
private static void savePosition(String filename, long position) {
try (BufferedWriter writer = new BufferedWriter(new FileWriter(POSITION_FILE))) {
writer.write(filename + "\n");
writer.write(String.valueOf(position));
System.out.println("Saved position: " + filename + "/" + position);
} catch (IOException e) {
System.err.println("Error saving position: " + e.getMessage());
}
}
}
2. 基于数据库存储的消费位置记录
import com.github.shyiko.mysql.binlog.BinaryLogClient;
import com.github.shyiko.mysql.binlog.event.*;
import java.sql.*;
public class DatabasePositionStorageExample {
private static final String DB_URL = "jdbc:mysql://localhost:3306/metadata_db";
private static final String DB_USER = "meta_user";
private static final String DB_PASS = "password";
public static void main(String[] args) {
BinaryLogClient client = new BinaryLogClient("hostname", 3306, "username", "password");
// 从数据库读取上次记录的位置
loadPositionFromDB(client);
client.registerEventListener(event -> {
EventHeader header = event.getHeader();
// 处理事件逻辑...
System.out.println("Event: " + header.getEventType());
// 每次处理完事件后记录位置
savePositionToDB(header);
});
client.registerLifecycleListener(new BinaryLogClient.LifecycleListener() {
@Override
public void onDisconnect(BinaryLogClient client) {
savePositionToDB(client.getBinlogFilename(), client.getBinlogPosition());
}
});
try {
client.connect();
} catch (Exception e) {
e.printStackTrace();
}
}
private static void loadPositionFromDB(BinaryLogClient client) {
try (Connection conn = DriverManager.getConnection(DB_URL, DB_USER, DB_PASS);
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT binlog_file, binlog_position FROM binlog_position WHERE id = 1")) {
if (rs.next()) {
String filename = rs.getString("binlog_file");
long position = rs.getLong("binlog_position");
if (filename != null && position > 0) {
client.setBinlogFilename(filename);
client.setBinlogPosition(position);
System.out.println("Resuming from position: " + filename + "/" + position);
}
}
} catch (SQLException e) {
System.err.println("Error reading position from DB: " + e.getMessage());
}
}
private static void savePositionToDB(EventHeader header) {
savePositionToDB(header.getNextBinlogFilename(), header.getNextBinlogPosition());
}
private static void savePositionToDB(String filename, long position) {
try (Connection conn = DriverManager.getConnection(DB_URL, DB_USER, DB_PASS);
PreparedStatement pstmt = conn.prepareStatement(
"INSERT INTO binlog_position (id, binlog_file, binlog_position) " +
"VALUES (1, ?, ?) ON DUPLICATE KEY UPDATE binlog_file = ?, binlog_position = ?")) {
pstmt.setString(1, filename);
pstmt.setLong(2, position);
pstmt.setString(3, filename);
pstmt.setLong(4, position);
pstmt.executeUpdate();
System.out.println("Saved position to DB: " + filename + "/" + position);
} catch (SQLException e) {
System.err.println("Error saving position to DB: " + e.getMessage());
}
}
}
3. 基于GTID的消费位置记录(推荐)
如果MySQL启用了GTID模式,使用GTID记录消费位置是更可靠的方式:
import com.github.shyiko.mysql.binlog.BinaryLogClient;
import com.github.shyiko.mysql.binlog.event.*;
import java.io.*;
public class GtidPositionStorageExample {
private static final String GTID_FILE = "binlog_gtid.txt";
public static void main(String[] args) {
BinaryLogClient client = new BinaryLogClient("hostname", 3306, "username", "password");
// 启用GTID模式
client.setGtidMode(true);
// 从文件读取上次记录的GTID
String lastGtid = loadGtid();
if (lastGtid != null) {
client.setGtidSet(lastGtid);
System.out.println("Resuming from GTID: " + lastGtid);
}
client.registerEventListener(event -> {
EventHeader header = event.getHeader();
// 处理事件逻辑...
System.out.println("Event: " + header.getEventType());
// 如果是GTID事件,记录GTID
if (header.getEventType() == EventType.GTID) {
GtidLogEvent gtidEvent = (GtidLogEvent) event.getData();
String currentGtid = gtidEvent.getGTID().toString();
saveGtid(currentGtid);
}
});
try {
client.connect();
} catch (Exception e) {
e.printStackTrace();
}
}
private static String loadGtid() {
try (BufferedReader reader = new BufferedReader(new FileReader(GTID_FILE))) {
return reader.readLine();
} catch (FileNotFoundException e) {
System.out.println("No GTID file found, starting from beginning");
return null;
} catch (IOException e) {
System.err.println("Error reading GTID file: " + e.getMessage());
return null;
}
}
private static void saveGtid(String gtid) {
try (BufferedWriter writer = new BufferedWriter(new FileWriter(GTID_FILE))) {
writer.write(gtid);
System.out.println("Saved GTID: " + gtid);
} catch (IOException e) {
System.err.println("Error saving GTID: " + e.getMessage());
}
}
}
最佳实践建议
-
记录位置的时机:
- 不要在每次事件处理后都立即记录位置,这会影响性能
- 建议批量记录或定期记录(如每处理N个事件或每T秒记录一次)
- 在程序正常退出时记录位置
-
错误处理:
- 记录位置失败不应导致程序中断
- 考虑实现重试机制或备用存储方案
-
GTID vs 文件位置:
- 如果MySQL启用了GTID,优先使用GTID方式
- GTID方式在主从切换等场景下更可靠
-
初始化检查:
- 程序启动时验证记录的位置是否有效(如binlog文件是否存在)
- 如果位置无效,可以从当前最新位置开始或报错退出
-
多线程环境:
- 如果使用多线程处理事件,确保位置记录是线程安全的
- 考虑使用同步机制或集中记录位置
通过以上方法,你可以实现可靠的binlog消费位置记录,确保程序重启后能够从正确的位置继续处理。
本文来自博客园,作者:蓝迷梦,转载请注明原文链接:https://www.cnblogs.com/hewei-blogs/articles/19298215

浙公网安备 33010602011771号