Kafka - SQL 程式碼實現

哥不是小蘿莉發表於2016-05-09

1.概述

  上次給大家分享了關於 Kafka SQL 的實現思路,這次給大家分享如何實現 Kafka SQL。要實現 Kafka SQL,在上一篇《Kafka - SQL 引擎分享》中分享了其實現的思路,核心包含資料來源的載入,以及 SQL 樹的對映。今天筆者給大家分享相關實現的程式碼。

2.內容

  這裡,將資料對映成 SQL Tree 是使用了 Apache Calcite 來承接這部分工作。在實現程式碼之前,我們首先來了解下 Apache Calcite 的相關內容,Apache Calcite 是一個面向 Hadoop 的查詢引擎,它提供了業界標準的 SQL 語言,以及多種查詢優化和連線各種儲存介質的介面卡。另外,還能處理 OLAP 和流處理場景。因為存在這麼多優秀和閃光的特性, Hadoop 生態圈中 Apache Calcite 越發引人注目,被諸多專案所整合,常見的有:

  • Apache Drill:基於大資料的實時查詢引擎
  • Apache Spark:繼 Hadoop 之後的新一代大資料分散式處理框架。
  • 更多詳情,這裡就不一一列舉了,詳情檢視地址:《Adapters

2.1 資料型別

  這裡資料來源的資料型別,我們分為兩種,一種是 SQL,另一種是基於程式語言的,這裡我們所使用的是 Java,定義內容如下:

public static Map<String, SqlTypeName> SQLTYPE_MAPPING = new HashMap<String, SqlTypeName>();
public static Map<String, Class> JAVATYPE_MAPPING = new HashMap<String, Class>();

public static void initRowType() {
        SQLTYPE_MAPPING.put("char", SqlTypeName.CHAR);
        JAVATYPE_MAPPING.put("char", Character.class);
        SQLTYPE_MAPPING.put("varchar", SqlTypeName.VARCHAR);
        JAVATYPE_MAPPING.put("varchar", String.class);
        // ......     
}

2.2 表的相關描述

  另外,我們需要對錶進行一個描述,在關係型資料庫中,一個正常的表由行列組成,定義內容如下:

    public static class Database {
        public List<Table> tables = new LinkedList<Table>();
    }

    public static class Table {
        public String tableName;
        public List<Column> columns = new LinkedList<Column>();
        public List<List<String>> data = new LinkedList<List<String>>();
    }

    public static class Column {
        public String name;
        public String type;
    }

  在每個集合中儲存資料庫相關名稱,每個資料庫儲存多個集合的表物件,每個表物件下面又有一系列的列以及繫結的資料來源。在每個列物件中包含欄位名和型別,層層遞進,依次關聯。在使用 Calcite 是,需要遵循其 JSON Model,上篇部落格我們已經定義過其 JSON Model,這裡我們直接拿來使用,內容如下:

{
    version: '1.0',
    defaultSchema: 'kafka',  
    schemas: [  
        {
            name: 'kafka',  
            type: 'custom',
            factory: 'cn.smartloli.kafka.visual.engine.KafkaMemorySchemaFactory',  
            operand: {
                database: 'kafka_db'
            }  
        } 
    ]
}

   要實現其 Model ,這裡需要我們去實現 org.apache.calcite.schema.SchemaFactory 的介面,內容如下所示:

public class KafkaMemorySchemaFactory implements SchemaFactory {
    @Override
    public Schema create(SchemaPlus parentSchema, String name, Map<String, Object> operand) {
        return new KafkaMemorySchema(name);
    }
}

  而在 KafkaMemorySchema 類中,我們只需要實現它的 getTableMap 方法,內容如下所示:

 @Override
 protected Map<String, Table> getTableMap() {
   Map<String, Table> tables = new HashMap<String, Table>();
    Database database = KafkaMemoryData.MAP.get(this.dbName);
    if (database == null)
      return tables;
    for (KafkaMemoryData.Table table : database.tables) {
      tables.put(table.tableName, new KafkaMemoryTable(table));
    }
    return tables;
 }

  從上述程式碼中,可以知道通過記憶體中的 Map 表檢視對應的資料庫物件,然後根據資料庫物件中的表作為 Schema 中的表,而表的型別為 KafkaMemoryTable。

