CDC 之 MySQL binlog订阅 二

一、订阅逻辑实现

mysql-binlog-connector-java 使用详解

mysql-binlog-connector-java 是一个用于读取和解析 MySQL 二进制日志(binlog)的 Java 库,它允许开发者实时监听数据库的变更事件,如数据的插入、更新和删除等操作。以下是该库的详细使用指南:

一、核心功能

  1. 实时监听 MySQL 复制流:通过监听 binlog,开发者可以实时获取数据库的变更信息。
  2. 支持断线重连:在网络不稳定或程序异常时,能够自动重新连接 MySQL 服务器。
  3. GTID 处理:支持全局事务标识符(GTID),便于在分布式环境中跟踪事务。
  4. 安全的 TLS 通信:支持通过 TLS 加密通信,确保数据传输的安全性。
  5. JMX 监控:提供 JMX 接口,便于监控和管理 binlog 监听过程。

二、使用条件

  1. MySQL 配置

    • 确保 MySQL 服务器已开启 binlog 功能。可以通过执行 SHOW VARIABLES LIKE 'log_bin'; 命令检查。
    • 配置 MySQL 用户具有 REPLICATION SLAVE 权限,以便能够读取 binlog。
  2. Java 环境

    • 确保项目中已引入 mysql-binlog-connector-java 库。可以通过 Maven 依赖管理工具添加依赖:
      <dependency>
          <groupId>com.zendesk</groupId>
          <artifactId>mysql-binlog-connector-java</artifactId>
          <version>0.29.2</version> <!-- 使用最新版本 -->
      </dependency>
      

三、使用步骤

  1. 创建 BinaryLogClient 客户端对象

    • 初始化时需要传入 MySQL 服务器的连接信息,包括主机名、端口、用户名和密码。
    • 示例代码:
      BinaryLogClient client = new BinaryLogClient("hostname", 3306, "username", "password");
      
  2. 注册事件监听器

    • 通过实现 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());
              }
          }
      });
      
  3. 设置 binlog 文件名和位置(可选)

    • 如果需要从特定的 binlog 文件和位置开始监听,可以设置 binlogFilenamebinlogPosition 属性。
    • 示例代码:
      client.setBinlogFilename("mysql-bin.000001");
      client.setBinlogPosition(11); // 从第11个字节开始监听
      
  4. 启动监听

    • 调用 client.connect() 方法启动监听过程。
    • 示例代码:
      client.connect();
      
  5. 处理断开连接和重连

    • mysql-binlog-connector-java 支持断线重连功能。当连接断开时,库会自动尝试重新连接。
    • 开发者也可以实现自定义的重连逻辑,通过监听 onConnectFailed 事件来处理连接失败的情况。

四、高级功能

  1. 断点续传

    • 当程序宕机后重启时,可以从上次宕机的位置继续监听 binlog,而不是从最新的位置开始。
    • 实现思路:将上次监听的 binlog 文件名和位置保存到持久化存储中(如数据库或文件),重启时读取这些信息并设置到 BinaryLogClient 对象中。
  2. 自定义事件反序列化

    • 通过设置不同的 EventDeserializer,可以控制事件的反序列化方式,以满足不同的业务需求。
  3. 监控和管理

    • 利用 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());
        }
    }
}

最佳实践建议

  1. 记录位置的时机

    • 不要在每次事件处理后都立即记录位置,这会影响性能
    • 建议批量记录或定期记录(如每处理N个事件或每T秒记录一次)
    • 在程序正常退出时记录位置
  2. 错误处理

    • 记录位置失败不应导致程序中断
    • 考虑实现重试机制或备用存储方案
  3. GTID vs 文件位置

    • 如果MySQL启用了GTID,优先使用GTID方式
    • GTID方式在主从切换等场景下更可靠
  4. 初始化检查

    • 程序启动时验证记录的位置是否有效(如binlog文件是否存在)
    • 如果位置无效,可以从当前最新位置开始或报错退出
  5. 多线程环境

    • 如果使用多线程处理事件,确保位置记录是线程安全的
    • 考虑使用同步机制或集中记录位置

通过以上方法,你可以实现可靠的binlog消费位置记录,确保程序重启后能够从正确的位置继续处理。

posted @ 2025-12-02 16:13  蓝迷梦  阅读(1)  评论(0)    收藏  举报