単純なデータ同期コンポーネントを実装する方法

現在、Flink CDC、DataX、seaTunel、Kattle など、データ同期コンポーネントのオープンソース コミュニティが多数あり、ログベースと JDBC ベースの 2 つのタイプに大別できます。これらの同期コンポーネントは、ライブラリ全体が同期されている場合、またはスキーマの違いが大きくない場合、ビジュアル インターフェイスまたは構成ファイル マッピングを通じてライブラリ間の同期要件を直接満たすことができますが、大規模なカスタマイズの問題を伴う一部のシナリオを処理するのは比較的困難です。このようなシナリオに対応するために、作成者は小さな同期コンポーネントを作成しました。

PS: 私たちのビジネス シナリオは非常に特殊で、Oracle、Mysql、ファイル、API インターフェイスなど、さまざまな種類のソースがあります。もう 1 つのポイントは、同期する必要があるデータの量がそれほど多くないことですが、追加のデータ同期コンポーネントを導入すると、追加の運用保守コストと学習コストがかかるため、上記 2 点を考慮して、自分たちで小さなコンポーネントを書きます。

ビジネスフレームワーク

画像.png

2つの基本的なニーズ

  • 二重書き込みをサポートします。つまり、外部ソースがデータ同期コンポーネントを通過します。一方で、カスタマイズされた標準化モデルに従って必要なテーマ データを変換します。他方では、元のデータをそのままの状態で当社に返す必要があります。独自のライブラリ。
  • 专题库数据逆変換をサポートできるため、異なるものとして記述されます原始库的数据格式

コンポーネントモデル

Mysql を例に挙げると、同期コンポーネントの主なモジュールは次のとおりです。

画像.png

  • 1. 緑色の背景はビジネス プラグインです。各ビジネスはビジネス プラグインに対応し、同期装置 Syncer とコンバータ Convertor を作成します。
  • 2. 点線は、ターゲット ライブラリ データを別のオリジナル ライブラリ データに変換するための別のチャネルです
  • 3. いくつかのキューの助けを借りて、単純な生産者/消費者モデルが実現され、キューは元のライブラリの読み取りとターゲット ライブラリの書き込みの制御のその後の実装を容易にするためのバッファとして使用されます。

プロジェクトのモジュールは次のように分かれています。

画像.png

  • api-web は API サービスを外部に提供し、データ同期タスクの開始、データ同期タスクのキャンセル、データ同期タスクの一時停止、データ同期タスクのキャンセルなどを行うことができます。
  • common は、いくつかの共通のツール、モデル、列挙です。
  • MysqlConnector などのコネクタの具体的な実装は、JDBC API に基づいて、元のライブラリのデータ読み取りを実現します。
  • コア コア パッケージには主に、いくつかのインターフェイスの定義とロジック処理の標準化されたプロセスが含まれます。
  • executor タスク管理、スレッド プール リソース管理などを含む実行層。
  • plugins 具体业务插件,主要使用 Syncer 同步器和 Convertor 模型转换器

核心问题

针对数据同步组件,解决的核心问题可以抽象成如下模型:A_DB -> A -> B -> B_DB;即将 A 数据库中的数据读取出来之后转换成 A class instance,然后将 A class instance 转换成 B class instance,再将 B class instance 写到 B 数据库。

解决 A_DB 到 A

从 A_DB -> A 或者 B -> B_DB 这个过程,就是我们所熟知的 ORM 解决的问题;不管是 hibernate、mybatis 还是 SpringBoot JPA 都是围绕着这个问题展开的。

在本篇的组件中,因没有引入 ORM,所以将数据库行映射成一个 java 对象也需要自己实现。DataX 中是通过配置文件来描述的,在本篇中没有才采用这种描述方式,而是通过语言耦合性更高的注解的方式来实现的(由业务属性决定);

如下是一个描述具体业务的 Java 对象的定义,@Table 注解用来描述 JmltModel 和哪个表是关联的, @Colum 注解用来描述属性是和哪个字段关联的.