2.3 表型別

  這裡筆者就直接使用全表掃描,使用 org.apache.calcite.schema.impl.AbstractTable 的預設方式,實現其 getRowType 方法和 scan 方法,內容如下所示:

public RelDataType getRowType(RelDataTypeFactory typeFactory) {
  if(dataType == null) {
     RelDataTypeFactory.FieldInfoBuilder fieldInfo = typeFactory.builder();
      for (KafkaMemoryData.Column column : this.sourceTable.columns) {
        RelDataType sqlType = typeFactory.createJavaType(
        KafkaMemoryData.JAVATYPE_MAPPING.get(column.type));
        sqlType = SqlTypeUtil.addCharsetAndCollation(sqlType, typeFactory);
        fieldInfo.add(column.name, sqlType);
      }
      this.dataType = typeFactory.createStructType(fieldInfo);
   }
   return this.dataType;
}
public Enumerable<Object[]> scan(DataContext root) {
        final List<String> types = new ArrayList<String>(sourceTable.columns.size());
        for(KafkaMemoryData.Column column : sourceTable.columns) {
            types.add(column.type);
        }
        final int[] fields = identityList(this.dataType.getFieldCount());
        return new AbstractEnumerable<Object[]>() {
            public Enumerator<Object[]> enumerator() {
                return new KafkaMemoryEnumerator<Object[]>(fields, types, sourceTable.data);
            }
        };
    }

  程式碼中,表中的欄位名和型別是根據初始化時,每個表中的資料型別對映匹配的,在 KafkaMemoryData.SQLTYPE_MAPPING 和 KafkaMemoryData.JAVATYPE_MAPPING 中有描述相關自定義型別對映,這裡就不多做贅述了。

  實現流程大致就是這個樣子,將每次的 SQL 查詢,通過 Calcite 解析成標準可執行的 SQL 計劃,執行期間會根據定義的資訊,初始化每一個 Schema,在通過呼叫 getTableMap 獲取欄位名和型別,根據這些資訊判斷查詢的表,欄位名,型別以及 SQL 語法是否標準規範。然後在使用 Calcite 內部機制,生成物理執行計劃。查詢計劃是 Tree 形式的,底層是進行掃表操作(可看作為 FROM),獲取每個表的資料,之後在根據表資料進行上層的關聯操作,如 JOIN,GROUP BY,LIMIT 等操作。

3.測試

  完成上述流程後,進行程式碼測試,測試程式碼如下所示:

public static void main(String[] args) {
        try {
            Class.forName("org.apache.calcite.jdbc.Driver");
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        Properties info = new Properties();
        try {
            Connection connection = DriverManager.getConnection("jdbc:calcite:model=/Users/dengjie/hadoop/workspace/kafka/kafka-visual/src/main/resources/plugins.json",info);    
            Statement st = connection.createStatement();
            // String sql = "select * from \"Kafka\" where \"_plat\"='1004' limit 1";
            String sql = "select * from \"Kafka\" limit 10";

            long start = System.currentTimeMillis();
            result = st.executeQuery(sql);
            ResultSetMetaData rsmd = result.getMetaData();
            List<Map<String, Object>> ret = new ArrayList<Map<String,Object>>();
            
            while (result.next()) {
                Map<String, Object> map = new HashMap<String, Object>();
                for (int i = 1; i <= rsmd.getColumnCount(); i++) {
                    System.out.print(result.getString(rsmd.getColumnName(i)) + " ");
                    map.put(rsmd.getColumnName(i), result.getString(rsmd.getColumnName(i)));
                }
                ret.add(map);
                System.out.println();
            }
            System.out.println(new Gson().toJson(ret));       
            result.close();
            connection.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

4.總結

  以上便是將 Kafka 中資料消費後,作為資料來源載入和 SQL Tree 對映的實現程式碼,實現不算太困難,在編寫 SQL 查詢的時候,需要遵循標準的 SQL 語法來運算元據源。

5.結束語

  這篇部落格就和大家分享到這裡,如果大家在研究學習的過程當中有什麼問題,可以加群進行討論或傳送郵件給我,我會盡我所能為您解答,與君共勉!

 

相關文章