Bootstrap

Debezium-TableConverters

文章目录

概要

主要用于处理MySQL数据库的二进制日志事件,并将其转换为Kafka Connect的源记录

核心流程

相关解释

假设你有一个数据库,里面有一些表。这个数据库有一个功能叫做“二进制日志”(binlog),它可以记录数据库中的所有变更操作(如插入、更新、删除)。

  1. 二进制日志启用前的表:假设你在某一天启用了二进制日志功能。在启用二进制日志之前,数据库中已经有一些表存在了。这些表的架构信息(比如表的列名、数据类型等)没有被记录在二进制日志中。
  2. 启用二进制日志后的新表:启用二进制日志之后,新创建的表的元数据信息会被记录在二进制日志中。
  3. 处理二进制日志:当你的应用程序读取二进制日志时,它需要知道每个表的元数据信息,以便正确解析和处理这些变更操作。对于那些在二进制日志启用前已经存在的表,因为它们的元数据信息没有被记录,所以你的应用程序无法找到这些表的以元数据信息。
  4. 处理缺失的元数据信息:当应用程序发现某个表的元数据信息缺失时,它会记录一条警告日志,提示这个表的事务影响了未记录元数据的行。为了防止数据处理错误,应用程序会忽略后续对该表的所有变更操作。

总结起来,这句话的意思是:如果在处理某个表的变更事件时,发现该表的元数据信息为空,那么这个表可能是在二进制日志功能启用之前创建的,因此缺少必要的元数据信息。为了避免错误,系统会记录一条警告并忽略后续对该表的变更操作。

技术细节

     1 updateTableMetadata

