diff --git a/client-adapter/launcher/src/main/java/com/alibaba/otter/canal/adapter/launcher/CanalAdapterApplication.java b/client-adapter/launcher/src/main/java/com/alibaba/otter/canal/adapter/launcher/CanalAdapterApplication.java
index 3d2ef585..64cab2e5 100644
--- a/client-adapter/launcher/src/main/java/com/alibaba/otter/canal/adapter/launcher/CanalAdapterApplication.java
+++ b/client-adapter/launcher/src/main/java/com/alibaba/otter/canal/adapter/launcher/CanalAdapterApplication.java
@@ -12,8 +12,8 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
*/
@SpringBootApplication
public class CanalAdapterApplication {
-
public static void main(String[] args) {
+
SpringApplication application = new SpringApplication(CanalAdapterApplication.class);
application.setBannerMode(Banner.Mode.OFF);
application.run(args);
diff --git a/client-adapter/launcher/src/main/resources/application.yml b/client-adapter/launcher/src/main/resources/application.yml
index efcfd146..889b401a 100644
--- a/client-adapter/launcher/src/main/resources/application.yml
+++ b/client-adapter/launcher/src/main/resources/application.yml
@@ -94,4 +94,11 @@ canal.conf:
# - name: kudu
# key: kudu
# properties:
-# kudu.master.address: 127.0.0.1 # ',' split multi address
\ No newline at end of file
+# kudu.master.address: 127.0.0.1 # ',' split multi address
+# - name: phoenix
+# key: phoenix
+# properties:
+# jdbc.driverClassName: org.apache.phoenix.jdbc.PhoenixDriver
+# jdbc.url: jdbc:phoenix:127.0.0.1:2181:/hbase/db
+# jdbc.username:
+# jdbc.password:
\ No newline at end of file
diff --git a/client-adapter/phoenix/pom.xml b/client-adapter/phoenix/pom.xml
new file mode 100644
index 00000000..196b331d
--- /dev/null
+++ b/client-adapter/phoenix/pom.xml
@@ -0,0 +1,154 @@
+
+
+
+ canal.client-adapter
+ com.alibaba.otter
+ 1.1.5-SNAPSHOT
+ ../pom.xml
+
+ 4.0.0
+ com.alibaba.otter
+ client-adapter.phoenix
+ jar
+ canal client adapter phoenix module for otter ${project.version}
+
+
+
+
+ org.apache.phoenix
+ phoenix-core
+ 4.14.1-HBase-1.4
+
+
+ org.apache.hbase
+ hbase-client
+ 1.4.8
+
+
+ com.google.protobuf
+ protobuf-java
+ 2.5.0
+
+
+
+ com.alibaba.otter
+ client-adapter.common
+ ${project.version}
+ provided
+
+
+ org.slf4j
+ slf4j-api
+ 1.7.12
+ provided
+
+
+ commons-lang
+ commons-lang
+ 2.6
+ provided
+
+
+ commons-io
+ commons-io
+ 2.4
+ provided
+
+
+ com.google.guava
+ guava
+ 18.0
+ provided
+
+
+ com.alibaba
+ druid
+ 1.1.9
+ provided
+
+
+ junit
+ junit
+ 4.12
+ test
+
+
+ com.alibaba
+ fastjson
+ 1.2.58
+ provided
+
+
+
+ mysql
+ mysql-connector-java
+ 5.1.48
+ provided
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-assembly-plugin
+ 2.4
+
+
+ jar-with-dependencies
+
+
+
+
+ make-assembly
+ package
+
+ single
+
+
+
+
+
+ maven-antrun-plugin
+
+
+ package
+
+ run
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/client-adapter/phoenix/src/main/java/com/alibaba/otter/canal/client/adapter/phoenix/PhoenixAdapter.java b/client-adapter/phoenix/src/main/java/com/alibaba/otter/canal/client/adapter/phoenix/PhoenixAdapter.java
new file mode 100644
index 00000000..218d9128
--- /dev/null
+++ b/client-adapter/phoenix/src/main/java/com/alibaba/otter/canal/client/adapter/phoenix/PhoenixAdapter.java
@@ -0,0 +1,291 @@
+package com.alibaba.otter.canal.client.adapter.phoenix;
+import com.alibaba.otter.canal.client.adapter.phoenix.config.ConfigLoader;
+import com.alibaba.otter.canal.client.adapter.phoenix.config.MappingConfig;
+import com.alibaba.otter.canal.client.adapter.phoenix.monitor.PhoenixConfigMonitor;
+import com.alibaba.otter.canal.client.adapter.phoenix.service.PhoenixEtlService;
+import com.alibaba.otter.canal.client.adapter.phoenix.service.PhoenixSyncService;
+import com.alibaba.otter.canal.client.adapter.OuterAdapter;
+import com.alibaba.otter.canal.client.adapter.phoenix.support.SyncUtil;
+import com.alibaba.otter.canal.client.adapter.support.*;
+import org.apache.commons.lang.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import javax.sql.DataSource;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.SQLException;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * @author: lihua
+ * @date: 2021/1/5 15:01
+ * @Description: Phoenix适配器实现类
+ */
+@SPI("phoenix")
+public class PhoenixAdapter implements OuterAdapter {
+
+ private static Logger logger = LoggerFactory.getLogger(PhoenixAdapter.class);
+
+ private Map phoenixMapping = new ConcurrentHashMap<>(); // 文件名对应配置
+ private Map> mappingConfigCache = new ConcurrentHashMap<>(); // 库名-表名对应配置
+
+ private static String DriverClass;
+ private static String PhoenixUrl;
+ private static Properties phoenixPro = new Properties();
+
+ private PhoenixSyncService phoenixSyncService;
+
+ private PhoenixConfigMonitor phoenixConfigMonitor;
+
+ private Properties envProperties;
+
+
+
+ public Map getPhoenixMapping() {
+ return phoenixMapping;
+ }
+
+ public Map> getMappingConfigCache() {
+ return mappingConfigCache;
+ }
+
+ public PhoenixAdapter() {
+ logger.info("PhoenixAdapter create: {} {}", this, Thread.currentThread().getStackTrace());
+ }
+ /**
+ * 初始化方法
+ *
+ * @param configuration 外部适配器配置信息
+ */
+ @Override
+ public void init(OuterAdapterConfig configuration, Properties envProperties) {
+ this.envProperties = envProperties;
+ Map phoenixMappingTmp = ConfigLoader.load(envProperties);
+ // 过滤不匹配的key的配置
+ phoenixMappingTmp.forEach((key, mappingConfig) -> {
+ if ((mappingConfig.getOuterAdapterKey() == null && configuration.getKey() == null)
+ || (mappingConfig.getOuterAdapterKey() != null
+ && mappingConfig.getOuterAdapterKey().equalsIgnoreCase(configuration.getKey()))) {
+ phoenixMapping.put(key, mappingConfig);
+ }
+ });
+
+ if (phoenixMapping.isEmpty()) {
+ throw new RuntimeException("No phoenix adapter found for config key: " + configuration.getKey());
+ } else {
+ logger.info("[{}]phoenix config mapping: {}", this, phoenixMapping.keySet());
+ }
+
+ for (Map.Entry entry : phoenixMapping.entrySet()) {
+ String configName = entry.getKey();
+ MappingConfig mappingConfig = entry.getValue();
+ String key;
+ if (envProperties != null && !"tcp".equalsIgnoreCase(envProperties.getProperty("canal.conf.mode"))) {
+ key = StringUtils.trimToEmpty(mappingConfig.getDestination()) + "-"
+ + StringUtils.trimToEmpty(mappingConfig.getGroupId()) + "_"
+ + mappingConfig.getDbMapping().getDatabase() + "-" + mappingConfig.getDbMapping().getTable().toLowerCase();
+ } else {
+ key = StringUtils.trimToEmpty(mappingConfig.getDestination()) + "_"
+ + mappingConfig.getDbMapping().getDatabase() + "-" + mappingConfig.getDbMapping().getTable().toLowerCase();
+ }
+ Map configMap = mappingConfigCache.computeIfAbsent(key,
+ k1 -> new ConcurrentHashMap<>());
+ configMap.put(configName, mappingConfig);
+ }
+
+
+ Map properties = configuration.getProperties();
+
+ DriverClass= properties.get("jdbc.driverClassName");
+ PhoenixUrl=properties.get("jdbc.url");
+
+ try {
+ //phoenix内部本身有连接池,不需要使用Druid初始化
+ phoenixPro.setProperty("hbase.rpc.timeout","600000");
+ phoenixPro.setProperty("hbase.client.scanner.timeout.period","600000");
+ phoenixPro.setProperty("dfs.client.socket-timeout","600000");
+ phoenixPro.setProperty("phoenix.query.keepAliveMs","600000");
+ phoenixPro.setProperty("phoenix.query.timeoutMs","3600000");
+ Class.forName(DriverClass);
+ } catch (ClassNotFoundException e) {
+ e.printStackTrace();
+ }
+ String threads = properties.get("threads");
+ phoenixSyncService = new PhoenixSyncService(
+ threads != null ? Integer.valueOf(threads) : null
+ );
+
+ phoenixConfigMonitor = new PhoenixConfigMonitor();
+ phoenixConfigMonitor.init(configuration.getKey(), this, envProperties);
+ }
+
+
+ /**
+ * 获取phoenix连接
+ * @return
+ */
+ public static Connection getPhoenixConnection() {
+ try {
+ return DriverManager.getConnection(PhoenixUrl,phoenixPro);
+ } catch (SQLException e) {
+ logger.error("getPhoenixConnection Exception"+e.getMessage());
+ }
+ return null;
+ }
+
+
+
+ /**
+ * 同步方法
+ *
+ * @param dmls 数据包
+ */
+ @Override
+ public void sync(List dmls) {
+ if (dmls == null || dmls.isEmpty()) {
+ return;
+ }
+ try {
+ phoenixSyncService.sync(mappingConfigCache, dmls, envProperties);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * ETL方法
+ *
+ * @param task 任务名, 对应配置名
+ * @param params etl筛选条件
+ * @return ETL结果
+ */
+ @Override
+ public EtlResult etl(String task, List params) {
+ EtlResult etlResult = new EtlResult();
+ MappingConfig config = phoenixMapping.get(task);
+ if (config != null) {
+ DataSource srcDataSource = DatasourceConfig.DATA_SOURCES.get(config.getDataSourceKey());
+ if (srcDataSource != null) {
+ return PhoenixEtlService.importData(srcDataSource, getPhoenixConnection(), config, params);
+ } else {
+ etlResult.setSucceeded(false);
+ etlResult.setErrorMessage("DataSource not found");
+ return etlResult;
+ }
+ } else {
+ StringBuilder resultMsg = new StringBuilder();
+ boolean resSucc = true;
+ // ds不为空说明传入的是destination
+ for (MappingConfig configTmp : phoenixMapping.values()) {
+ // 取所有的destination为task的配置
+ if (configTmp.getDestination().equals(task)) {
+ DataSource srcDataSource = DatasourceConfig.DATA_SOURCES.get(configTmp.getDataSourceKey());
+ if (srcDataSource == null) {
+ continue;
+ }
+ EtlResult etlRes = PhoenixEtlService.importData(srcDataSource,getPhoenixConnection(), configTmp, params);
+ if (!etlRes.getSucceeded()) {
+ resSucc = false;
+ resultMsg.append(etlRes.getErrorMessage()).append("\n");
+ } else {
+ resultMsg.append(etlRes.getResultMessage()).append("\n");
+ }
+ }
+ }
+ if (resultMsg.length() > 0) {
+ etlResult.setSucceeded(resSucc);
+ if (resSucc) {
+ etlResult.setResultMessage(resultMsg.toString());
+ } else {
+ etlResult.setErrorMessage(resultMsg.toString());
+ }
+ return etlResult;
+ }
+ }
+ etlResult.setSucceeded(false);
+ etlResult.setErrorMessage("Task not found");
+ return etlResult;
+ }
+
+ /**
+ * 获取总数方法
+ *
+ * @param task 任务名, 对应配置名
+ * @return 总数
+ */
+ @Override
+ public Map count(String task) {
+ Map res = new LinkedHashMap<>();
+ MappingConfig config = phoenixMapping.get(task);
+ if (config == null) {
+ logger.info("[{}]phoenix config mapping: {}", this, phoenixMapping.keySet());
+ res.put("succeeded", false);
+ res.put("errorMessage", "Task[" + task + "] not found");
+ res.put("tasks", phoenixMapping.keySet());
+ return res;
+ }
+ MappingConfig.DbMapping dbMapping = config.getDbMapping();
+ String sql = "SELECT COUNT(1) AS cnt FROM " + SyncUtil.getDbTableName(dbMapping);
+ Connection conn = null;
+ try {
+ //conn = dataSource.getConnection();
+ conn = getPhoenixConnection();
+ Util.sqlRS(conn, sql, rs -> {
+ try {
+ if (rs.next()) {
+ Long rowCount = rs.getLong("cnt");
+ res.put("count", rowCount);
+ }
+ } catch (SQLException e) {
+ logger.error(e.getMessage(), e);
+ }
+ });
+ } catch (Exception e) {
+ logger.error(e.getMessage(), e);
+ } finally {
+ if (conn != null) {
+ try {
+ conn.close();
+ } catch (SQLException e) {
+ logger.error(e.getMessage(), e);
+ }
+ }
+ }
+ res.put("targetTable", SyncUtil.getDbTableName(dbMapping));
+
+ return res;
+ }
+
+ /**
+ * 获取对应canal instance name 或 mq topic
+ *
+ * @param task 任务名, 对应配置名
+ * @return destination
+ */
+ @Override
+ public String getDestination(String task) {
+ MappingConfig config = phoenixMapping.get(task);
+ if (config != null) {
+ return config.getDestination();
+ }
+ return null;
+ }
+
+ /**
+ * 销毁方法
+ */
+ @Override
+ public void destroy() {
+ if (phoenixConfigMonitor != null) {
+ phoenixConfigMonitor.destroy();
+ }
+
+ if (phoenixSyncService != null) {
+ phoenixSyncService.close();
+ }
+ }
+}
diff --git a/client-adapter/phoenix/src/main/java/com/alibaba/otter/canal/client/adapter/phoenix/config/ConfigLoader.java b/client-adapter/phoenix/src/main/java/com/alibaba/otter/canal/client/adapter/phoenix/config/ConfigLoader.java
new file mode 100644
index 00000000..f5c753b3
--- /dev/null
+++ b/client-adapter/phoenix/src/main/java/com/alibaba/otter/canal/client/adapter/phoenix/config/ConfigLoader.java
@@ -0,0 +1,48 @@
+package com.alibaba.otter.canal.client.adapter.phoenix.config;
+
+import com.alibaba.otter.canal.client.adapter.config.YmlConfigBinder;
+import com.alibaba.otter.canal.client.adapter.support.MappingConfigsLoader;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Properties;
+
+/**
+ * Phoenix表映射配置加载器
+ */
+public class ConfigLoader {
+
+ private static Logger logger = LoggerFactory.getLogger(ConfigLoader.class);
+
+ /**
+ * 加载Phoenix表映射配置
+ *
+ * @return 配置名/配置文件名--对象
+ */
+ public static Map load(Properties envProperties) {
+ logger.info("## Start loading phoenix mapping config ... ");
+
+ Map result = new LinkedHashMap<>();
+
+ Map configContentMap = MappingConfigsLoader.loadConfigs("phoenix");
+ configContentMap.forEach((fileName, content) -> {
+ MappingConfig config = YmlConfigBinder
+ .bindYmlToObj(null, content, MappingConfig.class, null, envProperties);
+ if (config == null) {
+ return;
+ }
+ try {
+ config.validate();
+ } catch (Exception e) {
+ throw new RuntimeException("ERROR Config: " + fileName + " " + e.getMessage(), e);
+ }
+ result.put(fileName, config);
+ });
+
+ logger.info("## Phoenix mapping config loaded");
+ logger.info("## Phoenix sync threads: " + ConfigurationManager.getInteger("threads"));
+ return result;
+ }
+}
diff --git a/client-adapter/phoenix/src/main/java/com/alibaba/otter/canal/client/adapter/phoenix/config/ConfigurationManager.java b/client-adapter/phoenix/src/main/java/com/alibaba/otter/canal/client/adapter/phoenix/config/ConfigurationManager.java
new file mode 100644
index 00000000..3f905b07
--- /dev/null
+++ b/client-adapter/phoenix/src/main/java/com/alibaba/otter/canal/client/adapter/phoenix/config/ConfigurationManager.java
@@ -0,0 +1,49 @@
+package com.alibaba.otter.canal.client.adapter.phoenix.config;
+
+import java.io.InputStream;
+import java.util.Properties;
+
+/**
+ * 配置管理组件
+ * @author Administrator
+ *
+ */
+public class ConfigurationManager {
+
+ private static Properties prop = new Properties();
+
+ static {
+ try {
+ InputStream in = ConfigurationManager.class
+ .getClassLoader().getResourceAsStream("phoenix/phoenix_common.properties");
+ prop.load(in);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * 获取指定key对应的value
+ *
+ * @param key
+ * @return 返回value是字符串
+ */
+ public static String getProperty(String key) {
+ return prop.getProperty(key);
+ }
+
+ /**
+ * 获取整数类型的配置项
+ * @param key StringKye
+ * @return value
+ */
+ public static Integer getInteger(String key) {
+ String value = getProperty(key);
+ try {
+ return Integer.valueOf(value);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ return 0;
+ }
+}
diff --git a/client-adapter/phoenix/src/main/java/com/alibaba/otter/canal/client/adapter/phoenix/config/MappingConfig.java b/client-adapter/phoenix/src/main/java/com/alibaba/otter/canal/client/adapter/phoenix/config/MappingConfig.java
new file mode 100644
index 00000000..1e3919ee
--- /dev/null
+++ b/client-adapter/phoenix/src/main/java/com/alibaba/otter/canal/client/adapter/phoenix/config/MappingConfig.java
@@ -0,0 +1,288 @@
+package com.alibaba.otter.canal.client.adapter.phoenix.config;
+
+import org.apache.commons.lang.StringUtils;
+
+import java.util.*;
+
+/**
+ * Phoenix表映射配置
+ */
+@SuppressWarnings("unused")
+public class MappingConfig {
+
+ private String dataSourceKey; // 数据源key
+ private String destination; // canal实例或MQ的topic
+ private String groupId; // groupId
+ private String outerAdapterKey; // 对应适配器的key
+ private boolean concurrent = false; // 是否并行同步
+ private DbMapping dbMapping; // db映射配置
+ private boolean debug = false; // 调试
+
+ public String getDataSourceKey() {
+ return dataSourceKey;
+ }
+
+ public void setDataSourceKey(String dataSourceKey) {
+ this.dataSourceKey = dataSourceKey;
+ }
+
+ public String getGroupId() {
+ return groupId;
+ }
+
+ public void setGroupId(String groupId) {
+ this.groupId = groupId;
+ }
+
+ public String getOuterAdapterKey() {
+ return outerAdapterKey;
+ }
+
+ public void setOuterAdapterKey(String outerAdapterKey) {
+ this.outerAdapterKey = outerAdapterKey;
+ }
+
+ public boolean getConcurrent() {
+ return concurrent;
+ }
+
+ public void setConcurrent(boolean concurrent) {
+ this.concurrent = concurrent;
+ }
+
+ public DbMapping getDbMapping() {
+ return dbMapping;
+ }
+
+ public void setDbMapping(DbMapping dbMapping) {
+ this.dbMapping = dbMapping;
+ }
+
+ public String getDestination() {
+ return destination;
+ }
+
+ public void setDestination(String destination) {
+ this.destination = destination;
+ }
+
+ public boolean isDebug() {
+ return debug;
+ }
+
+ public void setDebug(boolean debug) {
+ this.debug = debug;
+ }
+
+ public void validate() {
+ if (dbMapping.database == null || dbMapping.database.isEmpty()) {
+ throw new NullPointerException("dbMapping.database");
+ }
+ if ((dbMapping.table == null || dbMapping.table.isEmpty())) {
+ throw new NullPointerException("dbMapping.table");
+ }
+ if ((dbMapping.targetTable == null || dbMapping.targetTable.isEmpty())) {
+ throw new NullPointerException("dbMapping.targetTable");
+ }
+ }
+
+ public static class DbMapping {
+
+ private String database; // 数据库名或schema名
+ private String table; // 表名
+ private Map targetPk = new LinkedHashMap<>(); // 目标表主键字段
+ private boolean mapAll = true; // 映射所有字段
+
+ private boolean alter = true; // 是否允许修改表
+ private boolean drop = false; // 是否允许删除字段
+ private boolean limit = false; // 是否限制字段长度
+ private boolean skipMissing = false; // 是否跳过丢失的字段
+ private boolean escapeUpper = true; // 字段默认大写加双引号
+
+ private String targetDb; // 目标库名
+ private String targetTable; // 目标表名
+ private Map targetColumns; // 目标表字段映射
+
+ private List excludeColumns; // 不映射的字段
+
+ private String etlCondition; // etl条件sql
+ private int readBatch = 5000;
+ private int commitBatch = 5000; // etl等批量提交大小
+ private Map allMapColumns;
+
+ public String escape(String name) {
+ if (escapeUpper) {
+ return "\"" + name.toUpperCase() + "\"";
+ } else {
+ return name;
+ }
+ }
+
+ public String getDatabase() {
+ return database;
+ }
+
+ public void setDatabase(String database) {
+ this.database = database;
+ }
+
+ public String getTable() {
+ return table;
+ }
+
+ public void setTable(String table) {
+ this.table = table;
+ }
+
+ public Map getTargetPk() {
+ return targetPk;
+ }
+
+ public void setTargetPk(Map targetPk) {
+ this.targetPk = targetPk;
+ }
+
+ public Boolean getMapAll() {
+ return mapAll;
+ }
+
+ public void setMapAll(Boolean mapAll) {
+ this.mapAll = mapAll;
+ }
+
+ public boolean isAlter() {
+ return alter;
+ }
+
+ public void setAlter(boolean alter) {
+ this.alter = alter;
+ }
+
+ public boolean isLimit() {
+ return limit;
+ }
+
+ public void setLimit(boolean limit) {
+ this.limit = limit;
+ }
+
+ public boolean isDrop() {
+ return drop;
+ }
+
+ public void setDrop(boolean drop) {
+ this.drop = drop;
+ }
+
+ public boolean isSkipMissing() {
+ return skipMissing;
+ }
+
+ public void setSkipMissing(boolean skipMissing) {
+ this.skipMissing = skipMissing;
+ }
+
+ public boolean isEscapeUpper() {
+ return escapeUpper;
+ }
+
+ public void setEscapeUpper(boolean escapeUpper) {
+ this.escapeUpper = escapeUpper;
+ }
+
+ public String getTargetDb() {
+ return targetDb;
+ }
+
+ public void setTargetDb(String targetDb) {
+ this.targetDb = targetDb;
+ }
+
+ public String getTargetTable() {
+ return targetTable;
+ }
+
+ public void setTargetTable(String targetTable) {
+ this.targetTable = targetTable;
+ }
+
+ public Map getTargetColumns() {
+ if (targetColumns != null) {
+ targetColumns.forEach((key, value) -> {
+ if (StringUtils.isEmpty(value)) {
+ targetColumns.put(key, key);
+ }
+ });
+ } else {
+ targetColumns = new HashMap<>();
+ }
+ return targetColumns;
+ }
+
+ public void setTargetColumns(Map targetColumns) {
+ this.targetColumns = targetColumns;
+ }
+
+ public void addTargetColumn(String key, String value) {
+ if (targetColumns == null) {
+ targetColumns = new HashMap<>();
+ }
+ targetColumns.put(key, value);
+ if (allMapColumns != null) {
+ allMapColumns.put(key, value);
+ }
+ }
+
+ public void removeTargetColumn(String key) {
+ if (targetColumns != null) {
+ targetColumns.remove(key);
+ }
+ if (allMapColumns != null) {
+ allMapColumns.remove(key);
+ }
+ }
+
+ public List getExcludeColumns() {
+ if (excludeColumns == null) {
+ excludeColumns = new ArrayList<>();
+ }
+ return excludeColumns;
+ }
+
+ public void setExcludeColumns(List excludeColumns) {
+ this.excludeColumns = excludeColumns;
+ }
+
+ public String getEtlCondition() {
+ return etlCondition;
+ }
+
+ public void setEtlCondition(String etlCondition) {
+ this.etlCondition = etlCondition;
+ }
+
+ public int getReadBatch() {
+ return readBatch;
+ }
+
+ public void setReadBatch(int readBatch) {
+ this.readBatch = readBatch;
+ }
+
+ public int getCommitBatch() {
+ return commitBatch;
+ }
+
+ public void setCommitBatch(int commitBatch) {
+ this.commitBatch = commitBatch;
+ }
+
+ public Map getAllMapColumns() {
+ return allMapColumns;
+ }
+
+ public void setAllMapColumns(Map allMapColumns) {
+ this.allMapColumns = allMapColumns;
+ }
+ }
+}
diff --git a/client-adapter/phoenix/src/main/java/com/alibaba/otter/canal/client/adapter/phoenix/monitor/PhoenixConfigMonitor.java b/client-adapter/phoenix/src/main/java/com/alibaba/otter/canal/client/adapter/phoenix/monitor/PhoenixConfigMonitor.java
new file mode 100644
index 00000000..0c784918
--- /dev/null
+++ b/client-adapter/phoenix/src/main/java/com/alibaba/otter/canal/client/adapter/phoenix/monitor/PhoenixConfigMonitor.java
@@ -0,0 +1,167 @@
+package com.alibaba.otter.canal.client.adapter.phoenix.monitor;
+
+import com.alibaba.otter.canal.client.adapter.phoenix.PhoenixAdapter;
+import com.alibaba.otter.canal.client.adapter.phoenix.config.MappingConfig;
+import com.alibaba.otter.canal.client.adapter.config.YmlConfigBinder;
+import com.alibaba.otter.canal.client.adapter.support.MappingConfigsLoader;
+import com.alibaba.otter.canal.client.adapter.support.Util;
+import org.apache.commons.io.filefilter.FileFilterUtils;
+import org.apache.commons.io.monitor.FileAlterationListenerAdaptor;
+import org.apache.commons.io.monitor.FileAlterationMonitor;
+import org.apache.commons.io.monitor.FileAlterationObserver;
+import org.apache.commons.lang.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+
+/**
+ * phoenix config monitor
+ */
+public class PhoenixConfigMonitor {
+
+ private static final Logger logger = LoggerFactory.getLogger(PhoenixConfigMonitor.class);
+
+ private static final String adapterName = "phoenix"; //相应组件名字
+
+ private String key;
+
+ private PhoenixAdapter phoenixAdapter; //相应适配器名实现类
+
+ private Properties envProperties;
+
+ private FileAlterationMonitor fileMonitor;
+
+ public void init(String key, PhoenixAdapter phoenixAdapter, Properties envProperties) {
+ this.key = key;
+ this.phoenixAdapter = phoenixAdapter;
+ this.envProperties = envProperties;
+ File confDir = Util.getConfDirPath(adapterName);
+ try {
+ FileAlterationObserver observer = new FileAlterationObserver(confDir,
+ FileFilterUtils.and(FileFilterUtils.fileFileFilter(), FileFilterUtils.suffixFileFilter("yml")));
+ FileListener listener = new FileListener();
+ observer.addListener(listener);
+ fileMonitor = new FileAlterationMonitor(3000, observer);
+ fileMonitor.start();
+
+ } catch (Exception e) {
+ logger.error(e.getMessage(), e);
+ }
+ }
+
+ public void destroy() {
+ try {
+ fileMonitor.stop();
+ } catch (Exception e) {
+ logger.error(e.getMessage(), e);
+ }
+ }
+
+ private class FileListener extends FileAlterationListenerAdaptor {
+
+ @Override
+ public void onFileCreate(File file) {
+ super.onFileCreate(file);
+ try {
+ // 加载新增的配置文件
+ String configContent = MappingConfigsLoader.loadConfig(adapterName + File.separator + file.getName());
+ MappingConfig config = YmlConfigBinder
+ .bindYmlToObj(null, configContent, MappingConfig.class, null, envProperties);
+ if (config == null) {
+ return;
+ }
+ config.validate();
+ if ((key == null && config.getOuterAdapterKey() == null)
+ || (key != null && key.equals(config.getOuterAdapterKey()))) {
+ addConfigToCache(file, config);
+
+ logger.info("Add a new phoenix mapping config: {} to canal adapter", file.getName());
+ }
+ } catch (Exception e) {
+ logger.error(e.getMessage(), e);
+ }
+ }
+
+ @Override
+ public void onFileChange(File file) {
+ super.onFileChange(file);
+
+ try {
+ if (phoenixAdapter.getPhoenixMapping().containsKey(file.getName())) {
+ // 加载配置文件
+ String configContent = MappingConfigsLoader
+ .loadConfig(adapterName + File.separator + file.getName());
+ if (configContent == null) {
+ onFileDelete(file);
+ return;
+ }
+ MappingConfig config = YmlConfigBinder
+ .bindYmlToObj(null, configContent, MappingConfig.class, null, envProperties);
+ if (config == null) {
+ return;
+ }
+ config.validate();
+ if ((key == null && config.getOuterAdapterKey() == null)
+ || (key != null && key.equals(config.getOuterAdapterKey()))) {
+ if (phoenixAdapter.getPhoenixMapping().containsKey(file.getName())) {
+ deleteConfigFromCache(file);
+ }
+ addConfigToCache(file, config);
+ } else {
+ // 不能修改outerAdapterKey
+ throw new RuntimeException("Outer adapter key not allowed modify");
+ }
+ logger.info("Change a phoenix mapping config: {} of canal adapter", file.getName());
+ }
+ } catch (Exception e) {
+ logger.error(e.getMessage(), e);
+ }
+ }
+
+ @Override
+ public void onFileDelete(File file) {
+ super.onFileDelete(file);
+
+ try {
+ if (phoenixAdapter.getPhoenixMapping().containsKey(file.getName())) {
+ deleteConfigFromCache(file);
+
+ logger.info("Delete a phoenix mapping config: {} of canal adapter", file.getName());
+ }
+ } catch (Exception e) {
+ logger.error(e.getMessage(), e);
+ }
+ }
+
+ private void addConfigToCache(File file, MappingConfig mappingConfig) {
+ if (mappingConfig == null || mappingConfig.getDbMapping() == null) {
+ return;
+ }
+ phoenixAdapter.getPhoenixMapping().put(file.getName(), mappingConfig);
+ Map configMap = phoenixAdapter.getMappingConfigCache()
+ .computeIfAbsent(StringUtils.trimToEmpty(mappingConfig.getDestination()) + "_"
+ + mappingConfig.getDbMapping().getDatabase() + "-"
+ + mappingConfig.getDbMapping().getTable().toLowerCase(),
+ k1 -> new HashMap<>());
+ configMap.put(file.getName(), mappingConfig);
+ }
+
+ private void deleteConfigFromCache(File file) {
+ logger.info("deleteConfigFromCache: {}", file.getName());
+ MappingConfig mappingConfig = phoenixAdapter.getPhoenixMapping().remove(file.getName());
+
+ if (mappingConfig == null || mappingConfig.getDbMapping() == null) {
+ return;
+ }
+ for (Map configMap : phoenixAdapter.getMappingConfigCache().values()) {
+ if (configMap != null) {
+ configMap.remove(file.getName());
+ }
+ }
+ }
+ }
+}
diff --git a/client-adapter/phoenix/src/main/java/com/alibaba/otter/canal/client/adapter/phoenix/service/PhoenixEtlService.java b/client-adapter/phoenix/src/main/java/com/alibaba/otter/canal/client/adapter/phoenix/service/PhoenixEtlService.java
new file mode 100644
index 00000000..23fab256
--- /dev/null
+++ b/client-adapter/phoenix/src/main/java/com/alibaba/otter/canal/client/adapter/phoenix/service/PhoenixEtlService.java
@@ -0,0 +1,429 @@
+package com.alibaba.otter.canal.client.adapter.phoenix.service;
+
+import com.alibaba.otter.canal.client.adapter.phoenix.PhoenixAdapter;
+import com.alibaba.otter.canal.client.adapter.phoenix.config.MappingConfig;
+import com.alibaba.otter.canal.client.adapter.phoenix.config.MappingConfig.DbMapping;
+import com.alibaba.otter.canal.client.adapter.phoenix.support.PhoenixSupportUtil;
+import com.alibaba.otter.canal.client.adapter.phoenix.support.SyncUtil;
+import com.alibaba.otter.canal.client.adapter.phoenix.support.TypeUtil;
+import com.alibaba.otter.canal.client.adapter.support.DatasourceConfig;
+import com.alibaba.otter.canal.client.adapter.support.EtlResult;
+import com.alibaba.otter.canal.client.adapter.support.Util;
+import com.google.common.base.Joiner;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.sql.DataSource;
+import java.sql.*;
+import java.util.*;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * Phoenix ETL 操作业务类
+ */
+public class PhoenixEtlService {
+
+ private static final Logger logger = LoggerFactory.getLogger(PhoenixEtlService.class);
+
+ private static String[] splitNotEmpty(String s) {
+ if (s != null && s.trim().length() > 0) {
+ return s.trim().split(",");
+ }
+ return new String[]{};
+ }
+
+
+ static boolean syncSchema(Connection targetDSConnection, MappingConfig config) {
+ DataSource srcDataSource = DatasourceConfig.DATA_SOURCES.get(config.getDataSourceKey());
+ if (srcDataSource == null) {
+ return false;
+ }
+ try {
+ return syncSchema(srcDataSource.getConnection(), targetDSConnection, config);
+ } catch (SQLException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static boolean syncSchema(DataSource srcDS,Connection targetDSConnection, MappingConfig config) {
+ try {
+ return syncSchema(srcDS.getConnection(),targetDSConnection, config);
+ } catch (SQLException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private static boolean syncSchema(Connection srcDS, Connection targetDS, MappingConfig config) {
+ DbMapping dbMapping = config.getDbMapping();
+ if (dbMapping.getMapAll() && dbMapping.isAlter()) { // 检查字段是否缺失
+ Map targetColumnType = new LinkedHashMap<>();
+ String targetTable = SyncUtil.getDbTableName(dbMapping);
+ try {
+ Util.sqlRS(targetDS, "SELECT * FROM " + targetTable + " LIMIT 1", rs -> {
+ try {
+ ResultSetMetaData rsd = rs.getMetaData();
+ int columnCount = rsd.getColumnCount();
+ for (int i = 1; i <= columnCount; i++) {
+ targetColumnType.put(rsd.getColumnName(i).toLowerCase(), rsd.getColumnType(i));
+ }
+ } catch (Exception e) {
+ logger.error(dbMapping.getTable() + " etl failed! ==>" + e.getMessage(), e);
+ }
+ });
+ } catch (RuntimeException e) {
+ if (!e.getCause().getClass().getName().endsWith("TableNotFoundException")) {
+ throw e;
+ }
+ }
+ StringBuilder missing = new StringBuilder();
+ StringBuilder constraint = new StringBuilder();
+ Util.sqlRS(srcDS, "SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = '" + dbMapping.getDatabase() + "' AND TABLE_NAME = '" + dbMapping.getTable() + "'", rs -> {
+ try {
+ List excludeColumns = config.getDbMapping().getExcludeColumns();
+ while (rs.next()) {
+ String name = rs.getString("COLUMN_NAME");
+ String lower = name.toLowerCase();
+ String colType = rs.getString("COLUMN_TYPE");
+ if (targetColumnType.get(lower) == null && !excludeColumns.contains(lower)) {
+ boolean isPri = rs.getString("COLUMN_KEY").equals("PRI");
+ String[] args = splitNotEmpty(colType.replaceAll("^\\w+(?:\\(([^)]*)\\))?[\\s\\S]*$", "$1"));
+ missing.append(dbMapping.escape(name)).append(" ").append(TypeUtil.getPhoenixType(
+ rs.getString("DATA_TYPE").toUpperCase(),
+ args,
+ colType.contains("unsigned"),
+ dbMapping.isLimit()
+ ));
+ if (isPri) {
+ if (args.length > 0 && dbMapping.isLimit() || rs.getString("IS_NULLABLE").equals("NO")) {
+ missing.append(" NOT NULL");
+ }
+ constraint.append(dbMapping.escape(name)).append(',');
+ }
+ missing.append(',');
+ }
+ }
+ } catch (Exception e) {
+ logger.error(dbMapping.getDatabase() + "." + dbMapping.getTable() + " schema failed! ==>" + e.getMessage(), e);
+ throw new RuntimeException(e);
+ }
+ });
+ if (missing.length() > 0) {
+ String sql;
+ if (targetColumnType.isEmpty()) {
+ if (constraint.length() > 0) {
+ constraint.deleteCharAt(constraint.length() - 1);
+ missing.append("CONSTRAINT pk PRIMARY KEY(").append(constraint.toString()).append(")");
+ } else {
+ missing.deleteCharAt(missing.length() - 1);
+ }
+ sql = "CREATE TABLE " + targetTable + " (" + missing.toString() + ")";
+ } else {
+ missing.deleteCharAt(missing.length() - 1);
+ sql = "ALTER TABLE " + targetTable + " ADD " + missing.toString();
+ }
+ logger.info("schema missing: {} {}", targetColumnType, sql);
+ try (PreparedStatement pstmt = targetDS.prepareStatement(sql)) {
+ pstmt.executeUpdate();
+ } catch (SQLException e) {
+ logger.error("sync schema error: " + e.getMessage(), e);
+ }
+ } else {
+ logger.debug("schema ok: {}", targetColumnType);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * 导入数据
+ */
+ public static EtlResult importData(DataSource srcDS, Connection targetDSConnection, MappingConfig config,
+ List params) {
+ EtlResult etlResult = new EtlResult();
+ AtomicLong successCount = new AtomicLong();
+ List errMsg = new ArrayList<>();
+ String hbaseTable = "";
+ try {
+ if (config == null) {
+ logger.error("Config is null!");
+ etlResult.setSucceeded(false);
+ etlResult.setErrorMessage("Config is null!");
+ return etlResult;
+ }
+ boolean debug = params != null && params.get(0).equals("_debug");
+ if (debug) {
+ params = params.subList(1, params.size());
+ }
+ syncSchema(srcDS, targetDSConnection, config);
+ DbMapping dbMapping = config.getDbMapping();
+
+ long start = System.currentTimeMillis();
+
+ // 拼接sql
+ StringBuilder sql = new StringBuilder(
+ "SELECT * FROM " + dbMapping.getDatabase() + "." + dbMapping.getTable());
+
+ // 拼接条件
+ appendCondition(params, dbMapping, srcDS, sql);
+
+ // 获取总数
+ String countSql = "SELECT COUNT(1) FROM ( " + sql + ") _CNT ";
+ long cnt = (Long) Util.sqlRS(srcDS, countSql, rs -> {
+ Long count = null;
+ try {
+ if (rs.next()) {
+ count = ((Number) rs.getObject(1)).longValue();
+ }
+ } catch (Exception e) {
+ logger.error(e.getMessage(), e);
+ }
+ return count == null ? 0 : count;
+ });
+
+ // 当大于1万条记录时开启多线程
+ if (cnt >= 10000) {
+ int threadCount = 3;
+ long perThreadCnt = cnt / threadCount;
+ ExecutorService executor = Util.newFixedThreadPool(threadCount, 5000L);
+ for (int i = 0; i < threadCount; i++) {
+ long offset = i * perThreadCnt;
+ Long size = null;
+ if (i != threadCount - 1) {
+ size = perThreadCnt;
+ }
+ String sqlFinal;
+ if (size != null) {
+ sqlFinal = sql + " LIMIT " + offset + "," + size;
+ } else {
+ sqlFinal = sql + " LIMIT " + offset + "," + cnt;
+ }
+ executor
+ .execute(() -> executeSqlImport(srcDS, targetDSConnection, sqlFinal, dbMapping, successCount, errMsg, debug));
+ }
+
+ executor.shutdown();
+ //noinspection StatementWithEmptyBody
+ while (!executor.awaitTermination(3, TimeUnit.SECONDS)) ;
+ } else {
+ executeSqlImport(srcDS, targetDSConnection, sql.toString(), dbMapping, successCount, errMsg, debug);
+ }
+
+ logger.info(
+ dbMapping.getTable() + " etl completed in: " + (System.currentTimeMillis() - start) / 1000 + "s!");
+
+ etlResult
+ .setResultMessage("导入目标表 " + SyncUtil.getDbTableName(dbMapping) + " 数据:" + successCount.get() + " 条");
+ } catch (Exception e) {
+ logger.error(e.getMessage(), e);
+ errMsg.add(hbaseTable + " etl failed! ==>" + e.getMessage());
+ }
+
+ if (errMsg.isEmpty()) {
+ etlResult.setSucceeded(true);
+ } else {
+ etlResult.setErrorMessage(Joiner.on("\n").join(errMsg));
+ }
+ return etlResult;
+ }
+
+ private static void appendCondition(List params, DbMapping dbMapping, DataSource ds,
+ StringBuilder sql) {
+ if (params != null && params.size() == 1 && dbMapping.getEtlCondition() == null) {
+ AtomicBoolean stExists = new AtomicBoolean(false);
+ // 验证是否有SYS_TIME字段
+ Util.sqlRS(ds, sql.toString(), rs -> {
+ try {
+ ResultSetMetaData rsmd = rs.getMetaData();
+ int cnt = rsmd.getColumnCount();
+ for (int i = 1; i <= cnt; i++) {
+ String columnName = rsmd.getColumnName(i);
+ if ("SYS_TIME".equalsIgnoreCase(columnName)) {
+ stExists.set(true);
+ break;
+ }
+ }
+ } catch (Exception e) {
+ // ignore
+ }
+ return null;
+ });
+ if (stExists.get()) {
+ sql.append(" WHERE SYS_TIME >= '").append(params.get(0)).append("' ");
+ }
+ } else if (dbMapping.getEtlCondition() != null && params != null && params.size() > 0) {
+ String etlCondition = dbMapping.getEtlCondition();
+ int size = params.size();
+ for (int i = 0; i < size; i++) {
+ etlCondition = etlCondition.replace("{" + i + "}", params.get(i));
+ }
+
+ sql.append(" ").append(etlCondition);
+ }
+ }
+
+ /**
+ * 执行导入
+ */
+ private static boolean executeSqlImport(DataSource srcDS, Connection targetDSConnection, String sql, DbMapping dbMapping,
+ AtomicLong successCount, List errMsg, boolean debug) {
+ try {
+ Map columnsMap = new LinkedHashMap<>();
+ Map columnType = new LinkedHashMap<>();
+
+
+ PhoenixSupportUtil.sqlRS(targetDSConnection, "SELECT * FROM " + SyncUtil.getDbTableName(dbMapping) + " LIMIT 1 ", rs -> {
+ try {
+
+ ResultSetMetaData rsd = rs.getMetaData();
+ int columnCount = rsd.getColumnCount();
+ List columns = new ArrayList<>();
+ List excludeColumns = dbMapping.getExcludeColumns();
+ for (int i = 1; i <= columnCount; i++) {
+ String lower = rsd.getColumnName(i).toLowerCase();
+ if (!excludeColumns.contains(lower)) {
+ columnType.put(lower, rsd.getColumnType(i));
+ columns.add(lower);
+ }
+ }
+ columnsMap.putAll(SyncUtil.getColumnsMap(dbMapping, columns));
+ return true;
+ } catch (Exception e) {
+ logger.error(e.getMessage(), e);
+ return false;
+ }
+ });
+ Util.sqlRS(srcDS, sql, rs -> {
+ int idx = 1;
+
+ try {
+ boolean completed = false;
+
+ // if (dbMapping.isMapAll()) {
+ // columnsMap = dbMapping.getAllColumns();
+ // } else {
+ // columnsMap = dbMapping.getTargetColumns();
+ // }
+
+ StringBuilder insertSql = new StringBuilder();
+ insertSql.append("UPSERT INTO ").append(SyncUtil.getDbTableName(dbMapping)).append(" (");
+ columnsMap
+ .forEach((targetColumnName, srcColumnName) -> insertSql.append(dbMapping.escape(targetColumnName)).append(","));
+
+ int len = insertSql.length();
+ insertSql.delete(len - 1, len).append(") VALUES (");
+ int mapLen = columnsMap.size();
+ for (int i = 0; i < mapLen; i++) {
+ insertSql.append("?,");
+ }
+ len = insertSql.length();
+ insertSql.delete(len - 1, len).append(")");
+ try (
+ //Connection connTarget = targetDS.getConnection();
+ Connection connTarget =PhoenixAdapter.getPhoenixConnection();
+ PreparedStatement pstmt = connTarget.prepareStatement(insertSql.toString())) {
+ connTarget.setAutoCommit(false);
+
+ while (rs.next()) {
+ completed = false;
+
+ pstmt.clearParameters();
+
+ // 删除数据
+ Map values = new LinkedHashMap<>();
+ StringBuilder deleteSql = new StringBuilder(
+ "DELETE FROM " + SyncUtil.getDbTableName(dbMapping) + " WHERE ");
+ appendCondition(dbMapping, deleteSql, values, rs);
+ try (PreparedStatement pstmt2 = connTarget.prepareStatement(deleteSql.toString())) {
+ int k = 1;
+ for (Object val : values.values()) {
+ pstmt2.setObject(k++, val);
+ }
+ pstmt2.execute();
+ }
+
+ Map insertValues = new HashMap<>();
+ int i = 1;
+ for (Map.Entry entry : columnsMap.entrySet()) {
+ String targetClolumnName = entry.getKey();
+ String srcColumnName = entry.getValue();
+ if (srcColumnName == null) {
+ srcColumnName = targetClolumnName;
+ }
+
+ Integer type = columnType.get(targetClolumnName.toLowerCase());
+
+ try {
+ Object value = rs.getObject(srcColumnName);
+ insertValues.put(srcColumnName, value);
+ if (value != null) {
+ SyncUtil.setPStmt(type, pstmt, value, i);
+ } else {
+ pstmt.setNull(i, type);
+ }
+ } catch (SQLException e) {
+ insertValues.put(srcColumnName, null);
+ pstmt.setNull(i, type);
+ }
+ i++;
+ }
+ if (debug) {
+ logger.info("insert sql: {} {} {}", insertSql, insertValues, pstmt);
+ }
+
+ pstmt.execute();
+ if (logger.isTraceEnabled()) {
+ logger.trace("Insert into target table, sql: {}", insertSql);
+ }
+
+ if (idx % dbMapping.getCommitBatch() == 0) {
+ connTarget.commit();
+ completed = true;
+ }
+ idx++;
+ successCount.incrementAndGet();
+ if (logger.isDebugEnabled()) {
+ logger.debug("successful import count:" + successCount.get());
+ }
+ }
+ if (!completed) {
+ connTarget.commit();
+ }
+ }
+
+ } catch (Exception e) {
+ logger.error(dbMapping.getTable() + " etl failed! ==>" + e.getMessage(), e);
+ errMsg.add(dbMapping.getTable() + " etl failed! ==>" + e.getMessage());
+ }
+ return idx;
+ });
+ return true;
+ } catch (Exception e) {
+ logger.error(e.getMessage(), e);
+ return false;
+ }
+ }
+
+ /**
+ * 拼接目标表主键where条件
+ */
+ private static void appendCondition(DbMapping dbMapping, StringBuilder sql, Map values,
+ ResultSet rs) throws SQLException {
+ // 拼接主键
+ for (Map.Entry entry : dbMapping.getTargetPk().entrySet()) {
+ String targetColumnName = entry.getKey();
+ String srcColumnName = entry.getValue();
+ if (srcColumnName == null) {
+ srcColumnName = targetColumnName;
+ }
+ sql.append(dbMapping.escape(targetColumnName)).append("=? AND ");
+ values.put(targetColumnName, rs.getObject(srcColumnName));
+ }
+ int len = sql.length();
+ sql.delete(len - 4, len);
+ }
+}
diff --git a/client-adapter/phoenix/src/main/java/com/alibaba/otter/canal/client/adapter/phoenix/service/PhoenixSyncService.java b/client-adapter/phoenix/src/main/java/com/alibaba/otter/canal/client/adapter/phoenix/service/PhoenixSyncService.java
new file mode 100644
index 00000000..cdfd63f1
--- /dev/null
+++ b/client-adapter/phoenix/src/main/java/com/alibaba/otter/canal/client/adapter/phoenix/service/PhoenixSyncService.java
@@ -0,0 +1,644 @@
+package com.alibaba.otter.canal.client.adapter.phoenix.service;
+
+import com.alibaba.druid.sql.SQLUtils;
+import com.alibaba.druid.sql.ast.SQLName;
+import com.alibaba.druid.sql.ast.SQLStatement;
+import com.alibaba.druid.sql.ast.statement.*;
+import com.alibaba.druid.sql.parser.ParserException;
+import com.alibaba.druid.util.JdbcConstants;
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.serializer.SerializerFeature;
+import com.alibaba.otter.canal.client.adapter.phoenix.config.ConfigurationManager;
+import com.alibaba.otter.canal.client.adapter.phoenix.config.MappingConfig;
+import com.alibaba.otter.canal.client.adapter.phoenix.config.MappingConfig.DbMapping;
+import com.alibaba.otter.canal.client.adapter.phoenix.support.BatchExecutor;
+import com.alibaba.otter.canal.client.adapter.phoenix.support.SingleDml;
+import com.alibaba.otter.canal.client.adapter.phoenix.support.SyncUtil;
+import com.alibaba.otter.canal.client.adapter.phoenix.support.TypeUtil;
+import com.alibaba.otter.canal.client.adapter.support.Dml;
+import com.alibaba.otter.canal.client.adapter.support.Util;
+import org.apache.commons.lang.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.sql.Connection;
+import java.sql.ResultSetMetaData;
+import java.sql.SQLException;
+import java.util.*;
+import java.util.concurrent.*;
+import java.util.function.Function;
+
+/**
+ * Phoenix同步操作业务
+ */
+public class PhoenixSyncService {
+
+ private static final Logger logger = LoggerFactory.getLogger(PhoenixSyncService.class);
+
+ // 源库表字段类型缓存: instance.schema.table ->
+ private Map> columnsTypeCache;
+
+ //同步线程数
+ //默认开启3个线程同步,此处配置了自定义同步线程数
+ private int threads = ConfigurationManager.getInteger("threads");
+
+ private List[] dmlsPartition;
+ private BatchExecutor[] batchExecutors;
+ private ExecutorService[] executorThreads;
+
+ public PhoenixSyncService(Integer threads) {
+ this(threads, new ConcurrentHashMap<>());
+ }
+
+
+ @SuppressWarnings("unchecked")
+ private PhoenixSyncService(Integer threads, Map> columnsTypeCache) {
+ this.columnsTypeCache = columnsTypeCache;
+ try {
+ if (threads != null) {
+ this.threads = threads;
+ }
+ this.dmlsPartition = new List[this.threads];
+ this.batchExecutors = new BatchExecutor[this.threads];
+ this.executorThreads = new ExecutorService[this.threads];
+ for (int i = 0; i < this.threads; i++) {
+ dmlsPartition[i] = new ArrayList<>();
+ batchExecutors[i] = new BatchExecutor();
+ //创建单个线程,用来操作一个无界的队列任务,不会使用额外的线程。如果线程崩溃会重新创建一个,直到任务完成。
+ executorThreads[i] = Executors.newSingleThreadExecutor();
+ }
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * 批量同步回调
+ *
+ * @param dmls 批量 DML
+ * @param function 回调方法
+ */
+ private void sync(List dmls, Function function) {
+ try {
+ boolean toExecute = false;
+ for (Dml dml : dmls) {
+ if (!toExecute) {
+ toExecute = function.apply(dml);
+ } else {
+ function.apply(dml);
+ }
+ }
+ if (toExecute) {
+ List> futures = new ArrayList<>();
+ for (int i = 0; i < threads; i++) {
+ int j = i;
+ if (dmlsPartition[j].isEmpty()) {
+ // bypass
+ continue;
+ }
+
+ futures.add(executorThreads[i].submit(() -> {
+ try {
+ dmlsPartition[j].forEach(syncItem -> sync(batchExecutors[j],
+ syncItem.config,
+ syncItem.singleDml));
+ //相对于RDB同步 少了 dmlsPartition[j].clear();
+ //在 try catch中获取异常后再次执行一次batchExecutors[j].commit()
+ batchExecutors[j].commit();
+ return true;
+ } catch (Throwable e) {
+ batchExecutors[j].rollback();
+ if (!e.getClass().getName().endsWith("ColumnNotFoundException")
+ && !e.getClass().getName().endsWith("TableNotFoundException")) {
+ throw new RuntimeException(e);
+ }
+ logger.info("table or column not found: " + e.getMessage());
+ boolean synced = false;
+ for (SyncItem syncItem : dmlsPartition[j]) {
+ if (PhoenixEtlService.syncSchema(batchExecutors[j].getConn(), syncItem.config)) {
+ synced = true;
+ }
+ }
+ if (!synced) {
+ throw new RuntimeException(e);
+ }
+ dmlsPartition[j].forEach(syncItem -> sync(batchExecutors[j],
+ syncItem.config,
+ syncItem.singleDml));
+ try {
+ batchExecutors[j].commit();
+ return true;
+ } catch (Throwable e1) {
+ batchExecutors[j].rollback();
+ throw new RuntimeException(e1);
+ }
+ } finally {
+ dmlsPartition[j].clear();
+ }
+ }));
+ }
+ futures.forEach(future -> {
+ try {
+ future.get();
+ } catch (ExecutionException | InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ });
+ }
+ } finally {
+ for (BatchExecutor batchExecutor : batchExecutors) {
+ if (batchExecutor != null) {
+ batchExecutor.close();
+ }
+ }
+ }
+ }
+
+ /**
+ * 批量同步 :将批量DML进行解析并放入List --> dmlsPartition[hash].add(syncItem);
+ * @param mappingConfig 配置集合
+ * @param dmls 批量 DML
+ */
+ public void sync(Map> mappingConfig, List dmls, Properties envProperties) {
+ sync(dmls, dml -> {
+ String destination = StringUtils.trimToEmpty(dml.getDestination());
+ String groupId = StringUtils.trimToEmpty(dml.getGroupId());
+ String database = dml.getDatabase();
+ String table = dml.getTable().toLowerCase();
+ Map configMap;
+ if (envProperties != null && !"tcp".equalsIgnoreCase(envProperties.getProperty("canal.conf.mode"))) {
+ //tcp 模式
+ configMap = mappingConfig.get(destination + "-" + groupId + "_" + database + "-" + table);
+ } else {
+ //kafka 模式 或者 RocketMQ模式
+ configMap = mappingConfig.get(destination + "_" + database + "-" + table);
+ }
+
+ if (configMap == null) {
+ if (logger.isTraceEnabled()) {
+ logger.trace("no config map: destination={},groupId={}, database={}, table={}, keys={}", destination, groupId, database, table, mappingConfig.keySet());
+ }
+ return false;
+ }
+ if (configMap.values().isEmpty()) {
+ logger.info("config map empty: destination={},groupId={}, database={}, table={}, keys={}", destination, groupId, database, table, mappingConfig.keySet());
+ return false;
+ }
+ if (dml.getIsDdl() != null && dml.getIsDdl() && StringUtils.isNotEmpty(dml.getSql())) {
+ // DDL
+ columnsTypeCache.remove(dml.getDestination() + "." + dml.getDatabase() + "." + dml.getTable());
+ List stmtList;
+ try {
+ stmtList = SQLUtils.parseStatements(dml.getSql(), JdbcConstants.MYSQL, false);
+ } catch (ParserException e) {
+ // 可能存在一些SQL是不支持的,比如存储过程
+ logger.info("parse sql error: " + dml.getSql(), e);
+ return false;
+ }
+ for (Map.Entry entry : configMap.entrySet()) {
+ try {
+ alter(batchExecutors[0], entry.getValue(), dml, stmtList, entry.getKey());
+ } catch (SQLException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ return false;
+ } else {
+ // DML
+ for (Map.Entry entry : configMap.entrySet()) {
+ MappingConfig config = entry.getValue();
+ if (config.isDebug()) {
+ logger.info("DML: {} {}", entry.getKey(), JSON.toJSONString(dml, SerializerFeature.WriteMapNullValue));
+ }
+ if (config.getConcurrent()) {
+ //并行同步
+ //将一批DML转成SingleDml
+ List singleDmls = SingleDml.dml2SingleDmls(dml);
+ singleDmls.forEach(singleDml -> {
+ //取主键hash
+ int hash = pkHash(config.getDbMapping(), singleDml.getData());
+ SyncItem syncItem = new SyncItem(config, singleDml);
+ //相同的主键数据的顺序是可以保证的
+ dmlsPartition[hash].add(syncItem);
+ });
+ } else {
+ //不并行同步
+ int hash = 0;
+ List singleDmls = SingleDml.dml2SingleDmls(dml);
+ singleDmls.forEach(singleDml -> {
+ SyncItem syncItem = new SyncItem(config, singleDml);
+ //这里线程默认是3个,如果不并行,则会出现2个线程空跑
+ dmlsPartition[hash].add(syncItem);
+ });
+ }
+ }
+ return true;
+ }
+ });
+ }
+
+ /**
+ * 单条 dml 同步
+ *
+ * @param batchExecutor 批量事务执行器
+ * @param config 对应配置对象
+ * @param dml DML
+ */
+ private void sync(BatchExecutor batchExecutor, MappingConfig config, SingleDml dml) {
+ if (config != null) {
+ try {
+ String type = dml.getType();
+ if (type != null && type.equalsIgnoreCase("INSERT")) {
+ insert(batchExecutor, config, dml);
+ } else if (type != null && type.equalsIgnoreCase("UPDATE")) {
+ insert(batchExecutor, config, dml);
+ } else if (type != null && type.equalsIgnoreCase("DELETE")) {
+ delete(batchExecutor, config, dml);
+ } else if (type != null && type.equalsIgnoreCase("TRUNCATE")) {
+ truncate(batchExecutor, config);
+ } else if (logger.isInfoEnabled()){
+ logger.info("SingleDml: {}", JSON.toJSONString(dml, SerializerFeature.WriteMapNullValue));
+ }
+ } catch (SQLException e) {
+ logger.error("sync error: " + e.getMessage(), e);
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ private void alter(BatchExecutor batchExecutor, MappingConfig config, Dml dml, List stmtList, String configFile) throws SQLException {
+ if (config.isDebug()) {
+ logger.info("DML: {} {}", configFile, JSON.toJSONString(dml, SerializerFeature.WriteMapNullValue));
+ }
+ DbMapping dbMapping = config.getDbMapping();
+ if (!dbMapping.isAlter()) {
+ logger.info("not alterable table: {} {}", dml.getTable(), configFile);
+ return;
+ }
+
+ Map columnsMap = dbMapping.getTargetColumns();
+
+ Map columnsMap1 = new HashMap<>();
+ for (Map.Entry entry : columnsMap.entrySet()) {
+ columnsMap1.put(entry.getValue(), entry.getKey());
+ }
+
+ String targetTable = SyncUtil.getDbTableName(dbMapping);
+ Map defValues = new HashMap<>();
+ for (SQLStatement statement : stmtList) {
+ if (statement instanceof SQLAlterTableStatement) {
+ SQLAlterTableStatement alterTable = (SQLAlterTableStatement) statement;
+ for (SQLAlterTableItem item : alterTable.getItems()) {
+ if (item instanceof SQLAlterTableDropColumnItem) {
+ SQLAlterTableDropColumnItem dropColumnItem = (SQLAlterTableDropColumnItem) item;
+ if (!dbMapping.isDrop()) {
+ logger.info("drop table column disabled: {} {}", targetTable, dropColumnItem.getColumns());
+ continue;
+ }
+ for (SQLName sqlName : dropColumnItem.getColumns()) {
+ String name = Util.cleanColumn(sqlName.getSimpleName());
+ String sql = "ALTER TABLE " + targetTable + " DROP COLUMN IF EXISTS " +
+ dbMapping.escape(columnsMap1.getOrDefault(name, name));
+ try {
+ logger.info("drop table column: {} {}", sql, batchExecutor.executeUpdate(sql));
+ dbMapping.removeTargetColumn(name);
+ } catch (Exception e) {
+ logger.warn("drop table column error: " + sql, e);
+ }
+ }
+ } else if (item instanceof SQLAlterTableAddColumn) {
+ SQLAlterTableAddColumn addColumn = (SQLAlterTableAddColumn) item;
+ if (!dbMapping.getMapAll()) {
+ logger.info("add table column disabled: {} {}", targetTable, addColumn.getColumns());
+ continue;
+ }
+ for (SQLColumnDefinition definition : addColumn.getColumns()) {
+ String name = Util.cleanColumn(definition.getNameAsString());
+ if (dbMapping.getExcludeColumns().contains(name)) {
+ continue;
+ }
+ String sql = "ALTER TABLE " + targetTable +
+ " ADD IF NOT EXISTS " +
+ dbMapping.escape(name) + " " + TypeUtil.getPhoenixType(definition, dbMapping.isLimit());
+ try {
+ logger.info("add table column: {} {}", sql, batchExecutor.executeUpdate(sql));
+ dbMapping.addTargetColumn(name, name);
+ if (definition.getDefaultExpr() != null) {
+ String defVal = definition.getDefaultExpr().toString();
+ if (!defVal.equalsIgnoreCase("NULL") && !defVal.equalsIgnoreCase("NOT NULL") && name.length() > 0) {
+ defValues.put(name, defVal);
+ }
+ }
+ } catch (Exception e) {
+ logger.error("add table column error: " + sql, e);
+ throw e;
+ }
+ }
+ }
+ }
+ }
+ }
+ if (!defValues.isEmpty()) {
+ StringBuilder defSql = new StringBuilder();
+ defSql.append("UPSERT INTO ").append(targetTable).append("(");
+ Set> pkSet = dbMapping.getTargetPk().entrySet();
+ Set> defSet = defValues.entrySet();
+ for (Map.Entry entry : pkSet) {
+ defSql.append(dbMapping.escape(entry.getKey())).append(",");
+ }
+ for (Map.Entry entry : defSet) {
+ defSql.append(dbMapping.escape(entry.getKey())).append(",");
+ }
+ defSql.deleteCharAt(defSql.length() - 1).append(") SELECT ");
+ for (Map.Entry entry : pkSet) {
+ defSql.append(dbMapping.escape(entry.getKey())).append(",");
+ }
+ for (Map.Entry entry : defSet) {
+ defSql.append(entry.getValue()).append(",");
+ }
+ defSql.deleteCharAt(defSql.length() - 1).append(" FROM ").append(targetTable);
+ try {
+ logger.info("set column default value: {} {}", defSql, batchExecutor.executeUpdate(defSql.toString()));
+ batchExecutor.commit();
+ } catch (SQLException e) {
+ logger.error("set column default value error: {}", defSql, e);
+ batchExecutor.rollback();
+ throw e;
+ }
+ }
+ }
+
+ /**
+ * 插入操作
+ *
+ * @param config 配置项
+ * @param dml DML数据
+ */
+ private void insert(BatchExecutor batchExecutor, MappingConfig config, SingleDml dml) throws SQLException {
+ Map data = dml.getData();
+ if (data == null || data.isEmpty()) {
+ return;
+ }
+
+ DbMapping dbMapping = config.getDbMapping();
+ Map columnsMap = SyncUtil.getColumnsMap(dbMapping, data);
+
+ StringBuilder insertSql = new StringBuilder();
+ insertSql.append("UPSERT INTO ").append(SyncUtil.getDbTableName(dbMapping)).append(" (");
+
+ Map ctype = getTargetColumnType(batchExecutor.getConn(), config);
+
+ int mapLen = columnsMap.size();
+ List