實現MySQL同步數據到ES構建寬表


作者介紹

Ceven,德勤樂融(北京)科技有限公司
郵箱:likailin@deqinyuerong.com

前言

CloudCanal 近期提供了自定義代碼構建寬表能力,我們第一時間參與了該特性內測,效果不錯。開發流程詳見官方文檔 《CloudCanal自定義代碼實時加工》

能力特點包括:

  • 靈活,支持反查打寬表,特定邏輯數據清洗,對賬,告警等場景
  • 調試方便,通過任務參數配置自動打開 debug 端口,對接 IDE 調試
  • SDK 接口清晰,提供豐富的上下文信息,方便數據邏輯開發

本文基於我們業務中的實際需求(MySQL -> ElasticSearch 寬表構建),梳理一下具體的開發調試流程,希望對大家有所幫助。

場景描述

MySQL 擅長關系型數據操作,我們在其中存儲了 product, tag, product_tag_mapping 表數據,用以表示產品標簽之間多對多關系。精簡的數據結構如下:

88ae6c35-4519-4d51-b725-d05765d67b06-image.png

ElasticSearch 擅長搜索,但是並不支持不同索引間的聯合查詢, 所以構造寬表是業界剛需。我們存儲其上的產品索引結構如下:

PUT es_product
{
  "mappings" : {
    "properties" : {
      "id" : {
        "type" : "integer"
      },
      "name" : {
        "type" : "text"
      },
      "tags" : {
        "type" : "nested", 
        "properties" : {
          "id" : {
            "type" : "integer"
          },
          "name" : {
            "type" : "text"
          }
        }
      }
    }
  }
}

同步策略

CloudCanal 在 同步 MySQL -> ElasticSearch 數據過程中,會兼顧全量增量兩種情況,我們可以創建兩個獨立的任務,分別同步產品的基礎信息和附加信息(即標簽信息)。

  • 基礎信息任務
    • 使用基本的映射關系,將 MySQL 中的 product 數據表,映射到 es_product 索引中,即可保證全量和增量的數據同步。
  • 附加信息任務
    • 創建 CloudCanal 任務將 MySQL 中的 product_tag_mapping 數據表映射到 es_product 索引中,同步過程中反查源數據庫中的 tag 信息,構造寬表數據,填充進 es_product 索引,實現附加信息全量和增量的數據同步。

實現步驟

1. MySQL 表結構初始化

# 創建產品信息表
CREATE TABLE `product` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(64) COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '名稱',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci COMMENT='產品信息記錄表';

# 創建標簽信息表
CREATE TABLE `tag` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(64) COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '名稱',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci COMMENT='標簽信息記錄表';

# 創建產品標簽關系表
CREATE TABLE `product_tag_mapping` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `product_id` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '產品ID',
  `tag_id` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '標簽ID',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci COMMENT='產品標簽關系表';

2. MySQL 填充測試數據

# 填充產品信息
INSERT INTO `product` (`name`)
VALUES
    ('product_1');
    
# 填充標簽信息
INSERT INTO `tag` (`name`)
VALUES
    ('tag_1'),
    ('tag_2');

# 填充產品標簽關系信息
INSERT INTO `product_tag_mapping` (`product_id`, `tag_id`)
VALUES
    (1, 1);

3. ElasticSearch 索引創建(也可以使用 CloudCanal 結構遷移)

PUT es_product
{
  "mappings" : {
    "properties" : {
      "id" : {
        "type" : "integer"
      },
      "name" : {
        "type" : "text"
      },
      "tags" : {
        "type" : "nested", 
        "properties" : {
          "id" : {
            "type" : "integer"
          },
          "name" : {
            "type" : "text"
          }
        }
      }
    }
  }
}

4. 編寫自定義代碼

自定義代碼的項目基於 maven 構建,可以參考 示例項目 cloudcanal-sdk-demos

4.1 修改 MAVEN 配置

初始化的項目需要手工配置一下 pom.xml 文件,將 sdk 指向本地目錄文件,代碼片段如下

<dependency>
    <groupId>com.clougence.cloudcanal</groupId>
    <artifactId>cloudcanal-sdk</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <scope>system</scope>
    <systemPath>
        /path/to/your/project/src/main/resources/lib/cloudcanal-sdk-2.0.0.9-SNAPSHOT.jar
    </systemPath>
</dependency>

4.2 實現 TAG 類

public class Tag {
    private int id;
    private String name;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

4.3 實現 PROCESSOR 處理邏輯