public void updateTableMetadata(Event event, SourceInfo source, Consumer<SourceRecord> recorder) {
        // 解析事件数据,获取表ID和元数据
        TableMapEventData metadata = event.getData();
        long tableNumber = metadata.getTableId();
        logger.debug("Received update table metadata event: {}", event);

        // 检查是否已存在该表的转换器,如果不存在,则创建
        if (!convertersByTableId.containsKey(tableNumber)) {
            // 获取服务器、数据库和表名称,用于构建主题名称
            String serverName = source.serverName();
            String databaseName = metadata.getDatabase();
            String tableName = metadata.getTable();
            String topicName = topicSelector.getTopic(serverName, databaseName, tableName);

            // 构建表标识,并获取当前表架构
            TableId tableId = new TableId(databaseName, null, tableName);
            TableSchema tableSchema = tableSchemaByTableId.get(tableId);
            logger.debug("Registering metadata for table {} with table #{}", tableId, tableNumber);

            // 如果表架构为空,说明该表在二进制日志启用前已创建,需要记录警告
            if (tableSchema == null) {
                if (unknownTableIds.add(tableId)) {
                    logger.warn("Transaction affects rows in {}, for which no metadata exists. All subsequent changes to rows in this table will be ignored.",
                                tableId);
                }
            }

            // 创建并注册该表的转换器
            Converter converter = new Converter() {
                @Override
                public TableId tableId() {
                    return tableId;
                }

                @Override
                public String topic() {
                    return topicName;
                }

                @Override
                public Integer partition() {
                    return null;
                }

                @Override
                public Schema keySchema() {
                    return tableSchema.keySchema();
                }

                @Override
                public Schema valueSchema() {
                    return tableSchema.valueSchema();
                }

                @Override
                public Object createKey(Serializable[] row, BitSet includedColumns) {
                    return tableSchema.keyFromColumnData(row);
                }

                @Override
                public Struct inserted(Serializable[] row, BitSet includedColumns) {
                    return tableSchema.valueFromColumnData(row);
                }

                @Override
                public Struct updated(Serializable[] before, BitSet includedColumns, Serializable[] after,
                                      BitSet includedColumnsBeforeUpdate) {
                    return tableSchema.valueFromColumnData(after);
                }

                @Override
                public Struct deleted(Serializable[] deleted, BitSet includedColumns) {
                    return null; // tableSchema.valueFromColumnData(row);
                }
            };
            convertersByTableId.put(tableNumber, converter);

            // 更新表名到表号的映射,如果表名已存在,移除旧的表号对应的转换器
            Long previousTableNumber = tableNumbersByTableName.put(tableName, tableNumber);
            if (previousTableNumber != null) {
                convertersByTableId.remove(previousTableNumber);
            }
        } else if (logger.isDebugEnabled()) {
            logger.debug("Skipping update table metadata event: {}", event);
        }
    }

    2 handleInsert

  /**
     * 处理插入事件的方法
     *
     * @param event   数据库事件,包含插入信息
     * @param source  事件源信息,用于记录分区和偏移量
     * @param recorder 消费者,用于处理转换后的源记录
     */
    public void handleInsert(Event event, SourceInfo source, Consumer<SourceRecord> recorder) {
        // 获取事件中的插入数据
        WriteRowsEventData write = event.getData();
        // 获取表ID
        long tableNumber = write.getTableId();
        // 获取包含的列信息
        BitSet includedColumns = write.getIncludedColumns();

        // 根据表ID获取相应的转换器
        Converter converter = convertersByTableId.get(tableNumber);

        // 如果找到对应的转换器
        if (converter != null) {
            // 获取表的唯一标识
            TableId tableId = converter.tableId();

            // 如果表符合过滤条件,则处理该插入事件
            if (tableFilter.test(tableId)) {
                logger.debug("Processing insert row event for {}: {}", tableId, event);

                // 获取主题和分区信息
                String topic = converter.topic();
                Integer partition = converter.partition();

                // 获取所有插入的行
                List<Serializable[]> rows = write.getRows();

                // 遍历每一行
                for (int row = 0; row != rows.size(); ++row) {
                    Serializable[] values = rows.get(row);

                    // 获取键的模式和值
                    Schema keySchema = converter.keySchema();
                    Object key = converter.createKey(values, includedColumns);

                    // 获取值的模式和转换后的值
                    Schema valueSchema = converter.valueSchema();
                    Struct value = converter.inserted(values, includedColumns);

                    // 如果值或键不为空,则创建源记录并传递给消费者
                    if (value != null || key != null) {
                        SourceRecord record = new SourceRecord(source.partition(), source.offset(row), topic, partition,
                                keySchema, key, valueSchema, value);
                        recorder.accept(record);
                    }
                }
            } else if (logger.isDebugEnabled()) {
                logger.debug("Skipping insert row event: {}", event);
            }
        } else {
            logger.warn("Unable to find converter for table #{} in {}", tableNumber, convertersByTableId);
        }
    }

 

   3 handleUpdate

 public void handleUpdate(Event event, SourceInfo source, Consumer<SourceRecord> recorder) {
        UpdateRowsEventData update = event.getData();
        long tableNumber = update.getTableId();
        BitSet includedColumns = update.getIncludedColumns();
        BitSet includedColumnsBefore = update.getIncludedColumnsBeforeUpdate();
        Converter converter = convertersByTableId.get(tableNumber);
        if (converter != null) {
            TableId tableId = converter.tableId();
            if (tableFilter.test(tableId)) {
                logger.debug("Processing update row event for {}: {}", tableId, event);
                String topic = converter.topic();
                Integer partition = converter.partition();
                List<Entry<Serializable[], Serializable[]>> rows = update.getRows();
                for (int row = 0; row != rows.size(); ++row) {
                    Map.Entry<Serializable[], Serializable[]> changes = rows.get(row);
                    Serializable[] before = changes.getKey();
                    Serializable[] after = changes.getValue();
                    Schema keySchema = converter.keySchema();
                    Object key = converter.createKey(after, includedColumns);
                    Schema valueSchema = converter.valueSchema();
                    Struct value = converter.updated(before, includedColumnsBefore, after, includedColumns);
                    if (value != null || key != null) {
                        SourceRecord record = new SourceRecord(source.partition(), source.offset(row), topic, partition,
                                keySchema, key, valueSchema, value);
                        recorder.accept(record);
                    }
                }
            } else if (logger.isDebugEnabled()) {
                logger.debug("Skipping update row event: {}", event);
            }
        } else {
            logger.warn("Unable to find converter for table #{} in {}", tableNumber, convertersByTableId);
        }
    }

 

   4 handleDelete

 public void handleDelete(Event event, SourceInfo source, Consumer<SourceRecord> recorder) {
        DeleteRowsEventData deleted = event.getData();
        long tableNumber = deleted.getTableId();
        BitSet includedColumns = deleted.getIncludedColumns();
        Converter converter = convertersByTableId.get(tableNumber);
        if (converter != null) {
            TableId tableId = converter.tableId();
            if (tableFilter.test(tableId)) {
                logger.debug("Processing delete row event for {}: {}", tableId, event);
                String topic = converter.topic();
                Integer partition = converter.partition();
                List<Serializable[]> rows = deleted.getRows();
                for (int row = 0; row != rows.size(); ++row) {
                    Serializable[] values = rows.get(row);
                    Schema keySchema = converter.keySchema();
                    Object key = converter.createKey(values, includedColumns);
                    Schema valueSchema = converter.valueSchema();
                    Struct value = converter.deleted(values, includedColumns);
                    if (value != null || key != null) {
                        if ( value == null ) valueSchema = null;
                        SourceRecord record = new SourceRecord(source.partition(), source.offset(row), topic, partition,
                                keySchema, key, valueSchema, value);
                        recorder.accept(record);
                    }
                }
            } else if (logger.isDebugEnabled()) {
                logger.debug("Skipping delete row event: {}", event);
            }
        } else {
            logger.warn("Unable to find converter for table #{} in {}", tableNumber, convertersByTableId);
        }
    }

 

5 Converter

 protected static interface Converter {
        TableId tableId();

        String topic();

        Integer partition();

        Schema keySchema();

        Schema valueSchema();

        Object createKey(Serializable[] row, BitSet includedColumns);

        Struct inserted(Serializable[] row, BitSet includedColumns);

        Struct updated(Serializable[] before, BitSet includedColumns, Serializable[] after, BitSet includedColumnsBeforeUpdate);

        Struct deleted(Serializable[] deleted, BitSet includedColumns);
    }

 

小结

  1. 初始化:构造函数中初始化了日志记录器、数据库历史记录、主题选择器、DDL解析器等组件。
  2. 加载表信息:loadTables 方法加载现有的表信息并创建表模式。
  3. 处理日志旋转:rotateLogs 方法处理日志旋转事件,清空转换器缓存。
  4. 处理表更新命令:updateTableCommand 方法处理DDL语句,更新表结构并记录历史。
  5. 更新表元数据:updateTableMetadata 方法处理表元数据更新事件,创建并注册新的转换器。
  6. 处理插入、更新和删除事件:handleInsert、handleUpdate 和 handleDelete 方法分别处理插入、更新和删除事件,生成相应的Kafka源记录。
;