@Data
// @Table 注解用来描述 JmltModel 和哪个表是关联 的
@Table(name = "user_info") 
public class JmltModel implements Serializable {
    // @Colum 注解用来描述属性是和哪个字段关联的
    @Colum(name = "id")
    private Long id;
    @Colum(name = "email")
    private String email;
    @Colum(name = "name")
    private String name;
    @Colum(name = "create_time")
    private Date create_time;
}

有了这个描述关系,即可以在 runtime 时通过泛型 + 反射来实现 A_DB -> A 过程的模板设计。

MysqlConnector 的实现来进行说明,下面抽取了 MysqlConnector 组件中的部分代码(做了一些删减);下面这段代码中有 1-6 6 的步骤,这部分属于生产端,即从原始 Mysql 表中分页读取数据,并将读取到的数据映射成实际的对象,再通过业务定义的 convertor 转换成目标的对象,最后丢到队列中去等待消费

// 1、originClass 是原始库对象,这里通过反射获取 Table 注解,从而拿到表名
Table table = (Table) originClass.getDeclaredAnnotation(Table.class);
String tableName = table.name();

// 2、计算所有的条数,然后按照分页的方式进行 fetch
SqlTemplate sqlTemplate = new SqlTemplate(this.originDataSource);
int totalCount = sqlTemplate.count();
RowBounds rowBounds = new RowBounds(totalCount);
int totalPage = rowBounds.getTotalPage();
// 3、这里是按分页批量拉取
for (int i = 1; i <= totalPage; i++) {
    int offset = rowBounds.getOffset(i);
    String condition = " limit " + offset + "," + rowBounds.getPageSize();
    ResultSet resultSet = sqlTemplate.select(condition);
    // 4、将 ResultSet 转成 A 这里就是从 A_DB 到 A 的过程
    ResultSetExtractor<R> extractor = (ResultSetExtractor<R>) new ResultSetExtractor<>(originClass);
    List<R> result = extractor.extractData(resultSet);
    // 5、将 A 转成 B
    List<T> targetResult = convertor.batchConvertFrom(result);
    // 6、丢到队列中去等待消费
    this.rowObjectManager.pushToQueue(targetResult);
}

上記コード部分の 4 が からA_DB 到 Aの、実際にはこの部分が ResultSet から Java オブジェクトまでの処理です。一般に、JDBC API に基づいてプログラミングする場合、Java への ResultSet はビジネスにとって非常に明確です。おおよそ次のとおりです。

String selectSql = "SELECT * FROM employees"; 
try (ResultSet resultSet = stmt.executeQuery(selectSql)) { 
List<Employee> employees = new ArrayList<>(); 
while (resultSet.next()) 
{ 
    Employee emp = new Employee(); 
    emp.setId(resultSet.getInt("emp_id"));
    emp.setName(resultSet.getString("name"));
    emp.setPosition(resultSet.getString("position")); 
    emp.setSalary(resultSet.getDouble("salary")); 
    employees.add(emp); 
} 

この種の動作は、明確な Java オブジェクトには問題ありませんが、一般的なコンポーネントには明らかに十分ではありません。次に、ResultSet を Java オブジェクトに変換してより一般的にする方法を解決する必要があります。アイデアは次のとおりですResultSet -> Map -> Java Object。ResultSet の getMetaData を通じてすべての列名 (K) と値 (V) を取得し、それらを Map に保存できます。コードは次のとおりです。

/**
 * 将 resultSet 转成 Map
 *
 * @param resultSet
 * @return
 * @throws SQLException
 */
private Map<String, Object> resultSetToMap(ResultSet resultSet) throws Exception {
    Map<String, Object> resultMap = new HashMap<>();
    // 获取 ResultSet 的元数据
    int columnCount = resultSet.getMetaData().getColumnCount();
    // 遍历每一列,将列名和值存储到 Map 中
    for (int i = 1; i <= columnCount; i++) {
        String columnName = resultSet.getMetaData().getColumnName(i);
        Object value = resultSet.getObject(i);
        resultMap.put(columnName, value);
    }
    return resultMap;
}

次のステップは、Map を Java オブジェクトに変換することです。もちろん、フレームワーク レベルでは、より一般的なシナリオを実装するために汎用メカニズムが使用されます。

private T mapResultSetToObject(Map<String, Object> resultMap, Class<T> objectType) throws Exception {
    // 通过目标对象类型构建一个对象
    T object = objectType.newInstance();  
    // 将 map 的 key 作为 field 的名字,map 的 value 作为 field 的值
    for (Map.Entry<String, Object> entry : resultMap.entrySet()) {  
        String fieldName = entry.getKey();  
        Object value = entry.getValue();  
        try {  
            Field declaredField = objectType.getDeclaredField(fieldName);  
            declaredField.setAccessible(true);  
            declaredField.set(object, value);  
        } catch (NoSuchFieldException e) {  
            LOGGER.error("ignore exception, fieldName: " + fieldName + ", objectType: " + objectType);  
        }  
    }
    // 完成对象的填充并返回
    return object;  
}

A から B まではビジネス自身、つまり Convertor 部分によって定義されます。以下は Convertor のインターフェース定義です。ビジネスはこのインターフェースを実装して、オブジェクト間の変換 (バッチ変換を含む) を実現します。

// T 是目标对象类型,R 是原始对象类型
public interface Convertor<T, R> {  
  
/**  
* 将 T 转换成 R  
*  
* @param origin  
* @return  
*/  
T convertFrom(R origin) throws SQLException;  
  
/**  
* 将 R 转换成 T  
*  
* @param target  
* @return  
*/  
R convertTo(T target);  
  
/**  
* 批量转换  
* @param origin  
* @return  
* @throws SQLException  
*/  
List<T> batchConvertFrom(List<R> origin) throws SQLException;  
  
/**  
* 批量转换将 R 转换成 T  
*  
* @param target  
* @return  
*/ 
List<R> batchConvertTo(List<T> target);

B から B_DB へ

前がA_DBからA、Bへの処理、その次がBからB_DBへの処理です。前のフローチャートからわかるように、キューはコンポーネントで使用されます。この記事の実装は Disruptor に基づいています。Convertor の A->B のロジックが完了すると、B のリストが Disruptor のリングバッファにスローされて消費されます。消費ロジックは次のとおりです。

public void onEvent(RowObjectEvent rowObjectEvent, long sequence, boolean endOfBatch) {  
    // targetResult  
    List<T> targetResult = (List) rowObjectEvent.getRowObject();  
    // 将 T 写到 目标库  
    Connection tc = null;  
    PreparedStatement pstm = null;  
    try {  
        // 下面即为获取连接,创建 prepareStatement 和执行
        tc = this.targetDataSource.getConnection();  
        SqlTemplate<T> sqlTemplate = new SqlTemplate<>(this.targetDataSource); 
        sqlTemplate.setObj(targetResult.get(0));  
        String sql = sqlTemplate.createBaseSql();  
        pstm = tc.prepareStatement(sql);  
        for (T item : targetResult) {  
            Object[] objects = sqlTemplate.createInsertSql(item);  
            for (int i = 1; i <= objects.length; i++) {  
                if (objects[i - 1] instanceof Long) {  
                    objects[i - 1] = (Long) objects[i - 1] + 1;  
                }  
                pstm.setObject(i, objects[i - 1]);  
            } 
            // 这里执行时批量操作的
            pstm.addBatch();  
        }  
        pstm.executeUpdate();  
    } catch (Exception e) {  
        // ignore some code...
    }  
  
}

上記のいくつかの部分を通じて、データ同期のプロセスと、この記事で実装されているコンポーネントのいくつかのコア ロジックについて概説します。

要約する

この記事のコード スニペットは比較的断片的であり、ロジックの関連部分の一部はこの記事には反映されていません。この記事の主な目的は、JDBC API に基づいて同期を実現する方法を説明することです (実際には JDBC に基づいているだけではありません)。DB -> A -> B -> DB この、興味のある学生は自分でデータ同期コンポーネントの実装を試すことができます。ご質問がございましたら、お気軽にお問い合わせください。

おすすめ

転載: juejin.im/post/7245922875182055484