        @Override
        public List<CustomRecord> process(List<CustomRecord> list, CustomProcessorContext context) {
            DataSource dataSource = (DataSource) context.getProcessorContextMap().get(RdbContextKey.SOURCE_DATASOURCE);
            String stage = context.getProcessorContextMap().get("currentTaskStage").toString();
    
            for (CustomRecord record : list) {
                try (Connection connection = dataSource.getConnection(); Statement statement = connection.createStatement()) {
    
                    // 由於 ES 的嵌套結構會被認為是獨立的文檔,故需要填充舊的數據
                    ResultSet rs = statement.executeQuery("SELECT `tag`.`id`, `tag`.`name`" +
                            " FROM `product`.`product_tag_mapping` AS `mapping`" +
                            " LEFT JOIN `product`.`tag` AS `tag` ON `tag`.`id` = `mapping`.`tag_id`" +
                            " WHERE `mapping`.`product_id` = " + record.getFieldMapAfter().get("product_id").getValue()
                    );
    
                    List<Tag> tags = buildTags(rs);
                    if ("INCREMENT".equals(stage)) {
                        // 增量創建的 product_tag_mapping 處於內存中,無法通過 SQL 語句查詢得到,故需要單獨處理
                        rs = statement.executeQuery("SELECT `id`, `name` FROM `product`.`tag` WHERE `id` = " + record.getFieldMapAfter().get("tag_id").getValue().toString());
                        List<Tag> newTags = buildTags(rs);
                        tags.add(newTags.get(0));
                    }
    
                    ObjectMapper mapper = new ObjectMapper();
                    String json = mapper.writeValueAsString(tags);
                    Map<String, Object> tagField = new LinkedHashMap<>();
                    tagField.put("tags", json);
                    RecordBuilder.modifyRecordBuilder(record)
                            .addField(tagField)
                            .build();
                } catch (SQLException | JsonProcessingException e) {
                    e.printStackTrace();
                }
            }
            return list;
        }
    
        private List<Tag> buildTags(ResultSet rs) throws SQLException {
            List<Tag> tags = new ArrayList<>();
            while (rs.next()) {
                Tag tag = new Tag();
                tag.setId(rs.getInt("id"));
                tag.setName(rs.getString("name"));
                tags.add(tag);
            }
            return tags;
        }

4.4 編譯自定義代碼包

執行如下命令編譯生成自定義代碼包, 之后會在 target 目錄中生成 jar 文件

mvn clean package -Dmaven.test.skip=true -Dmaven.compile.fork=true

5. 創建 CloudCanal 任務

5.1 同步 PRODUCT 基礎數據

全量增量同步 product 信息到 es_product 索引,在此就不做具體描述,詳情請參考 CloudCanal 文檔。

此時查詢產品數據,得到結果

787f8ce4-6ad8-4d57-8a05-5694c705fed1-image.png

5.2 擴展 PRODUCT TAG 數據

5.2.1 配置數據源和目標

b8b1f5ec-3e3c-4620-ba87-ba224ca265e1-image.png

5.2.2 配置規格

可去掉自動啟動任務選項,以便於單步追蹤調試
8b1e059d-b4cb-4795-b27e-50cb5ae2c2a3-image.png

5.2.3 配置索引映射

Tips: 只配置增加操作,不要配置編輯和刪除,否則可能造成對數據的誤刪;
      編輯和刪除操作,只最好使用 ES 調用的方式進行處理;
      增加操作最好不要使用 ES 調用的方式處理,會引起高並發問題。

5.2.4 上傳自定義代碼

4b8abc11-5c10-4be5-932a-b4dfc6e7740f-image.png

f1e71074-7ce3-48ec-a162-b1814fe928bb-image.png

Tips: 創建任務時如果不上傳自定義代碼包,之后將無法上傳,除非重建任務。
      上傳自定義代碼,意味着創建特殊類型的任務,然后才會出現特殊的選項進行字段映射。

5.2.5 配置字段映射

將 id 和 tag_id 調整為 “只訂閱不同步”(老版本此處會顯示為僅供自定義代碼使用),實現只訂閱這兩個字段,而不會真正寫入到 ES 索引,而將 product_id 映射到對端的 id。
1ec04979-b240-4953-8026-dbecbde0c886-image.png

設置映射 _id,以指定目標 ES 索引中的 id 為 product_id

513633e9-a603-43d5-b9f1-6d6b7b0cd504-image.png

b1419349-20cc-4c4d-a09b-a75bc7a9218b-image.png

Tips: product_id 字段必須做映射,否則即使配置了 _id 信息,依舊無法正常執行,會忽略 product_id 字段的值。

6. 同步結果

87ec9e06-17ac-4bed-b307-79e17cca03ea-image.png

調試自定義代碼

自定義代碼在開發階段最麻煩的事情是如何高效進行調試,CloudCanal 能夠比較友好的讓開發在本地直接調試代碼邏輯。

修改任務參數

任務詳情->參數修改

f3f59272-9b6a-40f9-ac3e-618782833676-image.png

00072b56-dbe0-4ce0-939a-7e22141419d5-image.png

Tips:每次修改完參數信息之后,必須點擊生效配置和重啟任務;
      在任務詳情配置中,也可以上傳新的代碼包,激活和重啟任務后可以使用。

配置 IntelliJ IDEA Debug 模式

b29b139e-1ffb-409c-bad5-6ee7ae76863b-image.png

Tips: 設置好斷點以后,需要先啟動 CloudCanal 任務,再點擊 debug 按鈕,才能 Attach 到遠程的 8787 端口;
      CloudCanal 會一直 pending,直到有 Attachment,才會繼續執行,所以不需要單步跟蹤調試時,一定記得關閉調試模式,否則任務無法執行。

總結

CloudCanal 自定義代碼能夠拓展的能力具有不錯的想象空間,我們甚至能加入一些在線業務邏輯的處理,讓業務需求能夠更好的滿足,同時配合社區版調試也很方便。希望未來這塊能力在便利功能,性能等層面有更好的表現。

參與內測

CloudCanal 會不斷提供一些預覽的能力,包括新數據鏈路, 優化能力,功能插件。本文所描述的自定義代碼能力目前也處於內測階段。如需體驗,可添加我們小助手(微信號:suhuayue001)進行了解和試用。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM