mirror of
https://github.com/alibaba/canal.git
synced 2026-11-04 00:30:15 +08:00
Compare commits
127 Commits
canal-1.1.
...
canal-1.1.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0741ccda1e | ||
|
|
02fffc9e74 | ||
|
|
5ca4c39313 | ||
|
|
3162d7093d | ||
|
|
f350fbdc0a | ||
|
|
2d0dcf0e5d | ||
|
|
515c05c873 | ||
|
|
7ee551cbf7 | ||
|
|
923675aa49 | ||
|
|
ebb541001c | ||
|
|
44ed76ebb8 | ||
|
|
fe836f7515 | ||
|
|
b30544c750 | ||
|
|
66483ef83f | ||
|
|
b391188baf | ||
|
|
2a7a507987 | ||
|
|
f53749bfc5 | ||
|
|
80ca4366db | ||
|
|
be0d945d7d | ||
|
|
5a968b3715 | ||
|
|
1e5b8a2021 | ||
|
|
ff82fd6532 | ||
|
|
b060f46ccf | ||
|
|
7e31b40531 | ||
|
|
b2b3223d1d | ||
|
|
fea4d8210a | ||
|
|
fb3d2c6811 | ||
|
|
3439c48948 | ||
|
|
2b8a309ec7 | ||
|
|
9373429015 | ||
|
|
9493ff733d | ||
|
|
31530dc9af | ||
|
|
b0f3ff8dcc | ||
|
|
5c61f2fb56 | ||
|
|
7bb619cf12 | ||
|
|
d66a15fe35 | ||
|
|
aa05d8e2a0 | ||
|
|
5ff6449620 | ||
|
|
bbea802914 | ||
|
|
e2219c88dd | ||
|
|
a92e61f5a9 | ||
|
|
eaacc01b77 | ||
|
|
caa46f3672 | ||
|
|
c0ff058fe1 | ||
|
|
059f21208d | ||
|
|
8ca900377a | ||
|
|
6dffcc8573 | ||
|
|
711971599f | ||
|
|
87c7a95523 | ||
|
|
e255baff11 | ||
|
|
d457cd835e | ||
|
|
cf9c64629e | ||
|
|
307547746c | ||
|
|
c7d5ed4ba1 | ||
|
|
b9ed5caf05 | ||
|
|
348d218120 | ||
|
|
e2a64daad8 | ||
|
|
1413cc9196 | ||
|
|
04c11ec8eb | ||
|
|
54145ced5e | ||
|
|
fb35dd4254 | ||
|
|
e233bec66a | ||
|
|
67e0928d80 | ||
|
|
93dac2d335 | ||
|
|
9baeff55e4 | ||
|
|
ca79258e4f | ||
|
|
0e4c5c40d7 | ||
|
|
c65477d97a | ||
|
|
fc3cfc5c0c | ||
|
|
b4074206ca | ||
|
|
98677c5bc8 | ||
|
|
2c7e918f30 | ||
|
|
163f719971 | ||
|
|
55a5e7592b | ||
|
|
259008a7cd | ||
|
|
26ba84e27e | ||
|
|
c8ef9552ec | ||
|
|
bff394042a | ||
|
|
b322201889 | ||
|
|
ea8949298f | ||
|
|
ba924b9f1d | ||
|
|
7e0061fe93 | ||
|
|
6440b744f2 | ||
|
|
066b022bb3 | ||
|
|
738f41049f | ||
|
|
0f6b9d156a | ||
|
|
ed754b5203 | ||
|
|
11f89163c1 | ||
|
|
cda330e49e | ||
|
|
f9f810226d | ||
|
|
d4a09704f2 | ||
|
|
34e9440383 | ||
|
|
d623bea71a | ||
|
|
5ea9259f0d | ||
|
|
a216b55010 | ||
|
|
2786e06ccf | ||
|
|
63dfbf65df | ||
|
|
d90066e01b | ||
|
|
69cfbabb10 | ||
|
|
ab30f3bd8d | ||
|
|
681d03fbe7 | ||
|
|
f13cc4ee9f | ||
|
|
252ca0279f | ||
|
|
7241af84f7 | ||
|
|
159457cccd | ||
|
|
d8a70d0c2f | ||
|
|
ec247114fd | ||
|
|
f067f761c7 | ||
|
|
d4c8780f26 | ||
|
|
894ef6e610 | ||
|
|
cc75d3dda4 | ||
|
|
d8169cbf71 | ||
|
|
fccc6bc816 | ||
|
|
97aa441bca | ||
|
|
b8c2a757a0 | ||
|
|
c8de48bbd5 | ||
|
|
08239ef23f | ||
|
|
04a2b6f941 | ||
|
|
5f667c3574 | ||
|
|
bfb8cf4190 | ||
|
|
18c38ee057 | ||
|
|
82b3aed62b | ||
|
|
a04a1912a8 | ||
|
|
5d7d19fdb1 | ||
|
|
9edbe7e827 | ||
|
|
b52c5d149b | ||
|
|
aada80ba38 |
@@ -4,6 +4,8 @@
|
||||

|
||||
[](http://isitmaintained.com/project/alibaba/canal "average time to resolve an issue")
|
||||
[](http://isitmaintained.com/project/alibaba/canal "percentage of issues still open")
|
||||
[](https://opensource.alibaba.com/contribution_leaderboard/details?projectValue=canal)
|
||||
|
||||
|
||||
## 简介
|
||||
|
||||
@@ -91,6 +93,10 @@ canal 作为 MySQL binlog 增量获取和解析工具,可将变更记录投递
|
||||
|
||||
- 参考文档: [Canal Kafka/RocketMQ QuickStart](https://github.com/alibaba/canal/wiki/Canal-Kafka-RocketMQ-QuickStart)
|
||||
|
||||
## 基于canal开发的工具
|
||||
|
||||
- canal2sql(基于binlog生成SQL) : [https://github.com/zhuchao941/canal2sql]
|
||||
|
||||
## 相关开源&产品
|
||||
|
||||
- [canal 消费端开源项目: Otter](http://github.com/alibaba/otter)
|
||||
@@ -101,3 +107,5 @@ canal 作为 MySQL binlog 增量获取和解析工具,可将变更记录投递
|
||||
|
||||
## 问题反馈
|
||||
- 报告 issue: [github issues](https://github.com/alibaba/canal/issues)
|
||||
|
||||
本项目的Issues会被同步沉淀至[阿里云开发者社区](https://developer.aliyun.com/ask)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<artifactId>canal.admin</artifactId>
|
||||
<groupId>com.alibaba.otter</groupId>
|
||||
<version>1.1.7-SNAPSHOT</version>
|
||||
<version>1.1.8</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
|
||||
@@ -66,8 +66,8 @@ export default {
|
||||
}
|
||||
}
|
||||
const validatePassword = (rule, value, callback) => {
|
||||
if (value.length < 6) {
|
||||
callback(new Error('The password can not be less than 6 digits'))
|
||||
if (value.length < 5) {
|
||||
callback(new Error('The password can not be less than 5 digits'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<artifactId>canal.admin</artifactId>
|
||||
<groupId>com.alibaba.otter</groupId>
|
||||
<version>1.1.7-SNAPSHOT</version>
|
||||
<version>1.1.8</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>canal.admin-web</artifactId>
|
||||
@@ -30,6 +30,8 @@
|
||||
<dependency>
|
||||
<groupId>mysql</groupId>
|
||||
<artifactId>mysql-connector-java</artifactId>
|
||||
<!-- 支持arm64需要mysql 8.0,升级为8.0的新版驱动 -->
|
||||
<version>8.0.33</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.ben-manes.caffeine</groupId>
|
||||
|
||||
@@ -59,7 +59,7 @@ esac
|
||||
JavaVersion=`$JAVA -version 2>&1 |awk 'NR==1{ gsub(/"/,""); print $3 }' | awk -F '.' '{print $1}'`
|
||||
str=`file -L $JAVA | grep 64-bit`
|
||||
|
||||
JAVA_OPTS="$JAVA_OPTS -Xss256k -XX:+AggressiveOpts -XX:-UseBiasedLocking -XX:-OmitStackTraceInFastThrow -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=$base/logs"
|
||||
JAVA_OPTS="$JAVA_OPTS -Xss1m -XX:+AggressiveOpts -XX:-UseBiasedLocking -XX:-OmitStackTraceInFastThrow -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=$base/logs"
|
||||
if [ $JavaVersion -ge 11 ] ; then
|
||||
#JAVA_OPTS="$JAVA_OPTS -Xlog:gc*:$base_log/gc.log:time "
|
||||
JAVA_OPTS="$JAVA_OPTS"
|
||||
|
||||
@@ -10,6 +10,7 @@ import java.util.List;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ import java.io.PrintWriter;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
@@ -26,8 +28,24 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
@Configuration
|
||||
public class WebConfig implements WebMvcConfigurer {
|
||||
|
||||
@Value(value = "${canal.adminUser}")
|
||||
private String user;
|
||||
|
||||
@Value(value = "${canal.adminPasswd}")
|
||||
private String passwd;
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
if (StringUtils.isEmpty(user)) {
|
||||
throw new IllegalArgumentException(
|
||||
"canal.adminUser is empty , pls check https://github.com/alibaba/canal/issues/4941");
|
||||
}
|
||||
|
||||
if (StringUtils.isEmpty(passwd)) {
|
||||
throw new IllegalArgumentException(
|
||||
"canal.adminPasswd is empty , pls check https://github.com/alibaba/canal/issues/4941");
|
||||
}
|
||||
|
||||
registry.addInterceptor(new HandlerInterceptor() {
|
||||
|
||||
@Override
|
||||
|
||||
@@ -38,12 +38,12 @@ public class SimpleAdminConnectors {
|
||||
}
|
||||
|
||||
private static String getDesensitizationPassword(String password) {
|
||||
String defaultPassword = "******";
|
||||
String defaultPassword = "*****";
|
||||
|
||||
if (Strings.isNullOrEmpty(password) || password.length() < 6) {
|
||||
if (Strings.isNullOrEmpty(password) || password.length() < 5) {
|
||||
return defaultPassword;
|
||||
} else {
|
||||
return String.format("%s******", password.substring(0, 3));
|
||||
return String.format("%s*****", password.substring(0, 3));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,15 +2,11 @@ package com.alibaba.otter.canal.admin.controller;
|
||||
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import org.apache.commons.lang.RandomStringUtils;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import com.alibaba.otter.canal.admin.model.BaseModel;
|
||||
import com.alibaba.otter.canal.admin.model.CanalConfig;
|
||||
@@ -28,7 +24,7 @@ import com.alibaba.otter.canal.protocol.SecurityUtil;
|
||||
@RequestMapping("/api/{env}/config")
|
||||
public class PollingConfigController {
|
||||
|
||||
private static final byte[] seeds = "canal is best!".getBytes();
|
||||
private static final byte[] seeds = RandomStringUtils.random(16).getBytes();
|
||||
|
||||
@Value(value = "${canal.adminUser}")
|
||||
String user;
|
||||
|
||||
@@ -21,7 +21,7 @@ public class UserServiceImpl implements UserService {
|
||||
|
||||
private static byte[] seeds = "canal is best!".getBytes();
|
||||
|
||||
private static final Integer PASSWORD_LENGTH = 6;
|
||||
private static final Integer PASSWORD_LENGTH = 5;
|
||||
|
||||
public User find4Login(String username, String password) {
|
||||
if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) {
|
||||
|
||||
@@ -18,4 +18,4 @@ spring.datasource:
|
||||
|
||||
canal:
|
||||
adminUser: admin
|
||||
adminPasswd: admin
|
||||
adminPasswd:
|
||||
|
||||
@@ -11,8 +11,8 @@ SET FOREIGN_KEY_CHECKS = 0;
|
||||
DROP TABLE IF EXISTS `canal_adapter_config`;
|
||||
CREATE TABLE `canal_adapter_config` (
|
||||
`id` bigint(20) NOT NULL AUTO_INCREMENT,
|
||||
`category` varchar(45) NOT NULL,
|
||||
`name` varchar(45) NOT NULL,
|
||||
`category` varchar(255) NOT NULL,
|
||||
`name` varchar(255) NOT NULL,
|
||||
`status` varchar(45) DEFAULT NULL,
|
||||
`content` text NOT NULL,
|
||||
`modified_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
@@ -25,7 +25,7 @@ CREATE TABLE `canal_adapter_config` (
|
||||
DROP TABLE IF EXISTS `canal_cluster`;
|
||||
CREATE TABLE `canal_cluster` (
|
||||
`id` bigint(20) NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(63) NOT NULL,
|
||||
`name` varchar(255) NOT NULL,
|
||||
`zk_hosts` varchar(255) NOT NULL,
|
||||
`modified_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`)
|
||||
@@ -39,7 +39,7 @@ CREATE TABLE `canal_config` (
|
||||
`id` bigint(20) NOT NULL AUTO_INCREMENT,
|
||||
`cluster_id` bigint(20) DEFAULT NULL,
|
||||
`server_id` bigint(20) DEFAULT NULL,
|
||||
`name` varchar(45) NOT NULL,
|
||||
`name` varchar(255) NOT NULL,
|
||||
`status` varchar(45) DEFAULT NULL,
|
||||
`content` text NOT NULL,
|
||||
`content_md5` varchar(128) NOT NULL,
|
||||
@@ -56,7 +56,7 @@ CREATE TABLE `canal_instance_config` (
|
||||
`id` bigint(20) NOT NULL AUTO_INCREMENT,
|
||||
`cluster_id` bigint(20) DEFAULT NULL,
|
||||
`server_id` bigint(20) DEFAULT NULL,
|
||||
`name` varchar(45) NOT NULL,
|
||||
`name` varchar(255) NOT NULL,
|
||||
`status` varchar(45) DEFAULT NULL,
|
||||
`content` text NOT NULL,
|
||||
`content_md5` varchar(128) DEFAULT NULL,
|
||||
@@ -72,8 +72,8 @@ DROP TABLE IF EXISTS `canal_node_server`;
|
||||
CREATE TABLE `canal_node_server` (
|
||||
`id` bigint(20) NOT NULL AUTO_INCREMENT,
|
||||
`cluster_id` bigint(20) DEFAULT NULL,
|
||||
`name` varchar(63) NOT NULL,
|
||||
`ip` varchar(63) NOT NULL,
|
||||
`name` varchar(255) NOT NULL,
|
||||
`ip` varchar(255) NOT NULL,
|
||||
`admin_port` int(11) DEFAULT NULL,
|
||||
`tcp_port` int(11) DEFAULT NULL,
|
||||
`metric_port` int(11) DEFAULT NULL,
|
||||
@@ -88,10 +88,10 @@ CREATE TABLE `canal_node_server` (
|
||||
DROP TABLE IF EXISTS `canal_user`;
|
||||
CREATE TABLE `canal_user` (
|
||||
`id` bigint(20) NOT NULL AUTO_INCREMENT,
|
||||
`username` varchar(31) NOT NULL,
|
||||
`password` varchar(128) NOT NULL,
|
||||
`name` varchar(31) NOT NULL,
|
||||
`roles` varchar(31) NOT NULL,
|
||||
`username` varchar(255) NOT NULL,
|
||||
`password` varchar(255) NOT NULL,
|
||||
`name` varchar(255) NOT NULL,
|
||||
`roles` varchar(255) NOT NULL,
|
||||
`introduction` varchar(255) DEFAULT NULL,
|
||||
`avatar` varchar(255) DEFAULT NULL,
|
||||
`creation_date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<groupId>com.alibaba.otter</groupId>
|
||||
<artifactId>canal</artifactId>
|
||||
<version>1.1.8</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>com.alibaba.otter</groupId>
|
||||
<artifactId>canal.admin</artifactId>
|
||||
<version>1.1.7-SNAPSHOT</version>
|
||||
<version>1.1.8</version>
|
||||
<packaging>pom</packaging>
|
||||
<name>canal admin module for otter ${project.version}</name>
|
||||
<modules>
|
||||
@@ -18,7 +24,6 @@
|
||||
<java_source_version>1.8</java_source_version>
|
||||
<java_target_version>1.8</java_target_version>
|
||||
<file_encoding>UTF-8</file_encoding>
|
||||
<log4j_version>2.17.0</log4j_version>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
@@ -26,86 +31,29 @@
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-context</artifactId>
|
||||
<version>2.0.0.RELEASE</version>
|
||||
<version>3.0.6</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-core</artifactId>
|
||||
<version>5.0.5.RELEASE</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-aop</artifactId>
|
||||
<version>5.0.5.RELEASE</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-context</artifactId>
|
||||
<version>5.0.5.RELEASE</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-jdbc</artifactId>
|
||||
<version>5.0.5.RELEASE</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-orm</artifactId>
|
||||
<version>5.0.5.RELEASE</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-test</artifactId>
|
||||
<version>5.0.5.RELEASE</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-dependencies</artifactId>
|
||||
<version>2.0.1.RELEASE</version>
|
||||
<version>2.5.15</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>mysql</groupId>
|
||||
<artifactId>mysql-connector-java</artifactId>
|
||||
<version>5.1.48</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.ben-manes.caffeine</groupId>
|
||||
<artifactId>caffeine</artifactId>
|
||||
<version>2.6.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-beanutils</groupId>
|
||||
<artifactId>commons-beanutils</artifactId>
|
||||
<version>1.8.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-lang</groupId>
|
||||
<artifactId>commons-lang</artifactId>
|
||||
<version>2.6</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean</artifactId>
|
||||
<version>11.41.1</version>
|
||||
<version>11.45.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.logging.log4j</groupId>
|
||||
<artifactId>log4j-core</artifactId>
|
||||
<version>${log4j_version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.logging.log4j</groupId>
|
||||
<artifactId>log4j-api</artifactId>
|
||||
<version>${log4j_version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.logging.log4j</groupId>
|
||||
<artifactId>log4j-to-slf4j</artifactId>
|
||||
<version>${log4j_version}</version>
|
||||
<groupId>org.yaml</groupId>
|
||||
<artifactId>snakeyaml</artifactId>
|
||||
<version>2.0</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
160
charts/README.md
Normal file
160
charts/README.md
Normal file
@@ -0,0 +1,160 @@
|
||||
# helm charts 使用说明
|
||||
|
||||
包含`canal-admin`和`canal-server`两个chart,基本配置保存在k8s的`secret`里,`canal.properties`和`instance.properties`保存在mysql数据库中,使用zookeeper注册中心实现高可用。
|
||||
|
||||
- [helm charts 使用说明](#helm-charts-使用说明)
|
||||
- [创建zk集群](#创建zk集群)
|
||||
- [初始化数据库](#初始化数据库)
|
||||
- [部署canal-admin chart](#部署canal-admin-chart)
|
||||
- [配置values.yaml](#配置valuesyaml)
|
||||
- [配置集群信息](#配置集群信息)
|
||||
- [部署canal-server chart](#部署canal-server-chart)
|
||||
- [配置values.yaml](#配置valuesyaml-1)
|
||||
- [instance 实例创建](#instance-实例创建)
|
||||
|
||||
## 创建zk集群
|
||||
|
||||
```sh
|
||||
helm install canal-zookeeper oci://registry-1.docker.io/bitnamicharts/zookeeper
|
||||
```
|
||||
|
||||
如需挂载磁盘参考 [bitnami/zookeeper](`https://github.com/bitnami/charts/tree/main/bitnami/zookeeper`)
|
||||
|
||||
|
||||
## 初始化数据库
|
||||
|
||||
[/admin/admin-web/src/main/resources/canal_manager.sql](/admin/admin-web/src/main/resources/canal_manager.sql)
|
||||
|
||||
[/deployer/src/main/resources/spring/tsdb/sql/create_table.sql](/deployer/src/main/resources/spring/tsdb/sql/create_table.sql)
|
||||
|
||||
## 部署canal-admin chart
|
||||
|
||||
### 配置values.yaml
|
||||
|
||||
```yaml
|
||||
# 主要配置
|
||||
admin:
|
||||
config: |
|
||||
server:
|
||||
port: 8089
|
||||
spring:
|
||||
jackson:
|
||||
date-format: yyyy-MM-dd HH:mm:ss
|
||||
time-zone: GMT+8
|
||||
spring.datasource:
|
||||
address: your_db_host:3306
|
||||
database: canal_manager
|
||||
username: ****
|
||||
password: ****
|
||||
driver-class-name: com.mysql.jdbc.Driver
|
||||
url: jdbc:mysql://${spring.datasource.address}/${spring.datasource.database}?useUnicode=true&characterEncoding=UTF-8&useSSL=false&allowPublicKeyRetrieval=true
|
||||
hikari:
|
||||
maximum-pool-size: 30
|
||||
minimum-idle: 1
|
||||
canal:
|
||||
adminUser: admin
|
||||
adminPasswd: admin
|
||||
# debug: true
|
||||
```
|
||||
|
||||
全量参数见:[canal-admin/values.yaml](./canal-admin/values.yaml),测试用例见:[tests/admin-values.yaml](./tests/admin-values.yaml)
|
||||
|
||||
```sh
|
||||
helm install canal-admin -f ./admin-values.yaml ./canal-admin
|
||||
```
|
||||
|
||||
### 配置集群信息
|
||||
|
||||

|
||||
|
||||
等待pod启动后,使用默认账户`admin`密码`123456`登录
|
||||
|
||||
创建集群`default`并配置`zk地址`为`canal-zookeeper.default.svc.cluster.local:2181`
|
||||
|
||||
配置完成后,点击`配置`-`主配置`,修改以下配置项,不是覆盖整个文件
|
||||
|
||||
```yaml
|
||||
canal.user = admin
|
||||
canal.passwd = 4ACFE3202A5FF5CF467898FC58AAB1D615029441
|
||||
|
||||
canal.zkServers = canal-zookeeper.default.svc.cluster.local:2181
|
||||
# tcp, kafka, rocketMQ, rabbitMQ, pulsarMQ 看实际需求,此处改为了rabbitMQ
|
||||
canal.serverMode = rabbitMQ
|
||||
|
||||
#canal.instance.tsdb.dir = ${canal.file.data.dir:../conf}/${canal.instance.destination:}
|
||||
canal.instance.tsdb.url = jdbc:mysql://you_db_host:3306/canal_tsdb?useUnicode=true&characterEncoding=UTF-8&useSSL=false
|
||||
canal.instance.tsdb.dbUsername = ****
|
||||
canal.instance.tsdb.dbPassword = ****
|
||||
|
||||
#canal.instance.tsdb.spring.xml = classpath:spring/tsdb/h2-tsdb.xml
|
||||
canal.instance.tsdb.spring.xml = classpath:spring/tsdb/mysql-tsdb.xml
|
||||
|
||||
#canal.instance.global.spring.xml = classpath:spring/file-instance.xml
|
||||
canal.instance.global.spring.xml = classpath:spring/default-instance.xml
|
||||
|
||||
# 如果用的是阿里云polardb需要配置
|
||||
canal.aliyun.accessKey = ****
|
||||
canal.aliyun.secretKey = ****
|
||||
canal.aliyun.uid = 主账号id
|
||||
|
||||
# 我们是推送到rabbitmq所以需要配置
|
||||
rabbitmq.host = ****:5672
|
||||
rabbitmq.virtual.host = ****
|
||||
rabbitmq.exchange = ****
|
||||
# canal.aliyun.accessKey
|
||||
rabbitmq.username = ****
|
||||
# canal.aliyun.secretKey
|
||||
rabbitmq.password = ****
|
||||
rabbitmq.deliveryMode = direct
|
||||
```
|
||||
|
||||
## 部署canal-server chart
|
||||
|
||||
### 配置values.yaml
|
||||
|
||||
```yaml
|
||||
# 主要配置
|
||||
server:
|
||||
config: |
|
||||
canal.port = 11111
|
||||
canal.metrics.pull.port = 11112
|
||||
|
||||
# register ip
|
||||
canal.register.ip =
|
||||
|
||||
# canal admin config
|
||||
canal.admin.manager = canal-admin.default:8089
|
||||
canal.admin.port = 11110
|
||||
canal.admin.user = admin
|
||||
canal.admin.passwd = 4ACFE3202A5FF5CF467898FC58AAB1D615029441
|
||||
# admin auto register
|
||||
canal.admin.register.auto = true
|
||||
canal.admin.register.cluster = default
|
||||
```
|
||||
|
||||
```sh
|
||||
helm install canal-server -f ./server-values.yaml ./canal-server
|
||||
```
|
||||
|
||||
全量参数见:[canal-server/values.yaml](./canal-server/values.yaml),测试用例见:[tests/server-values.yaml](./tests/server-values.yaml)
|
||||
|
||||

|
||||
|
||||
至此,canal-server已自动创建,并自动注册。
|
||||
|
||||
## instance 实例创建
|
||||
|
||||
主要配置如下
|
||||
|
||||
```yaml
|
||||
# 配置你要监听的数据库
|
||||
canal.instance.master.address=127.0.0.1:3306
|
||||
canal.instance.dbUsername=****
|
||||
canal.instance.dbPassword=****
|
||||
|
||||
# 过滤监听哪些表
|
||||
canal.instance.filter.regex=.*\\..*
|
||||
|
||||
# 如果是推送到rabbitmq,需要配置 Routing Key
|
||||
canal.mq.topic=你的Routing Key
|
||||
```
|
||||
23
charts/canal-admin/.helmignore
Normal file
23
charts/canal-admin/.helmignore
Normal file
@@ -0,0 +1,23 @@
|
||||
# Patterns to ignore when building packages.
|
||||
# This supports shell glob matching, relative path matching, and
|
||||
# negation (prefixed with !). Only one pattern per line.
|
||||
.DS_Store
|
||||
# Common VCS dirs
|
||||
.git/
|
||||
.gitignore
|
||||
.bzr/
|
||||
.bzrignore
|
||||
.hg/
|
||||
.hgignore
|
||||
.svn/
|
||||
# Common backup files
|
||||
*.swp
|
||||
*.bak
|
||||
*.tmp
|
||||
*.orig
|
||||
*~
|
||||
# Various IDEs
|
||||
.project
|
||||
.idea/
|
||||
*.tmproj
|
||||
.vscode/
|
||||
24
charts/canal-admin/Chart.yaml
Normal file
24
charts/canal-admin/Chart.yaml
Normal file
@@ -0,0 +1,24 @@
|
||||
apiVersion: v2
|
||||
name: canal-admin
|
||||
description: A Helm chart for Kubernetes
|
||||
|
||||
# A chart can be either an 'application' or a 'library' chart.
|
||||
#
|
||||
# Application charts are a collection of templates that can be packaged into versioned archives
|
||||
# to be deployed.
|
||||
#
|
||||
# Library charts provide useful utilities or functions for the chart developer. They're included as
|
||||
# a dependency of application charts to inject those utilities and functions into the rendering
|
||||
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
|
||||
type: application
|
||||
|
||||
# This is the chart version. This version number should be incremented each time you make changes
|
||||
# to the chart and its templates, including the app version.
|
||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||
version: 1.1.8-alpha-3
|
||||
|
||||
# This is the version number of the application being deployed. This version number should be
|
||||
# incremented each time you make changes to the application. Versions are not expected to
|
||||
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||
# It is recommended to use it with quotes.
|
||||
appVersion: "1.16.0"
|
||||
22
charts/canal-admin/templates/NOTES.txt
Normal file
22
charts/canal-admin/templates/NOTES.txt
Normal file
@@ -0,0 +1,22 @@
|
||||
1. Get the application URL by running these commands:
|
||||
{{- if .Values.ingress.enabled }}
|
||||
{{- range $host := .Values.ingress.hosts }}
|
||||
{{- range .paths }}
|
||||
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- else if contains "NodePort" .Values.service.type }}
|
||||
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "canal-admin.fullname" . }})
|
||||
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
|
||||
echo http://$NODE_IP:$NODE_PORT
|
||||
{{- else if contains "LoadBalancer" .Values.service.type }}
|
||||
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
|
||||
You can watch its status by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "canal-admin.fullname" . }}'
|
||||
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "canal-admin.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
|
||||
echo http://$SERVICE_IP:{{ .Values.service.port }}
|
||||
{{- else if contains "ClusterIP" .Values.service.type }}
|
||||
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "canal-admin.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
|
||||
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
|
||||
echo "Visit http://127.0.0.1:8080 to use your application"
|
||||
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
|
||||
{{- end }}
|
||||
62
charts/canal-admin/templates/_helpers.tpl
Normal file
62
charts/canal-admin/templates/_helpers.tpl
Normal file
@@ -0,0 +1,62 @@
|
||||
{{/*
|
||||
Expand the name of the chart.
|
||||
*/}}
|
||||
{{- define "canal-admin.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create a default fully qualified app name.
|
||||
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||
If release name contains chart name it will be used as a full name.
|
||||
*/}}
|
||||
{{- define "canal-admin.fullname" -}}
|
||||
{{- if .Values.fullnameOverride }}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride }}
|
||||
{{- if contains $name .Release.Name }}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create chart name and version as used by the chart label.
|
||||
*/}}
|
||||
{{- define "canal-admin.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Common labels
|
||||
*/}}
|
||||
{{- define "canal-admin.labels" -}}
|
||||
helm.sh/chart: {{ include "canal-admin.chart" . }}
|
||||
{{ include "canal-admin.selectorLabels" . }}
|
||||
{{- if .Chart.AppVersion }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
{{- end }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Selector labels
|
||||
*/}}
|
||||
{{- define "canal-admin.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "canal-admin.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create the name of the service account to use
|
||||
*/}}
|
||||
{{- define "canal-admin.serviceAccountName" -}}
|
||||
{{- if .Values.serviceAccount.create }}
|
||||
{{- default (include "canal-admin.fullname" .) .Values.serviceAccount.name }}
|
||||
{{- else }}
|
||||
{{- default "default" .Values.serviceAccount.name }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
75
charts/canal-admin/templates/deployment.yaml
Normal file
75
charts/canal-admin/templates/deployment.yaml
Normal file
@@ -0,0 +1,75 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "canal-admin.fullname" . }}
|
||||
labels:
|
||||
{{- include "canal-admin.labels" . | nindent 4 }}
|
||||
spec:
|
||||
{{- if not .Values.autoscaling.enabled }}
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "canal-admin.selectorLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
{{- with .Values.podAnnotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "canal-admin.labels" . | nindent 8 }}
|
||||
{{- with .Values.podLabels }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- with .Values.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
serviceAccountName: {{ include "canal-admin.serviceAccountName" . }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.podSecurityContext | nindent 8 }}
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.securityContext | nindent 12 }}
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: {{ .Values.service.port }}
|
||||
protocol: TCP
|
||||
livenessProbe:
|
||||
{{- toYaml .Values.livenessProbe | nindent 12 }}
|
||||
readinessProbe:
|
||||
{{- toYaml .Values.readinessProbe | nindent 12 }}
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
volumeMounts:
|
||||
- mountPath: /home/admin/canal-admin/conf/application.yml
|
||||
name: secret
|
||||
subPath: application.yml
|
||||
{{- with .Values.volumeMounts }}
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
volumes:
|
||||
- name: secret
|
||||
secret:
|
||||
defaultMode: 420
|
||||
secretName: {{ include "canal-admin.fullname" . }}
|
||||
{{- with .Values.volumes }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
32
charts/canal-admin/templates/hpa.yaml
Normal file
32
charts/canal-admin/templates/hpa.yaml
Normal file
@@ -0,0 +1,32 @@
|
||||
{{- if .Values.autoscaling.enabled }}
|
||||
apiVersion: autoscaling/v2
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: {{ include "canal-admin.fullname" . }}
|
||||
labels:
|
||||
{{- include "canal-admin.labels" . | nindent 4 }}
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: {{ include "canal-admin.fullname" . }}
|
||||
minReplicas: {{ .Values.autoscaling.minReplicas }}
|
||||
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
|
||||
metrics:
|
||||
{{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
|
||||
{{- end }}
|
||||
{{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
|
||||
- type: Resource
|
||||
resource:
|
||||
name: memory
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
43
charts/canal-admin/templates/ingress.yaml
Normal file
43
charts/canal-admin/templates/ingress.yaml
Normal file
@@ -0,0 +1,43 @@
|
||||
{{- if .Values.ingress.enabled -}}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ include "canal-admin.fullname" . }}
|
||||
labels:
|
||||
{{- include "canal-admin.labels" . | nindent 4 }}
|
||||
{{- with .Values.ingress.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- with .Values.ingress.className }}
|
||||
ingressClassName: {{ . }}
|
||||
{{- end }}
|
||||
{{- if .Values.ingress.tls }}
|
||||
tls:
|
||||
{{- range .Values.ingress.tls }}
|
||||
- hosts:
|
||||
{{- range .hosts }}
|
||||
- {{ . | quote }}
|
||||
{{- end }}
|
||||
secretName: {{ .secretName }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
rules:
|
||||
{{- range .Values.ingress.hosts }}
|
||||
- host: {{ .host | quote }}
|
||||
http:
|
||||
paths:
|
||||
{{- range .paths }}
|
||||
- path: {{ .path }}
|
||||
{{- with .pathType }}
|
||||
pathType: {{ . }}
|
||||
{{- end }}
|
||||
backend:
|
||||
service:
|
||||
name: {{ include "canal-admin.fullname" $ }}
|
||||
port:
|
||||
number: {{ $.Values.service.port }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
9
charts/canal-admin/templates/secret.yaml
Normal file
9
charts/canal-admin/templates/secret.yaml
Normal file
@@ -0,0 +1,9 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: {{ include "canal-admin.fullname" . }}
|
||||
labels:
|
||||
{{- include "canal-admin.labels" . | nindent 4 }}
|
||||
type: Opaque
|
||||
data:
|
||||
application.yml: {{tpl .Values.admin.config $ | b64enc | quote }}
|
||||
15
charts/canal-admin/templates/service.yaml
Normal file
15
charts/canal-admin/templates/service.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "canal-admin.fullname" . }}
|
||||
labels:
|
||||
{{- include "canal-admin.labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
ports:
|
||||
- port: {{ .Values.service.port }}
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
name: http
|
||||
selector:
|
||||
{{- include "canal-admin.selectorLabels" . | nindent 4 }}
|
||||
13
charts/canal-admin/templates/serviceaccount.yaml
Normal file
13
charts/canal-admin/templates/serviceaccount.yaml
Normal file
@@ -0,0 +1,13 @@
|
||||
{{- if .Values.serviceAccount.create -}}
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: {{ include "canal-admin.serviceAccountName" . }}
|
||||
labels:
|
||||
{{- include "canal-admin.labels" . | nindent 4 }}
|
||||
{{- with .Values.serviceAccount.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
automountServiceAccountToken: {{ .Values.serviceAccount.automount }}
|
||||
{{- end }}
|
||||
15
charts/canal-admin/templates/tests/test-connection.yaml
Normal file
15
charts/canal-admin/templates/tests/test-connection.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: "{{ include "canal-admin.fullname" . }}-test-connection"
|
||||
labels:
|
||||
{{- include "canal-admin.labels" . | nindent 4 }}
|
||||
annotations:
|
||||
"helm.sh/hook": test
|
||||
spec:
|
||||
containers:
|
||||
- name: wget
|
||||
image: busybox
|
||||
command: ['wget']
|
||||
args: ['{{ include "canal-admin.fullname" . }}:{{ .Values.service.port }}']
|
||||
restartPolicy: Never
|
||||
146
charts/canal-admin/values.yaml
Normal file
146
charts/canal-admin/values.yaml
Normal file
@@ -0,0 +1,146 @@
|
||||
# Default values for canal-admin.
|
||||
# This is a YAML-formatted file.
|
||||
# Declare variables to be passed into your templates.
|
||||
|
||||
# This will set the replicaset count more information can be found here: https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/
|
||||
replicaCount: 1
|
||||
|
||||
# This sets the container image more information can be found here: https://kubernetes.io/docs/concepts/containers/images/
|
||||
image:
|
||||
repository: canal/canal-admin
|
||||
# This sets the pull policy for images.
|
||||
pullPolicy: IfNotPresent
|
||||
# Overrides the image tag whose default is the chart appVersion.
|
||||
tag: "latest"
|
||||
|
||||
# This is for the secretes for pulling an image from a private repository more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/
|
||||
imagePullSecrets: []
|
||||
# This is to override the chart name.
|
||||
nameOverride: ""
|
||||
fullnameOverride: ""
|
||||
|
||||
# This section builds out the service account more information can be found here: https://kubernetes.io/docs/concepts/security/service-accounts/
|
||||
serviceAccount:
|
||||
# Specifies whether a service account should be created
|
||||
create: false
|
||||
# Automatically mount a ServiceAccount's API credentials?
|
||||
automount: false
|
||||
# Annotations to add to the service account
|
||||
annotations: {}
|
||||
# The name of the service account to use.
|
||||
# If not set and create is true, a name is generated using the fullname template
|
||||
name: ""
|
||||
|
||||
# This is for setting Kubernetes Annotations to a Pod.
|
||||
# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/
|
||||
podAnnotations: {}
|
||||
# This is for setting Kubernetes Labels to a Pod.
|
||||
# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/
|
||||
podLabels: {}
|
||||
|
||||
podSecurityContext: {}
|
||||
# fsGroup: 2000
|
||||
|
||||
securityContext: {}
|
||||
# capabilities:
|
||||
# drop:
|
||||
# - ALL
|
||||
# readOnlyRootFilesystem: true
|
||||
# runAsNonRoot: true
|
||||
# runAsUser: 1000
|
||||
|
||||
# This is for setting up a service more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/
|
||||
service:
|
||||
# This sets the service type more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types
|
||||
type: ClusterIP
|
||||
# This sets the ports more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#field-spec-ports
|
||||
port: 8089
|
||||
|
||||
# This block is for setting up the ingress for more information can be found here: https://kubernetes.io/docs/concepts/services-networking/ingress/
|
||||
ingress:
|
||||
enabled: false
|
||||
className: ""
|
||||
annotations: {}
|
||||
# kubernetes.io/ingress.class: nginx
|
||||
# kubernetes.io/tls-acme: "true"
|
||||
hosts:
|
||||
- host: chart-example.local
|
||||
paths:
|
||||
- path: /
|
||||
pathType: ImplementationSpecific
|
||||
tls: []
|
||||
# - secretName: chart-example-tls
|
||||
# hosts:
|
||||
# - chart-example.local
|
||||
|
||||
resources: {}
|
||||
# We usually recommend not to specify default resources and to leave this as a conscious
|
||||
# choice for the user. This also increases chances charts run on environments with little
|
||||
# resources, such as Minikube. If you do want to specify resources, uncomment the following
|
||||
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
|
||||
# limits:
|
||||
# cpu: 100m
|
||||
# memory: 128Mi
|
||||
# requests:
|
||||
# cpu: 100m
|
||||
# memory: 128Mi
|
||||
|
||||
# This is to setup the liveness and readiness probes more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: http
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: http
|
||||
|
||||
# This section is for setting up autoscaling more information can be found here: https://kubernetes.io/docs/concepts/workloads/autoscaling/
|
||||
autoscaling:
|
||||
enabled: false
|
||||
minReplicas: 1
|
||||
maxReplicas: 100
|
||||
targetCPUUtilizationPercentage: 80
|
||||
# targetMemoryUtilizationPercentage: 80
|
||||
|
||||
# Additional volumes on the output Deployment definition.
|
||||
volumes: []
|
||||
# - name: foo
|
||||
# secret:
|
||||
# secretName: mysecret
|
||||
# optional: false
|
||||
|
||||
# Additional volumeMounts on the output Deployment definition.
|
||||
volumeMounts: []
|
||||
# - name: foo
|
||||
# mountPath: "/etc/foo"
|
||||
# readOnly: true
|
||||
|
||||
nodeSelector: {}
|
||||
|
||||
tolerations: []
|
||||
|
||||
affinity: {}
|
||||
|
||||
admin:
|
||||
config: |
|
||||
server:
|
||||
port: 8089
|
||||
spring:
|
||||
jackson:
|
||||
date-format: yyyy-MM-dd HH:mm:ss
|
||||
time-zone: GMT+8
|
||||
spring.datasource:
|
||||
address: you_db_host:3306
|
||||
database: canal_manager
|
||||
username: ****
|
||||
password: ****
|
||||
driver-class-name: com.mysql.jdbc.Driver
|
||||
url: jdbc:mysql://${spring.datasource.address}/${spring.datasource.database}?useUnicode=true&characterEncoding=UTF-8&useSSL=false&allowPublicKeyRetrieval=true
|
||||
hikari:
|
||||
maximum-pool-size: 30
|
||||
minimum-idle: 1
|
||||
canal:
|
||||
adminUser: admin
|
||||
adminPasswd: admin
|
||||
# debug: true
|
||||
23
charts/canal-server/.helmignore
Normal file
23
charts/canal-server/.helmignore
Normal file
@@ -0,0 +1,23 @@
|
||||
# Patterns to ignore when building packages.
|
||||
# This supports shell glob matching, relative path matching, and
|
||||
# negation (prefixed with !). Only one pattern per line.
|
||||
.DS_Store
|
||||
# Common VCS dirs
|
||||
.git/
|
||||
.gitignore
|
||||
.bzr/
|
||||
.bzrignore
|
||||
.hg/
|
||||
.hgignore
|
||||
.svn/
|
||||
# Common backup files
|
||||
*.swp
|
||||
*.bak
|
||||
*.tmp
|
||||
*.orig
|
||||
*~
|
||||
# Various IDEs
|
||||
.project
|
||||
.idea/
|
||||
*.tmproj
|
||||
.vscode/
|
||||
24
charts/canal-server/Chart.yaml
Normal file
24
charts/canal-server/Chart.yaml
Normal file
@@ -0,0 +1,24 @@
|
||||
apiVersion: v2
|
||||
name: canal-server
|
||||
description: A Helm chart for Kubernetes
|
||||
|
||||
# A chart can be either an 'application' or a 'library' chart.
|
||||
#
|
||||
# Application charts are a collection of templates that can be packaged into versioned archives
|
||||
# to be deployed.
|
||||
#
|
||||
# Library charts provide useful utilities or functions for the chart developer. They're included as
|
||||
# a dependency of application charts to inject those utilities and functions into the rendering
|
||||
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
|
||||
type: application
|
||||
|
||||
# This is the chart version. This version number should be incremented each time you make changes
|
||||
# to the chart and its templates, including the app version.
|
||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||
version: 1.1.8-alpha-3
|
||||
|
||||
# This is the version number of the application being deployed. This version number should be
|
||||
# incremented each time you make changes to the application. Versions are not expected to
|
||||
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||
# It is recommended to use it with quotes.
|
||||
appVersion: "1.16.0"
|
||||
22
charts/canal-server/templates/NOTES.txt
Normal file
22
charts/canal-server/templates/NOTES.txt
Normal file
@@ -0,0 +1,22 @@
|
||||
1. Get the application URL by running these commands:
|
||||
{{- if .Values.ingress.enabled }}
|
||||
{{- range $host := .Values.ingress.hosts }}
|
||||
{{- range .paths }}
|
||||
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- else if contains "NodePort" .Values.service.type }}
|
||||
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "canal-server.fullname" . }})
|
||||
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
|
||||
echo http://$NODE_IP:$NODE_PORT
|
||||
{{- else if contains "LoadBalancer" .Values.service.type }}
|
||||
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
|
||||
You can watch its status by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "canal-server.fullname" . }}'
|
||||
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "canal-server.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
|
||||
echo http://$SERVICE_IP:{{ .Values.service.port }}
|
||||
{{- else if contains "ClusterIP" .Values.service.type }}
|
||||
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "canal-server.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
|
||||
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
|
||||
echo "Visit http://127.0.0.1:8080 to use your application"
|
||||
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
|
||||
{{- end }}
|
||||
62
charts/canal-server/templates/_helpers.tpl
Normal file
62
charts/canal-server/templates/_helpers.tpl
Normal file
@@ -0,0 +1,62 @@
|
||||
{{/*
|
||||
Expand the name of the chart.
|
||||
*/}}
|
||||
{{- define "canal-server.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create a default fully qualified app name.
|
||||
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||
If release name contains chart name it will be used as a full name.
|
||||
*/}}
|
||||
{{- define "canal-server.fullname" -}}
|
||||
{{- if .Values.fullnameOverride }}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride }}
|
||||
{{- if contains $name .Release.Name }}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create chart name and version as used by the chart label.
|
||||
*/}}
|
||||
{{- define "canal-server.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Common labels
|
||||
*/}}
|
||||
{{- define "canal-server.labels" -}}
|
||||
helm.sh/chart: {{ include "canal-server.chart" . }}
|
||||
{{ include "canal-server.selectorLabels" . }}
|
||||
{{- if .Chart.AppVersion }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
{{- end }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Selector labels
|
||||
*/}}
|
||||
{{- define "canal-server.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "canal-server.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create the name of the service account to use
|
||||
*/}}
|
||||
{{- define "canal-server.serviceAccountName" -}}
|
||||
{{- if .Values.serviceAccount.create }}
|
||||
{{- default (include "canal-server.fullname" .) .Values.serviceAccount.name }}
|
||||
{{- else }}
|
||||
{{- default "default" .Values.serviceAccount.name }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
32
charts/canal-server/templates/hpa.yaml
Normal file
32
charts/canal-server/templates/hpa.yaml
Normal file
@@ -0,0 +1,32 @@
|
||||
{{- if .Values.autoscaling.enabled }}
|
||||
apiVersion: autoscaling/v2
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: {{ include "canal-server.fullname" . }}
|
||||
labels:
|
||||
{{- include "canal-server.labels" . | nindent 4 }}
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: {{ include "canal-server.fullname" . }}
|
||||
minReplicas: {{ .Values.autoscaling.minReplicas }}
|
||||
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
|
||||
metrics:
|
||||
{{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
|
||||
{{- end }}
|
||||
{{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
|
||||
- type: Resource
|
||||
resource:
|
||||
name: memory
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
9
charts/canal-server/templates/secret.yaml
Normal file
9
charts/canal-server/templates/secret.yaml
Normal file
@@ -0,0 +1,9 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: {{ include "canal-server.fullname" . }}
|
||||
labels:
|
||||
{{- include "canal-server.labels" . | nindent 4 }}
|
||||
type: Opaque
|
||||
data:
|
||||
canal.properties: {{tpl .Values.server.config $ | b64enc | quote }}
|
||||
23
charts/canal-server/templates/service.yaml
Normal file
23
charts/canal-server/templates/service.yaml
Normal file
@@ -0,0 +1,23 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ printf "%s-headless" (include "canal-server.fullname" .) | trunc 63 | trimSuffix "-" }}
|
||||
labels:
|
||||
{{- include "canal-server.labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
ports:
|
||||
- port: {{ .Values.service.ports.admin }}
|
||||
targetPort: admin
|
||||
protocol: TCP
|
||||
name: tcp-admin
|
||||
- port: {{ .Values.service.ports.tcp }}
|
||||
targetPort: tcp
|
||||
protocol: TCP
|
||||
name: tcp-tcp
|
||||
- port: {{ .Values.service.ports.metric }}
|
||||
targetPort: metric
|
||||
protocol: TCP
|
||||
name: tcp-metric
|
||||
selector:
|
||||
{{- include "canal-server.selectorLabels" . | nindent 4 }}
|
||||
13
charts/canal-server/templates/serviceaccount.yaml
Normal file
13
charts/canal-server/templates/serviceaccount.yaml
Normal file
@@ -0,0 +1,13 @@
|
||||
{{- if .Values.serviceAccount.create -}}
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: {{ include "canal-server.serviceAccountName" . }}
|
||||
labels:
|
||||
{{- include "canal-server.labels" . | nindent 4 }}
|
||||
{{- with .Values.serviceAccount.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
automountServiceAccountToken: {{ .Values.serviceAccount.automount }}
|
||||
{{- end }}
|
||||
80
charts/canal-server/templates/statefulsets.yaml
Normal file
80
charts/canal-server/templates/statefulsets.yaml
Normal file
@@ -0,0 +1,80 @@
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: {{ include "canal-server.fullname" . }}
|
||||
labels:
|
||||
{{- include "canal-server.labels" . | nindent 4 }}
|
||||
spec:
|
||||
serviceName: {{ printf "%s-headless" (include "canal-server.fullname" .) | trunc 63 | trimSuffix "-" }}
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "canal-server.selectorLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
{{- with .Values.podAnnotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "canal-server.labels" . | nindent 8 }}
|
||||
{{- with .Values.podLabels }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- with .Values.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
serviceAccountName: {{ include "canal-server.serviceAccountName" . }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.podSecurityContext | nindent 8 }}
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.securityContext | nindent 12 }}
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
ports:
|
||||
- name: admin
|
||||
containerPort: {{ .Values.service.ports.admin }}
|
||||
protocol: TCP
|
||||
- name: tcp
|
||||
containerPort: {{ .Values.service.ports.tcp }}
|
||||
protocol: TCP
|
||||
- name: metric
|
||||
containerPort: {{ .Values.service.ports.metric }}
|
||||
protocol: TCP
|
||||
livenessProbe:
|
||||
{{- toYaml .Values.livenessProbe | nindent 12 }}
|
||||
readinessProbe:
|
||||
{{- toYaml .Values.readinessProbe | nindent 12 }}
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
volumeMounts:
|
||||
- mountPath: /home/admin/canal-server/conf/canal.properties
|
||||
name: secret
|
||||
subPath: canal.properties
|
||||
{{- with .Values.volumeMounts }}
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
volumes:
|
||||
- name: secret
|
||||
secret:
|
||||
defaultMode: 420
|
||||
secretName: {{ include "canal-server.fullname" . }}
|
||||
{{- with .Values.volumes }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
15
charts/canal-server/templates/tests/test-connection.yaml
Normal file
15
charts/canal-server/templates/tests/test-connection.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: "{{ include "canal-server.fullname" . }}-test-connection"
|
||||
labels:
|
||||
{{- include "canal-server.labels" . | nindent 4 }}
|
||||
annotations:
|
||||
"helm.sh/hook": test
|
||||
spec:
|
||||
containers:
|
||||
- name: wget
|
||||
image: busybox
|
||||
command: ['wget']
|
||||
args: ['{{ include "canal-server.fullname" . }}:{{ .Values.service.port }}']
|
||||
restartPolicy: Never
|
||||
144
charts/canal-server/values.yaml
Normal file
144
charts/canal-server/values.yaml
Normal file
@@ -0,0 +1,144 @@
|
||||
# Default values for canal-server.
|
||||
# This is a YAML-formatted file.
|
||||
# Declare variables to be passed into your templates.
|
||||
|
||||
# This will set the replicaset count more information can be found here: https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/
|
||||
replicaCount: 1
|
||||
|
||||
# This sets the container image more information can be found here: https://kubernetes.io/docs/concepts/containers/images/
|
||||
image:
|
||||
repository: canal/canal-server
|
||||
# This sets the pull policy for images.
|
||||
pullPolicy: IfNotPresent
|
||||
# Overrides the image tag whose default is the chart appVersion.
|
||||
tag: "1.1.8-alpha-3"
|
||||
|
||||
# This is for the secretes for pulling an image from a private repository more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/
|
||||
imagePullSecrets: []
|
||||
# This is to override the chart name.
|
||||
nameOverride: ""
|
||||
fullnameOverride: ""
|
||||
|
||||
# This section builds out the service account more information can be found here: https://kubernetes.io/docs/concepts/security/service-accounts/
|
||||
serviceAccount:
|
||||
# Specifies whether a service account should be created
|
||||
create: false
|
||||
# Automatically mount a ServiceAccount's API credentials?
|
||||
automount: false
|
||||
# Annotations to add to the service account
|
||||
annotations: {}
|
||||
# The name of the service account to use.
|
||||
# If not set and create is true, a name is generated using the fullname template
|
||||
name: ""
|
||||
|
||||
# This is for setting Kubernetes Annotations to a Pod.
|
||||
# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/
|
||||
podAnnotations: {}
|
||||
# This is for setting Kubernetes Labels to a Pod.
|
||||
# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/
|
||||
podLabels: {}
|
||||
|
||||
podSecurityContext: {}
|
||||
# fsGroup: 2000
|
||||
|
||||
securityContext: {}
|
||||
# capabilities:
|
||||
# drop:
|
||||
# - ALL
|
||||
# readOnlyRootFilesystem: true
|
||||
# runAsNonRoot: true
|
||||
# runAsUser: 1000
|
||||
|
||||
# This is for setting up a service more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/
|
||||
service:
|
||||
# This sets the service type more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types
|
||||
type: ClusterIP
|
||||
# This sets the ports more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#field-spec-ports
|
||||
# port: 80
|
||||
ports:
|
||||
admin: 11110
|
||||
tcp: 11111
|
||||
metric: 11112
|
||||
|
||||
# This block is for setting up the ingress for more information can be found here: https://kubernetes.io/docs/concepts/services-networking/ingress/
|
||||
ingress:
|
||||
enabled: false
|
||||
className: ""
|
||||
annotations: {}
|
||||
# kubernetes.io/ingress.class: nginx
|
||||
# kubernetes.io/tls-acme: "true"
|
||||
hosts:
|
||||
- host: chart-example.local
|
||||
paths:
|
||||
- path: /
|
||||
pathType: ImplementationSpecific
|
||||
tls: []
|
||||
# - secretName: chart-example-tls
|
||||
# hosts:
|
||||
# - chart-example.local
|
||||
|
||||
resources: {}
|
||||
# We usually recommend not to specify default resources and to leave this as a conscious
|
||||
# choice for the user. This also increases chances charts run on environments with little
|
||||
# resources, such as Minikube. If you do want to specify resources, uncomment the following
|
||||
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
|
||||
# limits:
|
||||
# cpu: 100m
|
||||
# memory: 128Mi
|
||||
# requests:
|
||||
# cpu: 100m
|
||||
# memory: 128Mi
|
||||
|
||||
# This is to setup the liveness and readiness probes more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: metric
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: metric
|
||||
|
||||
# This section is for setting up autoscaling more information can be found here: https://kubernetes.io/docs/concepts/workloads/autoscaling/
|
||||
autoscaling:
|
||||
enabled: false
|
||||
minReplicas: 1
|
||||
maxReplicas: 100
|
||||
targetCPUUtilizationPercentage: 80
|
||||
# targetMemoryUtilizationPercentage: 80
|
||||
|
||||
# Additional volumes on the output Deployment definition.
|
||||
volumes: []
|
||||
# - name: foo
|
||||
# secret:
|
||||
# secretName: mysecret
|
||||
# optional: false
|
||||
|
||||
# Additional volumeMounts on the output Deployment definition.
|
||||
volumeMounts: []
|
||||
# - name: foo
|
||||
# mountPath: "/etc/foo"
|
||||
# readOnly: true
|
||||
|
||||
nodeSelector: {}
|
||||
|
||||
tolerations: []
|
||||
|
||||
affinity: {}
|
||||
|
||||
server:
|
||||
config: |
|
||||
canal.port = 11111
|
||||
canal.metrics.pull.port = 11112
|
||||
|
||||
# register ip
|
||||
canal.register.ip =
|
||||
|
||||
# canal admin config
|
||||
canal.admin.manager = canal-admin.default:8089
|
||||
canal.admin.port = 11110
|
||||
canal.admin.user = admin
|
||||
canal.admin.passwd = 4ACFE3202A5FF5CF467898FC58AAB1D615029441
|
||||
# admin auto register
|
||||
canal.admin.register.auto = true
|
||||
canal.admin.register.cluster = default
|
||||
BIN
charts/images/cluster.png
Normal file
BIN
charts/images/cluster.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 100 KiB |
BIN
charts/images/server.png
Normal file
BIN
charts/images/server.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 157 KiB |
34
charts/tests/admin-values.yaml
Normal file
34
charts/tests/admin-values.yaml
Normal file
@@ -0,0 +1,34 @@
|
||||
ingress:
|
||||
enabled: true
|
||||
hosts:
|
||||
- host: admin.canal.com
|
||||
paths:
|
||||
- path: /
|
||||
pathType: ImplementationSpecific
|
||||
|
||||
admin:
|
||||
config: |
|
||||
server:
|
||||
port: 8089
|
||||
spring:
|
||||
jackson:
|
||||
date-format: yyyy-MM-dd HH:mm:ss
|
||||
time-zone: GMT+8
|
||||
spring.datasource:
|
||||
address: you_db_host:3306
|
||||
database: canal_manager
|
||||
username: ****
|
||||
password: ****
|
||||
driver-class-name: com.mysql.jdbc.Driver
|
||||
url: jdbc:mysql://${spring.datasource.address}/${spring.datasource.database}?useUnicode=true&characterEncoding=UTF-8&useSSL=false&allowPublicKeyRetrieval=true
|
||||
hikari:
|
||||
maximum-pool-size: 30
|
||||
minimum-idle: 1
|
||||
canal:
|
||||
adminUser: admin
|
||||
adminPasswd: admin
|
||||
debug: true
|
||||
|
||||
image:
|
||||
repository: registry.us-east-1.aliyuncs.com/sll0/canal-admin
|
||||
tag: "latest"
|
||||
20
charts/tests/server-values.yaml
Normal file
20
charts/tests/server-values.yaml
Normal file
@@ -0,0 +1,20 @@
|
||||
server:
|
||||
config: |
|
||||
canal.port = 11111
|
||||
canal.metrics.pull.port = 11112
|
||||
|
||||
# register ip
|
||||
canal.register.ip =
|
||||
|
||||
# canal admin config
|
||||
canal.admin.manager = canal-admin.default:8089
|
||||
canal.admin.port = 11110
|
||||
canal.admin.user = admin
|
||||
canal.admin.passwd = 4ACFE3202A5FF5CF467898FC58AAB1D615029441
|
||||
# admin auto register
|
||||
canal.admin.register.auto = true
|
||||
canal.admin.register.cluster = default
|
||||
|
||||
image:
|
||||
repository: registry.us-east-1.aliyuncs.com/sll0/canal-server
|
||||
tag: "v1.1.8-alpha-3"
|
||||
5
charts/tests/test.sh
Executable file
5
charts/tests/test.sh
Executable file
@@ -0,0 +1,5 @@
|
||||
#! /bin/bash
|
||||
|
||||
helm install canal-admin -f ./admin-values.yaml ../canal-admin --dry-run
|
||||
|
||||
helm install canal-server -f ./server-values.yaml ../canal-server --dry-run
|
||||
82
client-adapter/clickhouse/pom.xml
Normal file
82
client-adapter/clickhouse/pom.xml
Normal file
@@ -0,0 +1,82 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<artifactId>canal.client-adapter</artifactId>
|
||||
<groupId>com.alibaba.otter</groupId>
|
||||
<version>1.1.8</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>com.alibaba.otter</groupId>
|
||||
<artifactId>client-adapter.clickhouse</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<name>canal client adapter clickhouse module for otter ${project.version}</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.alibaba.otter</groupId>
|
||||
<artifactId>client-adapter.common</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ru.yandex.clickhouse</groupId>
|
||||
<artifactId>clickhouse-jdbc</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.yaml</groupId>
|
||||
<artifactId>snakeyaml</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-assembly-plugin</artifactId>
|
||||
<version>2.4</version>
|
||||
<configuration>
|
||||
<descriptorRefs>
|
||||
<descriptorRef>jar-with-dependencies</descriptorRef>
|
||||
</descriptorRefs>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>make-assembly</id>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>single</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-antrun-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>run</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<tasks>
|
||||
<copy todir="${project.basedir}/../launcher/target/classes/clickhouse" overwrite="true">
|
||||
<fileset dir="${project.basedir}/target/classes/clickhouse" erroronmissingdir="true">
|
||||
<include name="*.yml" />
|
||||
</fileset>
|
||||
</copy>
|
||||
</tasks>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,340 @@
|
||||
package com.alibaba.otter.canal.client.adapter.clickhouse;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.apache.commons.lang.BooleanUtils;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.alibaba.druid.filter.stat.StatFilter;
|
||||
import com.alibaba.druid.pool.DruidDataSource;
|
||||
import com.alibaba.druid.util.JdbcUtils;
|
||||
import com.alibaba.otter.canal.client.adapter.OuterAdapter;
|
||||
import com.alibaba.otter.canal.client.adapter.clickhouse.config.ConfigLoader;
|
||||
import com.alibaba.otter.canal.client.adapter.clickhouse.config.MappingConfig;
|
||||
import com.alibaba.otter.canal.client.adapter.clickhouse.config.MirrorDbConfig;
|
||||
import com.alibaba.otter.canal.client.adapter.clickhouse.monitor.ClickHouseConfigMonitor;
|
||||
import com.alibaba.otter.canal.client.adapter.clickhouse.service.ClickHouseBatchSyncService;
|
||||
import com.alibaba.otter.canal.client.adapter.clickhouse.service.ClickHouseEtlService;
|
||||
import com.alibaba.otter.canal.client.adapter.clickhouse.service.ClickHouseMirrorDbBatchSyncService;
|
||||
import com.alibaba.otter.canal.client.adapter.clickhouse.support.SyncUtil;
|
||||
import com.alibaba.otter.canal.client.adapter.support.*;
|
||||
|
||||
/**
|
||||
* ClickHouse Adapter implementation
|
||||
*
|
||||
* @author: Xander
|
||||
* @date: Created in 2023/11/10 1:13
|
||||
* @email: zhrunxin33@gmail.com
|
||||
*/
|
||||
@SPI
|
||||
public class ClickHouseAdapter implements OuterAdapter {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ClickHouseAdapter.class);
|
||||
|
||||
// Store the mapping of filename and configuration, load yml files below
|
||||
// resource path
|
||||
private Map<String, MappingConfig> clickHouseMapping = new ConcurrentHashMap<>();
|
||||
|
||||
// Schema -> Table -> MappingConfig
|
||||
private Map<String, Map<String, MappingConfig>> mappingConfigCache = new ConcurrentHashMap<>();
|
||||
|
||||
// Mirror DB Configuration, don't need to load column mapping
|
||||
private Map<String, MirrorDbConfig> mirrorDbConfigCache = new ConcurrentHashMap<>();
|
||||
|
||||
private DruidDataSource dataSource;
|
||||
|
||||
private ClickHouseBatchSyncService clickHouseBatchSyncService;
|
||||
|
||||
private ClickHouseMirrorDbBatchSyncService clickHouseMirrorDbBatchSyncService;
|
||||
|
||||
private Properties envProperties;
|
||||
|
||||
// Launch configuration
|
||||
private OuterAdapterConfig configuration;
|
||||
|
||||
private ClickHouseConfigMonitor clickHouseConfigMonitor;
|
||||
|
||||
public Map<String, MappingConfig> getClickHouseMapping() {
|
||||
return clickHouseMapping;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(OuterAdapterConfig configuration, Properties envProperties) {
|
||||
this.envProperties = envProperties;
|
||||
this.configuration = configuration;
|
||||
// Load DB type from adapter.launch/bootstrap.yml
|
||||
Map<String, String> properties = configuration.getProperties();
|
||||
String dbType = JdbcUtils.getDbType(properties.get("jdbc.url"), null);
|
||||
// 当.yml文件编码格式存在问题,此处clickhouse yml文件构建 可能会抛出异常
|
||||
Map<String, MappingConfig> clickHouseMappingTmp = ConfigLoader.load(envProperties);
|
||||
// 过滤不匹配的key的配置
|
||||
clickHouseMappingTmp.forEach((key, config) -> {
|
||||
addConfig(key, config);
|
||||
});
|
||||
|
||||
if (clickHouseMapping.isEmpty()) {
|
||||
throw new RuntimeException("No clickhouse adapter found for config key: " + configuration.getKey());
|
||||
}
|
||||
|
||||
// 初始化连接池
|
||||
dataSource = new DruidDataSource();
|
||||
dataSource.setDriverClassName(properties.get("jdbc.driverClassName"));
|
||||
dataSource.setUrl(properties.get("jdbc.url"));
|
||||
dataSource.setUsername(properties.get("jdbc.username"));
|
||||
dataSource.setPassword(properties.get("jdbc.password"));
|
||||
dataSource.setInitialSize(1);
|
||||
dataSource.setMinIdle(1);
|
||||
dataSource.setMaxActive(30);
|
||||
dataSource.setMaxWait(60000);
|
||||
dataSource.setTimeBetweenEvictionRunsMillis(60000);
|
||||
dataSource.setMinEvictableIdleTimeMillis(300000);
|
||||
dataSource.setUseUnfairLock(true);
|
||||
dataSource.setDefaultAutoCommit(false); // disable auto commit or disable Transactional
|
||||
dataSource.setDbType(dbType);
|
||||
|
||||
if ("true".equals(properties.getOrDefault("druid.stat.enable", "true"))) {
|
||||
StatFilter statFilter = new StatFilter();
|
||||
statFilter.setSlowSqlMillis(Long.parseLong(properties.getOrDefault("druid.stat.slowSqlMillis", "1000")));
|
||||
statFilter.setMergeSql(true);
|
||||
statFilter.setLogSlowSql(true);
|
||||
dataSource.setProxyFilters(Collections.singletonList(statFilter));
|
||||
}
|
||||
|
||||
try {
|
||||
dataSource.init();
|
||||
} catch (SQLException e) {
|
||||
logger.error("ERROR ## failed to initial datasource: " + properties.get("jdbc.url"), e);
|
||||
}
|
||||
|
||||
String threads = properties.get("threads");
|
||||
String scheduleTime = properties.get("scheduleTime");
|
||||
String batchSize = properties.get("batchSize");
|
||||
|
||||
boolean skipDupException = BooleanUtils.toBoolean(configuration.getProperties()
|
||||
.getOrDefault("skipDupException", "true"));
|
||||
clickHouseBatchSyncService = new ClickHouseBatchSyncService(dataSource,
|
||||
threads != null ? Integer.valueOf(threads) : null,
|
||||
batchSize != null ? Integer.valueOf(batchSize) : null,
|
||||
scheduleTime != null ? Long.valueOf(scheduleTime) : null,
|
||||
skipDupException);
|
||||
|
||||
clickHouseMirrorDbBatchSyncService = new ClickHouseMirrorDbBatchSyncService(mirrorDbConfigCache,
|
||||
dataSource,
|
||||
threads != null ? Integer.valueOf(threads) : null,
|
||||
batchSize != null ? Integer.valueOf(batchSize) : null,
|
||||
scheduleTime != null ? Long.valueOf(scheduleTime) : null,
|
||||
clickHouseBatchSyncService.getColumnsTypeCache(),
|
||||
skipDupException);
|
||||
|
||||
clickHouseConfigMonitor = new ClickHouseConfigMonitor();
|
||||
clickHouseConfigMonitor.init(configuration.getKey(), this, envProperties);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync main entrance
|
||||
*
|
||||
* @param dmls 数据包
|
||||
*/
|
||||
@Override
|
||||
public void sync(List<Dml> dmls) {
|
||||
if (dmls == null || dmls.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// If mappingConfigCache(column mapping) is empty, that must be mirroring synchronize
|
||||
if (!mappingConfigCache.isEmpty()) {
|
||||
clickHouseBatchSyncService.sync(mappingConfigCache, dmls, envProperties);
|
||||
}
|
||||
clickHouseMirrorDbBatchSyncService.sync(dmls);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy
|
||||
*/
|
||||
@Override
|
||||
public void destroy() {
|
||||
if (clickHouseConfigMonitor != null) {
|
||||
clickHouseConfigMonitor.destroy();
|
||||
}
|
||||
|
||||
if (clickHouseBatchSyncService != null) {
|
||||
clickHouseBatchSyncService.close();
|
||||
}
|
||||
|
||||
if (dataSource != null) {
|
||||
dataSource.close();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* ETL方法
|
||||
*
|
||||
* @param task 任务名, 对应配置名
|
||||
* @param params etl筛选条件
|
||||
* @return ETL结果
|
||||
*/
|
||||
@Override
|
||||
public EtlResult etl(String task, List<String> params) {
|
||||
EtlResult etlResult = new EtlResult();
|
||||
MappingConfig config = clickHouseMapping.get(task);
|
||||
ClickHouseEtlService clickhouseEtlService = new ClickHouseEtlService(dataSource, config);
|
||||
if (config != null) {
|
||||
return clickhouseEtlService.importData(params);
|
||||
} else {
|
||||
StringBuilder resultMsg = new StringBuilder();
|
||||
boolean resSucc = true;
|
||||
for (MappingConfig configTmp : clickHouseMapping.values()) {
|
||||
// 取所有的destination为task的配置
|
||||
if (configTmp.getDestination().equals(task)) {
|
||||
EtlResult etlRes = clickhouseEtlService.importData(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<String, Object> count(String task) {
|
||||
MappingConfig config = clickHouseMapping.get(task);
|
||||
MappingConfig.DbMapping dbMapping = config.getDbMapping();
|
||||
String sql = "SELECT COUNT(1) AS cnt FROM " + SyncUtil.getDbTableName(dbMapping, dataSource.getDbType());
|
||||
Connection conn = null;
|
||||
Map<String, Object> res = new LinkedHashMap<>();
|
||||
try {
|
||||
conn = dataSource.getConnection();
|
||||
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 (SQLException 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, dataSource.getDbType()));
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取对应canal instance name 或 mq topic
|
||||
*
|
||||
* @param task 任务名, 对应配置名
|
||||
* @return destination
|
||||
*/
|
||||
@Override
|
||||
public String getDestination(String task) {
|
||||
MappingConfig config = clickHouseMapping.get(task);
|
||||
if (config != null) {
|
||||
return config.getDestination();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void addSyncConfigToCache(String configName, MappingConfig mappingConfig) {
|
||||
if (!mappingConfig.getDbMapping().getMirrorDb()) {
|
||||
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();
|
||||
} else {
|
||||
key = StringUtils.trimToEmpty(mappingConfig.getDestination()) + "_"
|
||||
+ mappingConfig.getDbMapping().getDatabase() + "-" + mappingConfig.getDbMapping().getTable();
|
||||
}
|
||||
Map<String, MappingConfig> configMap = mappingConfigCache.computeIfAbsent(key,
|
||||
k1 -> new ConcurrentHashMap<>());
|
||||
configMap.put(configName, mappingConfig);
|
||||
} else {
|
||||
// mirrorDB
|
||||
String key = StringUtils.trimToEmpty(mappingConfig.getDestination()) + "."
|
||||
+ mappingConfig.getDbMapping().getDatabase();
|
||||
mirrorDbConfigCache.put(key, MirrorDbConfig.create(configName, mappingConfig));
|
||||
}
|
||||
}
|
||||
|
||||
public boolean addConfig(String fileName, MappingConfig config) {
|
||||
if (match(config)) {
|
||||
clickHouseMapping.put(fileName, config);
|
||||
addSyncConfigToCache(fileName, config);
|
||||
FileName2KeyMapping.register(getClass().getAnnotation(SPI.class).value(), fileName,
|
||||
configuration.getKey());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void updateConfig(String fileName, MappingConfig config) {
|
||||
if (config.getOuterAdapterKey() != null && !config.getOuterAdapterKey()
|
||||
.equals(configuration.getKey())) {
|
||||
// 理论上不允许改这个 因为本身就是通过这个关联起Adapter和Config的
|
||||
throw new RuntimeException("not allow to change outAdapterKey");
|
||||
}
|
||||
clickHouseMapping.put(fileName, config);
|
||||
addSyncConfigToCache(fileName, config);
|
||||
}
|
||||
|
||||
public void deleteConfig(String fileName) {
|
||||
clickHouseMapping.remove(fileName);
|
||||
for (Map<String, MappingConfig> configMap : mappingConfigCache.values()) {
|
||||
if (configMap != null) {
|
||||
configMap.remove(fileName);
|
||||
}
|
||||
}
|
||||
FileName2KeyMapping.unregister(getClass().getAnnotation(SPI.class).value(), fileName);
|
||||
}
|
||||
|
||||
private boolean match(MappingConfig config) {
|
||||
boolean sameMatch = config.getOuterAdapterKey() != null && config.getOuterAdapterKey()
|
||||
.equalsIgnoreCase(configuration.getKey());
|
||||
boolean prefixMatch = config.getOuterAdapterKey() == null && configuration.getKey()
|
||||
.startsWith(StringUtils
|
||||
.join(new String[]{Util.AUTO_GENERATED_PREFIX, config.getDestination(),
|
||||
config.getGroupId()}, '-'));
|
||||
return sameMatch || prefixMatch;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.alibaba.otter.canal.client.adapter.clickhouse.config;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.alibaba.otter.canal.client.adapter.support.MappingConfigsLoader;
|
||||
import com.alibaba.otter.canal.client.adapter.support.YamlUtils;
|
||||
|
||||
/**
|
||||
* CLICKHOUSE表映射配置加载器
|
||||
*
|
||||
* @author: Xander
|
||||
* @date: Created in 2023/11/10 22:23
|
||||
* @email: zhrunxin33@gmail.com
|
||||
* @version 1.1.8
|
||||
*/
|
||||
public class ConfigLoader {
|
||||
|
||||
private static Logger logger = LoggerFactory.getLogger(ConfigLoader.class);
|
||||
|
||||
/**
|
||||
* 加载CLICKHOUSE表映射配置
|
||||
*
|
||||
* @return 配置名/配置文件名--对象
|
||||
*/
|
||||
public static Map<String, MappingConfig> load(Properties envProperties) {
|
||||
logger.info("## Start loading clickhouse mapping config ... ");
|
||||
|
||||
Map<String, MappingConfig> result = new LinkedHashMap<>();
|
||||
|
||||
Map<String, String> configContentMap = MappingConfigsLoader.loadConfigs("clickhouse");
|
||||
configContentMap.forEach((fileName, content) -> {
|
||||
MappingConfig config = YamlUtils.ymlToObj(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("## ClickHouse mapping config loaded");
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
package com.alibaba.otter.canal.client.adapter.clickhouse.config;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
|
||||
import com.alibaba.otter.canal.client.adapter.support.AdapterConfig;
|
||||
|
||||
/**
|
||||
* CLICKHOUSE表映射配置
|
||||
*
|
||||
* @author: Xander
|
||||
* @date: Created in 2023/11/10 22:23
|
||||
* @email: zhrunxin33@gmail.com
|
||||
* @version 1.1.8
|
||||
*/
|
||||
public class MappingConfig implements AdapterConfig {
|
||||
|
||||
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映射配置
|
||||
|
||||
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 AdapterMapping getMapping() {
|
||||
return dbMapping;
|
||||
}
|
||||
|
||||
public void validate() {
|
||||
if (dbMapping.database == null || dbMapping.database.isEmpty()) {
|
||||
throw new NullPointerException("dbMapping.database");
|
||||
}
|
||||
if (!dbMapping.getMirrorDb() && (dbMapping.table == null || dbMapping.table.isEmpty())) {
|
||||
throw new NullPointerException("dbMapping.table");
|
||||
}
|
||||
if (!dbMapping.getMirrorDb() && (dbMapping.targetTable == null || dbMapping.targetTable.isEmpty())) {
|
||||
throw new NullPointerException("dbMapping.targetTable");
|
||||
}
|
||||
}
|
||||
|
||||
public static class DbMapping implements AdapterMapping {
|
||||
|
||||
private boolean mirrorDb = false; // 是否镜像库
|
||||
private String database; // 数据库名或schema名
|
||||
private String table; // 表名
|
||||
private Map<String, String> targetPk = new LinkedHashMap<>(); // 目标表主键字段
|
||||
private boolean mapAll = false; // 映射所有字段
|
||||
private String targetDb; // 目标库名
|
||||
private String targetTable; // 目标表名
|
||||
private Map<String, String> targetColumns; // 目标表字段映射
|
||||
|
||||
private boolean caseInsensitive = false; // 目标表不区分大小写,默认是否
|
||||
|
||||
private String etlCondition; // etl条件sql
|
||||
|
||||
private int readBatch = 5000;
|
||||
private int commitBatch = 5000; // etl等批量提交大小
|
||||
|
||||
private Map<String, String> allMapColumns;
|
||||
|
||||
public boolean getMirrorDb() {
|
||||
return mirrorDb;
|
||||
}
|
||||
|
||||
public void setMirrorDb(boolean mirrorDb) {
|
||||
this.mirrorDb = mirrorDb;
|
||||
}
|
||||
|
||||
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<String, String> getTargetPk() {
|
||||
return targetPk;
|
||||
}
|
||||
|
||||
public void setTargetPk(Map<String, String> targetPk) {
|
||||
this.targetPk = targetPk;
|
||||
}
|
||||
|
||||
public Boolean getMapAll() {
|
||||
return mapAll;
|
||||
}
|
||||
|
||||
public void setMapAll(Boolean mapAll) {
|
||||
this.mapAll = mapAll;
|
||||
}
|
||||
|
||||
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<String, String> getTargetColumns() {
|
||||
if (targetColumns != null) {
|
||||
targetColumns.forEach((key, value) -> {
|
||||
if (StringUtils.isEmpty(value)) {
|
||||
targetColumns.put(key, key);
|
||||
}
|
||||
});
|
||||
}
|
||||
return targetColumns;
|
||||
}
|
||||
|
||||
public void setTargetColumns(Map<String, String> targetColumns) {
|
||||
this.targetColumns = targetColumns;
|
||||
}
|
||||
|
||||
public boolean isCaseInsensitive() {
|
||||
return caseInsensitive;
|
||||
}
|
||||
|
||||
public void setCaseInsensitive(boolean caseInsensitive) {
|
||||
this.caseInsensitive = caseInsensitive;
|
||||
}
|
||||
|
||||
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<String, String> getAllMapColumns() {
|
||||
return allMapColumns;
|
||||
}
|
||||
|
||||
public void setAllMapColumns(Map<String, String> allMapColumns) {
|
||||
this.allMapColumns = allMapColumns;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.alibaba.otter.canal.client.adapter.clickhouse.config;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* @author: Xander
|
||||
* @date: Created in 2023/11/10 22:23
|
||||
* @email: zhrunxin33@gmail.com
|
||||
* @version 1.1.8
|
||||
*/
|
||||
|
||||
public class MirrorDbConfig {
|
||||
|
||||
private String fileName;
|
||||
private MappingConfig mappingConfig;
|
||||
private Map<String, MappingConfig> tableConfig = new ConcurrentHashMap<>();
|
||||
|
||||
public static MirrorDbConfig create(String fileName, MappingConfig mappingConfig) {
|
||||
return new MirrorDbConfig(fileName, mappingConfig);
|
||||
}
|
||||
|
||||
public MirrorDbConfig(String fileName, MappingConfig mappingConfig){
|
||||
this.fileName = fileName;
|
||||
this.mappingConfig = mappingConfig;
|
||||
}
|
||||
|
||||
public String getFileName() {
|
||||
return fileName;
|
||||
}
|
||||
|
||||
public void setFileName(String fileName) {
|
||||
this.fileName = fileName;
|
||||
}
|
||||
|
||||
public MappingConfig getMappingConfig() {
|
||||
return mappingConfig;
|
||||
}
|
||||
|
||||
public void setMappingConfig(MappingConfig mappingConfig) {
|
||||
this.mappingConfig = mappingConfig;
|
||||
}
|
||||
|
||||
public Map<String, MappingConfig> getTableConfig() {
|
||||
return tableConfig;
|
||||
}
|
||||
|
||||
public void setTableConfig(Map<String, MappingConfig> tableConfig) {
|
||||
this.tableConfig = tableConfig;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
package com.alibaba.otter.canal.client.adapter.clickhouse.monitor;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Properties;
|
||||
|
||||
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.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.alibaba.otter.canal.client.adapter.clickhouse.ClickHouseAdapter;
|
||||
import com.alibaba.otter.canal.client.adapter.clickhouse.config.MappingConfig;
|
||||
import com.alibaba.otter.canal.client.adapter.support.MappingConfigsLoader;
|
||||
import com.alibaba.otter.canal.client.adapter.support.Util;
|
||||
import com.alibaba.otter.canal.client.adapter.support.YamlUtils;
|
||||
|
||||
/**
|
||||
* @author: Xander
|
||||
* @date: Created in 2023/11/10 22:23
|
||||
* @email: zhrunxin33@gmail.com
|
||||
* @version 1.1.8
|
||||
*/
|
||||
|
||||
public class ClickHouseConfigMonitor {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ClickHouseConfigMonitor.class);
|
||||
|
||||
private static final String adapterName = "clickhouse";
|
||||
|
||||
private String key;
|
||||
|
||||
private ClickHouseAdapter clickHouseAdapter;
|
||||
|
||||
private Properties envProperties;
|
||||
|
||||
private FileAlterationMonitor fileMonitor;
|
||||
|
||||
public void init(String key, ClickHouseAdapter clickHouseAdapter, Properties envProperties) {
|
||||
this.key = key;
|
||||
this.clickHouseAdapter = clickHouseAdapter;
|
||||
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 = YamlUtils
|
||||
.ymlToObj(null, configContent, MappingConfig.class, null, envProperties);
|
||||
if (config == null) {
|
||||
return;
|
||||
}
|
||||
config.validate();
|
||||
boolean result = clickHouseAdapter.addConfig(file.getName(), config);
|
||||
if (result) {
|
||||
logger.info("Add a new clickhouse 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 (clickHouseAdapter.getClickHouseMapping().containsKey(file.getName())) {
|
||||
// 加载配置文件
|
||||
String configContent = MappingConfigsLoader
|
||||
.loadConfig(adapterName + File.separator + file.getName());
|
||||
if (configContent == null) {
|
||||
onFileDelete(file);
|
||||
return;
|
||||
}
|
||||
MappingConfig config = YamlUtils
|
||||
.ymlToObj(null, configContent, MappingConfig.class, null, envProperties);
|
||||
if (config == null) {
|
||||
return;
|
||||
}
|
||||
config.validate();
|
||||
clickHouseAdapter.updateConfig(file.getName(), config);
|
||||
logger.info("Change a clickhouse 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 (clickHouseAdapter.getClickHouseMapping().containsKey(file.getName())) {
|
||||
clickHouseAdapter.deleteConfig(file.getName());
|
||||
|
||||
logger.info("Delete a clickhouse mapping config: {} of canal adapter", file.getName());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,555 @@
|
||||
package com.alibaba.otter.canal.client.adapter.clickhouse.service;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.ResultSetMetaData;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Types;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.alibaba.druid.pool.DruidDataSource;
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONWriter.Feature;
|
||||
import com.alibaba.otter.canal.client.adapter.clickhouse.config.MappingConfig;
|
||||
import com.alibaba.otter.canal.client.adapter.clickhouse.config.MappingConfig.DbMapping;
|
||||
import com.alibaba.otter.canal.client.adapter.clickhouse.support.BatchExecutor;
|
||||
import com.alibaba.otter.canal.client.adapter.clickhouse.support.SingleDml;
|
||||
import com.alibaba.otter.canal.client.adapter.clickhouse.support.SyncUtil;
|
||||
import com.alibaba.otter.canal.client.adapter.support.Dml;
|
||||
import com.alibaba.otter.canal.client.adapter.support.Util;
|
||||
|
||||
/**
|
||||
* ClickHouse batch synchronize
|
||||
*
|
||||
* @author: Xander
|
||||
* @date: Created in 2023/11/10 22:23
|
||||
* @email: zhrunxin33@gmail.com
|
||||
* @version 1.1.8
|
||||
*/
|
||||
public class ClickHouseBatchSyncService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ClickHouseBatchSyncService.class);
|
||||
|
||||
private DruidDataSource dataSource;
|
||||
|
||||
private Map<String, Map<String, Integer>> columnsTypeCache; // Cache of instance.schema.table -> <columnName, jdbcType>
|
||||
|
||||
private Map<MappingConfig, List<SingleDml>>[] bufferPools; // Data buffer pool store sync data, List<Dml> dispersed as arrays according to hash value
|
||||
|
||||
private BatchExecutor[] batchExecutors; // Batch Executor
|
||||
|
||||
private BatchExecutor alterExecutors; // Alter Single Executor(update/delete/truncate)
|
||||
|
||||
private ExecutorService[] executorThreads; // Be initialized once
|
||||
|
||||
private ScheduledExecutorService[] scheduledExecutors;
|
||||
|
||||
private int threads = 3; // Default parallel thread count
|
||||
private int batchSize = 1000;
|
||||
private long scheduleTime = 10;
|
||||
private boolean skipDupException;
|
||||
|
||||
public Map<String, Map<String, Integer>> getColumnsTypeCache() {
|
||||
return columnsTypeCache;
|
||||
}
|
||||
|
||||
public ClickHouseBatchSyncService(DruidDataSource dataSource, Integer threads, Integer batchSize, Long scheduleTime, boolean skipDupException){
|
||||
this(dataSource, threads, batchSize, scheduleTime, new ConcurrentHashMap<>(), skipDupException);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public ClickHouseBatchSyncService(DruidDataSource dataSource, Integer threads, Integer batchSize, Long scheduleTime, Map<String, Map<String, Integer>> columnsTypeCache,
|
||||
boolean skipDupException){
|
||||
this.dataSource = dataSource;
|
||||
this.columnsTypeCache = columnsTypeCache;
|
||||
this.skipDupException = skipDupException;
|
||||
try {
|
||||
if (threads != null) {
|
||||
this.threads = threads;
|
||||
}
|
||||
if (batchSize != null) {
|
||||
this.batchSize = batchSize;
|
||||
}
|
||||
if (scheduleTime != null) {
|
||||
this.scheduleTime = scheduleTime;
|
||||
}
|
||||
this.alterExecutors = new BatchExecutor(dataSource);
|
||||
this.bufferPools = new ConcurrentHashMap[this.threads];
|
||||
this.batchExecutors = new BatchExecutor[this.threads];
|
||||
this.executorThreads = new ExecutorService[this.threads];
|
||||
this.scheduledExecutors = new ScheduledExecutorService[this.threads];
|
||||
for (int i = 0; i < this.threads; i++) {
|
||||
bufferPools[i] = new ConcurrentHashMap<>();
|
||||
batchExecutors[i] = new BatchExecutor(dataSource);
|
||||
executorThreads[i] = Executors.newSingleThreadExecutor();
|
||||
scheduledExecutors[i] = Executors.newSingleThreadScheduledExecutor();
|
||||
}
|
||||
scheduleBatchSync();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Timing-driven event
|
||||
* start schedule batch sync threadPool
|
||||
*/
|
||||
private void scheduleBatchSync() {
|
||||
for (int i = 0; i < scheduledExecutors.length; i++) {
|
||||
int index = i;
|
||||
scheduledExecutors[i].scheduleAtFixedRate(()->{
|
||||
List<Future<Boolean>> futures = new ArrayList<>();
|
||||
for (MappingConfig mappingConfig : bufferPools[index].keySet()) {
|
||||
List<SingleDml> dmls = bufferPools[index].get(mappingConfig);
|
||||
if (dmls == null || dmls.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
List<SingleDml> tempDmls;
|
||||
synchronized (dmls) {
|
||||
tempDmls = new ArrayList<>(dmls);
|
||||
dmls.clear();
|
||||
}
|
||||
futures.add(executorThreads[index].submit(()->{
|
||||
try {
|
||||
insert(batchExecutors[index], mappingConfig, tempDmls);
|
||||
batchExecutors[index].commit();
|
||||
return true;
|
||||
} catch (Exception e){
|
||||
batchExecutors[index].rollback();
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}));
|
||||
}
|
||||
}, 0, scheduleTime, TimeUnit.SECONDS);
|
||||
}
|
||||
logger.info("Schedule batch executors has started successfully!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量同步回调
|
||||
*
|
||||
* @param dmls 批量 DML
|
||||
* @param function 回调方法
|
||||
*/
|
||||
public void sync(List<Dml> dmls, Function<Dml, Boolean> function) {
|
||||
boolean toExecute = false;
|
||||
for (Dml dml : dmls) {
|
||||
if (!toExecute) {
|
||||
toExecute = function.apply(dml);
|
||||
} else {
|
||||
function.apply(dml);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Distribute dmls into different partition
|
||||
*
|
||||
* @param mappingConfig {@link com.alibaba.otter.canal.client.adapter.clickhouse.ClickHouseAdapter#mappingConfigCache }
|
||||
* @param dmls received DML
|
||||
*/
|
||||
public void sync(Map<String, Map<String, MappingConfig>> mappingConfig, List<Dml> dmls, Properties envProperties) {
|
||||
sync(dmls, dml -> {
|
||||
if (dml.getIsDdl() != null && dml.getIsDdl() && StringUtils.isNotEmpty(dml.getSql())) {
|
||||
// DDL(Cache need to update when DDL was executed)
|
||||
columnsTypeCache.remove(dml.getDestination() + "." + dml.getDatabase() + "." + dml.getTable());
|
||||
return false;
|
||||
} else {
|
||||
// DML
|
||||
String destination = StringUtils.trimToEmpty(dml.getDestination());
|
||||
String groupId = StringUtils.trimToEmpty(dml.getGroupId());
|
||||
String database = dml.getDatabase();
|
||||
String table = dml.getTable();
|
||||
Map<String, MappingConfig> configMap;
|
||||
if (envProperties != null && !"tcp".equalsIgnoreCase(envProperties.getProperty("canal.conf.mode"))) {
|
||||
configMap = mappingConfig.get(destination + "-" + groupId + "_" + database + "-" + table);
|
||||
} else {
|
||||
configMap = mappingConfig.get(destination + "_" + database + "-" + table);
|
||||
}
|
||||
|
||||
if (configMap == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (configMap.values().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (MappingConfig config : configMap.values()) {
|
||||
distributeDml(config, dml);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
/**
|
||||
* Dml distributor
|
||||
*/
|
||||
private void distributeDml(MappingConfig config, Dml dml) {
|
||||
if (config != null) {
|
||||
try {
|
||||
String type = dml.getType();
|
||||
if (type == null) return;
|
||||
|
||||
if (type.equalsIgnoreCase("INSERT")) {
|
||||
appendDmlBufferPartition(config, dml);
|
||||
} else {
|
||||
boolean caseInsensitive = config.getDbMapping().isCaseInsensitive();
|
||||
List<SingleDml> singleDmls = SingleDml.dml2SingleDmls(dml, caseInsensitive);
|
||||
|
||||
if (type.equalsIgnoreCase("UPDATE")) {
|
||||
for (SingleDml singleDml : singleDmls) {
|
||||
update(alterExecutors, config, singleDml);
|
||||
}
|
||||
} else if (type.equalsIgnoreCase("DELETE")) {
|
||||
for (SingleDml singleDml : singleDmls) {
|
||||
delete(alterExecutors, config, singleDml);
|
||||
}
|
||||
} else if (type.equalsIgnoreCase("TRUNCATE")) {
|
||||
truncate(alterExecutors, config);
|
||||
}
|
||||
}
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("DML: {}", JSON.toJSONString(dml, Feature.WriteNulls));
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void appendDmlBufferPartition(MappingConfig config, Dml dml) {
|
||||
boolean caseInsensitive = config.getDbMapping().isCaseInsensitive();
|
||||
List<SingleDml> singleDmls = SingleDml.dml2SingleDmls(dml, caseInsensitive);
|
||||
|
||||
singleDmls.forEach(singleDml -> {
|
||||
int hash = mappingHash(config.getDbMapping());
|
||||
if (!config.getConcurrent()) {
|
||||
hash = 0;
|
||||
}
|
||||
List<SingleDml> dmls = bufferPools[hash].computeIfAbsent(config, k -> new ArrayList<>());
|
||||
synchronized (dmls) {
|
||||
dmls.add(singleDml);
|
||||
logger.info("Append one data into pool, id {}", singleDml.getData().get("id"));
|
||||
}
|
||||
// Check the size of the List, achieve when it reaches the maximum value
|
||||
if (dmls.size() >= batchSize) syncToClickHouse(config, hash);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* sync when size of list{@link #bufferPools } reaches the maximum value
|
||||
*
|
||||
* @param config key
|
||||
* @param index parallel thread index
|
||||
*/
|
||||
private void syncToClickHouse(MappingConfig config, int index) {
|
||||
List<SingleDml> dmls = bufferPools[index].get(config);
|
||||
logger.info("schema:{} table:{} reaches the maximum value, ready to synchronize, size {}", config.getDbMapping().getDatabase(), config.getDbMapping().getTable(), dmls.size());
|
||||
if (dmls ==null || dmls.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
List<SingleDml> tempDmls;
|
||||
synchronized (dmls) {
|
||||
tempDmls = new ArrayList<>(dmls);
|
||||
dmls.clear();
|
||||
}
|
||||
executorThreads[index].submit(() -> {
|
||||
try {
|
||||
insert(batchExecutors[index], config, tempDmls);
|
||||
batchExecutors[index].commit();
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
batchExecutors[index].rollback();
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert
|
||||
*
|
||||
* @param batchExecutor batch translational executor
|
||||
* @param config corresponding configuration object
|
||||
* @param dmls DMLs
|
||||
*/
|
||||
private void insert(BatchExecutor batchExecutor, MappingConfig config, List<SingleDml> dmls) throws SQLException {
|
||||
if (dmls == null || dmls.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
List<SingleDml> clearDmls = dmls.stream().filter(e -> e.getData() != null && !e.getData().isEmpty()).collect(Collectors.toList());
|
||||
if (clearDmls == null || clearDmls.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
DbMapping dbMapping = config.getDbMapping();
|
||||
String backtick = SyncUtil.getBacktickByDbType(dataSource.getDbType());
|
||||
Map<String, String> columnsMap = SyncUtil.getColumnsMap(dbMapping, clearDmls.get(0).getData());
|
||||
|
||||
StringBuilder insertSql = new StringBuilder();
|
||||
insertSql.append("INSERT INTO ").append(SyncUtil.getDbTableName(dbMapping, dataSource.getDbType())).append(" (");
|
||||
|
||||
columnsMap.forEach((targetColumnName, srcColumnName) -> insertSql.append(backtick)
|
||||
.append(targetColumnName)
|
||||
.append(backtick)
|
||||
.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(")");
|
||||
|
||||
Map<String, Integer> ctype = getTargetColumnType(batchExecutor.getConn(), config);
|
||||
|
||||
List<List<Map<String, ?>>> values = new ArrayList<>();
|
||||
boolean flag = false;
|
||||
for (Map.Entry<String, String> entry : columnsMap.entrySet()) {
|
||||
String targetColumnName = entry.getKey();
|
||||
String srcColumnName = entry.getValue();
|
||||
if (srcColumnName == null) {
|
||||
srcColumnName = Util.cleanColumn(targetColumnName);
|
||||
}
|
||||
|
||||
Integer type = ctype.get(Util.cleanColumn(targetColumnName).toLowerCase());
|
||||
if (type == null) {
|
||||
throw new RuntimeException("Target column: " + targetColumnName + " not matched");
|
||||
}
|
||||
for (int i = 0; i < clearDmls.size(); i++) {
|
||||
Map<String, Object> dmlData = clearDmls.get(i).getData();
|
||||
List<Map<String, ?>> item;
|
||||
if (flag == false) {
|
||||
item = new ArrayList<>();
|
||||
values.add(item);
|
||||
} else {
|
||||
item = values.get(i);
|
||||
}
|
||||
Object value = dmlData.get(srcColumnName);
|
||||
BatchExecutor.setValue(item, type, value);
|
||||
}
|
||||
flag = true;
|
||||
}
|
||||
|
||||
try {
|
||||
batchExecutor.batchExecute(insertSql.toString(), values);
|
||||
} catch (SQLException e) {
|
||||
if (skipDupException
|
||||
&& (e.getMessage().contains("Duplicate entry") || e.getMessage().startsWith("ORA-00001:"))) {
|
||||
// ignore
|
||||
// TODO 增加更多关系数据库的主键冲突的错误码
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Insert into target table, sql: {}", insertSql);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新操作
|
||||
*
|
||||
* @param config 配置项
|
||||
* @param dml DML数据
|
||||
*/
|
||||
private void update(BatchExecutor batchExecutor, MappingConfig config, SingleDml dml) throws SQLException {
|
||||
Map<String, Object> data = dml.getData();
|
||||
if (data == null || data.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Map<String, Object> old = dml.getOld();
|
||||
if (old == null || old.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
DbMapping dbMapping = config.getDbMapping();
|
||||
String backtick = SyncUtil.getBacktickByDbType(dataSource.getDbType());
|
||||
Map<String, String> columnsMap = SyncUtil.getColumnsMap(dbMapping, data);
|
||||
|
||||
Map<String, Integer> ctype = getTargetColumnType(batchExecutor.getConn(), config);
|
||||
|
||||
StringBuilder updateSql = new StringBuilder();
|
||||
updateSql.append("ALTER TABLE ").append(SyncUtil.getDbTableName(dbMapping, dataSource.getDbType())).append(" UPDATE ");
|
||||
List<Map<String, ?>> values = new ArrayList<>();
|
||||
boolean hasMatched = false;
|
||||
for (String srcColumnName : old.keySet()) {
|
||||
List<String> targetColumnNames = new ArrayList<>();
|
||||
columnsMap.forEach((targetColumn, srcColumn) -> {
|
||||
if (srcColumnName.equalsIgnoreCase(srcColumn)) {
|
||||
targetColumnNames.add(targetColumn);
|
||||
}
|
||||
});
|
||||
if (!targetColumnNames.isEmpty()) {
|
||||
hasMatched = true;
|
||||
for (String targetColumnName : targetColumnNames) {
|
||||
updateSql.append(backtick).append(targetColumnName).append(backtick).append("=?, ");
|
||||
Integer type = ctype.get(Util.cleanColumn(targetColumnName).toLowerCase());
|
||||
if (type == null) {
|
||||
throw new RuntimeException("Target column: " + targetColumnName + " not matched");
|
||||
}
|
||||
BatchExecutor.setValue(values, type, data.get(srcColumnName));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!hasMatched) {
|
||||
logger.warn("Did not matched any columns to update ");
|
||||
return;
|
||||
}
|
||||
int len = updateSql.length();
|
||||
updateSql.delete(len - 2, len).append(" WHERE ");
|
||||
|
||||
// 拼接主键
|
||||
appendCondition(dbMapping, updateSql, ctype, values, data, old);
|
||||
batchExecutor.execute(updateSql.toString(), values);
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Alter update target table, sql: {}", updateSql);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除操作
|
||||
*
|
||||
* @param config
|
||||
* @param dml
|
||||
*/
|
||||
private void delete(BatchExecutor batchExecutor, MappingConfig config, SingleDml dml) throws SQLException {
|
||||
Map<String, Object> data = dml.getData();
|
||||
if (data == null || data.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
DbMapping dbMapping = config.getDbMapping();
|
||||
Map<String, Integer> ctype = getTargetColumnType(batchExecutor.getConn(), config);
|
||||
|
||||
StringBuilder sql = new StringBuilder();
|
||||
sql.append("ALTER TABLE ").append(SyncUtil.getDbTableName(dbMapping, dataSource.getDbType())).append(" DELETE WHERE ");
|
||||
|
||||
List<Map<String, ?>> values = new ArrayList<>();
|
||||
// 拼接主键
|
||||
appendCondition(dbMapping, sql, ctype, values, data);
|
||||
batchExecutor.execute(sql.toString(), values);
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Alter delete from target table, sql: {}", sql);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* truncate操作
|
||||
*
|
||||
* @param config
|
||||
*/
|
||||
private void truncate(BatchExecutor batchExecutor, MappingConfig config) throws SQLException {
|
||||
DbMapping dbMapping = config.getDbMapping();
|
||||
StringBuilder sql = new StringBuilder();
|
||||
sql.append("TRUNCATE TABLE ").append(SyncUtil.getDbTableName(dbMapping, dataSource.getDbType()));
|
||||
batchExecutor.execute(sql.toString(), new ArrayList<>());
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Truncate target table, sql: {}", sql);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取目标字段类型
|
||||
*
|
||||
* @param conn sql connection
|
||||
* @param config 映射配置
|
||||
* @return 字段sqlType
|
||||
*/
|
||||
private Map<String, Integer> getTargetColumnType(Connection conn, MappingConfig config) {
|
||||
DbMapping dbMapping = config.getDbMapping();
|
||||
String cacheKey = config.getDestination() + "." + dbMapping.getDatabase() + "." + dbMapping.getTable();
|
||||
Map<String, Integer> columnType = columnsTypeCache.get(cacheKey);
|
||||
if (columnType == null) {
|
||||
synchronized (ClickHouseBatchSyncService.class) {
|
||||
columnType = columnsTypeCache.get(cacheKey);
|
||||
if (columnType == null) {
|
||||
columnType = new LinkedHashMap<>();
|
||||
final Map<String, Integer> columnTypeTmp = columnType;
|
||||
String sql = "SELECT * FROM " + SyncUtil.getDbTableName(dbMapping, dataSource.getDbType()) + " WHERE 1=2";
|
||||
Util.sqlRS(conn, sql, rs -> {
|
||||
try {
|
||||
ResultSetMetaData rsd = rs.getMetaData();
|
||||
int columnCount = rsd.getColumnCount();
|
||||
for (int i = 1; i <= columnCount; i++) {
|
||||
int colType = rsd.getColumnType(i);
|
||||
// 修复year类型作为date处理时的data truncated问题
|
||||
if ("YEAR".equals(rsd.getColumnTypeName(i))) {
|
||||
colType = Types.VARCHAR;
|
||||
}
|
||||
columnTypeTmp.put(rsd.getColumnName(i).toLowerCase(), colType);
|
||||
}
|
||||
columnsTypeCache.put(cacheKey, columnTypeTmp);
|
||||
} catch (SQLException e) {
|
||||
logger.error(e.getMessage(), e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return columnType;
|
||||
}
|
||||
|
||||
/**
|
||||
* 拼接主键 where条件
|
||||
*/
|
||||
private void appendCondition(MappingConfig.DbMapping dbMapping, StringBuilder sql, Map<String, Integer> ctype,
|
||||
List<Map<String, ?>> values, Map<String, Object> d) {
|
||||
appendCondition(dbMapping, sql, ctype, values, d, null);
|
||||
}
|
||||
|
||||
private void appendCondition(MappingConfig.DbMapping dbMapping, StringBuilder sql, Map<String, Integer> ctype,
|
||||
List<Map<String, ?>> values, Map<String, Object> d, Map<String, Object> o) {
|
||||
String backtick = SyncUtil.getBacktickByDbType(dataSource.getDbType());
|
||||
|
||||
// 拼接主键
|
||||
for (Map.Entry<String, String> entry : dbMapping.getTargetPk().entrySet()) {
|
||||
String targetColumnName = entry.getKey();
|
||||
String srcColumnName = entry.getValue();
|
||||
if (srcColumnName == null) {
|
||||
srcColumnName = Util.cleanColumn(targetColumnName);
|
||||
}
|
||||
sql.append(backtick).append(targetColumnName).append(backtick).append("=? AND ");
|
||||
Integer type = ctype.get(Util.cleanColumn(targetColumnName).toLowerCase());
|
||||
if (type == null) {
|
||||
throw new RuntimeException("Target column: " + targetColumnName + " not matched");
|
||||
}
|
||||
// 如果有修改主键的情况
|
||||
if (o != null && o.containsKey(srcColumnName)) {
|
||||
BatchExecutor.setValue(values, type, o.get(srcColumnName));
|
||||
} else {
|
||||
BatchExecutor.setValue(values, type, d.get(srcColumnName));
|
||||
}
|
||||
}
|
||||
int len = sql.length();
|
||||
sql.delete(len - 4, len);
|
||||
}
|
||||
|
||||
/**
|
||||
* make sure the same table in one index
|
||||
*
|
||||
* @param dbMapping
|
||||
* @return
|
||||
*/
|
||||
private int mappingHash(MappingConfig.DbMapping dbMapping) {
|
||||
int hash = dbMapping.getDatabase().toLowerCase().hashCode() + dbMapping.getTable().toLowerCase().hashCode();
|
||||
hash = Math.abs(hash) % threads;
|
||||
return Math.abs(hash);
|
||||
}
|
||||
|
||||
public void close() {
|
||||
for (int i = 0; i < threads; i++) {
|
||||
executorThreads[i].shutdown();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
package com.alibaba.otter.canal.client.adapter.clickhouse.service;
|
||||
|
||||
import java.sql.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
import com.alibaba.druid.pool.DruidDataSource;
|
||||
import com.alibaba.otter.canal.client.adapter.clickhouse.config.MappingConfig;
|
||||
import com.alibaba.otter.canal.client.adapter.clickhouse.config.MappingConfig.DbMapping;
|
||||
import com.alibaba.otter.canal.client.adapter.clickhouse.support.SyncUtil;
|
||||
import com.alibaba.otter.canal.client.adapter.support.*;
|
||||
|
||||
/**
|
||||
* ClickHouse ETL 操作业务类
|
||||
*
|
||||
* @author: Xander
|
||||
* @date: Created in 2023/11/10 22:23
|
||||
* @email: zhrunxin33@gmail.com
|
||||
* @version 1.1.8
|
||||
*/
|
||||
public class ClickHouseEtlService extends AbstractEtlService {
|
||||
|
||||
private DataSource targetDS;
|
||||
private MappingConfig config;
|
||||
|
||||
public ClickHouseEtlService(DataSource targetDS, MappingConfig config){
|
||||
super("CLICKHOUSE", config);
|
||||
this.targetDS = targetDS;
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入数据
|
||||
*/
|
||||
public EtlResult importData(List<String> params) {
|
||||
DbMapping dbMapping = config.getDbMapping();
|
||||
DruidDataSource dataSource = DatasourceConfig.DATA_SOURCES.get(config.getDataSourceKey());
|
||||
String sql = "SELECT * FROM " + SyncUtil.getSourceDbTableName(dbMapping, dataSource.getDbType());
|
||||
return importData(sql, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行导入
|
||||
*/
|
||||
protected boolean executeSqlImport(DataSource srcDS, String sql, List<Object> values,
|
||||
AdapterConfig.AdapterMapping mapping, AtomicLong impCount, List<String> errMsg) {
|
||||
try {
|
||||
DbMapping dbMapping = (DbMapping) mapping;
|
||||
Map<String, String> columnsMap = new LinkedHashMap<>();
|
||||
Map<String, Integer> columnType = new LinkedHashMap<>();
|
||||
DruidDataSource dataSource = (DruidDataSource) srcDS;
|
||||
String backtick = SyncUtil.getBacktickByDbType(dataSource.getDbType());
|
||||
|
||||
Util.sqlRS(targetDS,
|
||||
"SELECT * FROM " + SyncUtil.getDbTableName(dbMapping, dataSource.getDbType()) + " LIMIT 1 ",
|
||||
rs -> {
|
||||
try {
|
||||
|
||||
ResultSetMetaData rsd = rs.getMetaData();
|
||||
int columnCount = rsd.getColumnCount();
|
||||
List<String> columns = new ArrayList<>();
|
||||
for (int i = 1; i <= columnCount; i++) {
|
||||
columnType.put(rsd.getColumnName(i).toLowerCase(), rsd.getColumnType(i));
|
||||
columns.add(rsd.getColumnName(i));
|
||||
}
|
||||
|
||||
columnsMap.putAll(SyncUtil.getColumnsMap(dbMapping, columns));
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
logger.error(e.getMessage(), e);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
Util.sqlRS(srcDS, sql, values, rs -> {
|
||||
int idx = 1;
|
||||
|
||||
try {
|
||||
boolean completed = false;
|
||||
|
||||
StringBuilder insertSql = new StringBuilder();
|
||||
insertSql.append("INSERT INTO ")
|
||||
.append(SyncUtil.getDbTableName(dbMapping, dataSource.getDbType()))
|
||||
.append(" (");
|
||||
columnsMap.forEach((targetColumnName, srcColumnName) -> insertSql.append(backtick)
|
||||
.append(targetColumnName)
|
||||
.append(backtick)
|
||||
.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(")");
|
||||
logger.info("executeSqlImport sql:{}", insertSql.toString());
|
||||
try (Connection connTarget = targetDS.getConnection();
|
||||
PreparedStatement pstmt = connTarget.prepareStatement(insertSql.toString())) {
|
||||
connTarget.setAutoCommit(false);
|
||||
|
||||
while (rs.next()) {
|
||||
completed = false;
|
||||
|
||||
pstmt.clearParameters();
|
||||
|
||||
// 删除数据
|
||||
Map<String, Object> pkVal = new LinkedHashMap<>();
|
||||
StringBuilder deleteSql = new StringBuilder(
|
||||
"ALTER TABLE " + SyncUtil.getDbTableName(dbMapping, dataSource.getDbType())
|
||||
+ " DELETE WHERE ");
|
||||
appendCondition(dbMapping, deleteSql, pkVal, rs, backtick);
|
||||
try (PreparedStatement pstmt2 = connTarget.prepareStatement(deleteSql.toString())) {
|
||||
int k = 1;
|
||||
for (Object val : pkVal.values()) {
|
||||
pstmt2.setObject(k++, val);
|
||||
}
|
||||
pstmt2.execute();
|
||||
}
|
||||
|
||||
int i = 1;
|
||||
for (Map.Entry<String, String> entry : columnsMap.entrySet()) {
|
||||
String targetColumnName = entry.getKey();
|
||||
String srcColumnName = entry.getValue();
|
||||
if (srcColumnName == null) {
|
||||
srcColumnName = targetColumnName;
|
||||
}
|
||||
|
||||
Integer type = columnType.get(targetColumnName.toLowerCase());
|
||||
Object value = rs.getObject(srcColumnName);
|
||||
if (value != null) {
|
||||
SyncUtil.setPStmt(type, pstmt, value, i);
|
||||
} else {
|
||||
pstmt.setNull(i, type);
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
pstmt.execute();
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Insert into target table, sql: {}", insertSql);
|
||||
}
|
||||
|
||||
if (idx % dbMapping.getCommitBatch() == 0) {
|
||||
connTarget.commit();
|
||||
completed = true;
|
||||
}
|
||||
idx++;
|
||||
impCount.incrementAndGet();
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("successful import count:" + impCount.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<String, Object> values,
|
||||
ResultSet rs, String backtick) throws SQLException {
|
||||
// 拼接主键
|
||||
for (Map.Entry<String, String> entry : dbMapping.getTargetPk().entrySet()) {
|
||||
String targetColumnName = entry.getKey();
|
||||
String srcColumnName = entry.getValue();
|
||||
if (srcColumnName == null) {
|
||||
srcColumnName = targetColumnName;
|
||||
}
|
||||
sql.append(backtick).append(targetColumnName).append(backtick).append("=? AND ");
|
||||
values.put(targetColumnName, rs.getObject(srcColumnName));
|
||||
}
|
||||
int len = sql.length();
|
||||
sql.delete(len - 4, len);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
package com.alibaba.otter.canal.client.adapter.clickhouse.service;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.Statement;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.alibaba.druid.pool.DruidDataSource;
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONWriter.Feature;
|
||||
import com.alibaba.otter.canal.client.adapter.clickhouse.config.MappingConfig;
|
||||
import com.alibaba.otter.canal.client.adapter.clickhouse.config.MirrorDbConfig;
|
||||
import com.alibaba.otter.canal.client.adapter.clickhouse.support.SyncUtil;
|
||||
import com.alibaba.otter.canal.client.adapter.support.Dml;
|
||||
|
||||
/**
|
||||
* CLICKHOUSE镜像库同步操作业务
|
||||
*
|
||||
* @author: Xander
|
||||
* @date: Created in 2023/11/10 22:23
|
||||
* @email: zhrunxin33@gmail.com
|
||||
* @version 1.1.8
|
||||
*/
|
||||
public class ClickHouseMirrorDbBatchSyncService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ClickHouseMirrorDbBatchSyncService.class);
|
||||
|
||||
// 镜像库配置
|
||||
private Map<String, MirrorDbConfig> mirrorDbConfigCache;
|
||||
private DruidDataSource dataSource;
|
||||
// clickhouseSyncService代理
|
||||
private ClickHouseBatchSyncService clickHouseBatchSyncService;
|
||||
|
||||
public ClickHouseMirrorDbBatchSyncService(Map<String, MirrorDbConfig> mirrorDbConfigCache,
|
||||
DruidDataSource dataSource, Integer threads, Integer batchSize,
|
||||
Long scheduleTime, Map<String, Map<String, Integer>> columnsTypeCache,
|
||||
boolean skipDupException){
|
||||
this.mirrorDbConfigCache = mirrorDbConfigCache;
|
||||
this.dataSource = dataSource;
|
||||
this.clickHouseBatchSyncService = new ClickHouseBatchSyncService(dataSource,
|
||||
threads,
|
||||
batchSize,
|
||||
scheduleTime,
|
||||
columnsTypeCache,
|
||||
skipDupException);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量同步方法
|
||||
*
|
||||
* @param dmls 批量 DML,包含DDL
|
||||
*/
|
||||
public void sync(List<Dml> dmls) {
|
||||
List<Dml> dmlList = new ArrayList<>();
|
||||
for (Dml dml : dmls) {
|
||||
String destination = StringUtils.trimToEmpty(dml.getDestination());
|
||||
String database = dml.getDatabase();
|
||||
MirrorDbConfig mirrorDbConfig = mirrorDbConfigCache.get(destination + "." + database);
|
||||
if (mirrorDbConfig == null) {
|
||||
continue;
|
||||
}
|
||||
if (mirrorDbConfig.getMappingConfig() == null) {
|
||||
continue;
|
||||
}
|
||||
if (dml.getGroupId() != null && StringUtils.isNotEmpty(mirrorDbConfig.getMappingConfig().getGroupId())) {
|
||||
if (!mirrorDbConfig.getMappingConfig().getGroupId().equals(dml.getGroupId())) {
|
||||
continue; // 如果groupId不匹配则过滤
|
||||
}
|
||||
}
|
||||
|
||||
if (dml.getIsDdl() != null && dml.getIsDdl() && StringUtils.isNotEmpty(dml.getSql())) {
|
||||
// 确保执行DDL前DML已执行完
|
||||
syncDml(dmlList);
|
||||
dmlList.clear();
|
||||
|
||||
// DDL
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("DDL: {}", JSON.toJSONString(dml, Feature.WriteNulls));
|
||||
}
|
||||
executeDdl(mirrorDbConfig, dml);
|
||||
clickHouseBatchSyncService.getColumnsTypeCache()
|
||||
.remove(destination + "." + database + "." + dml.getTable());
|
||||
mirrorDbConfig.getTableConfig().remove(dml.getTable()); // 删除对应库表配置
|
||||
} else {
|
||||
// DML
|
||||
initMappingConfig(dml.getTable(), mirrorDbConfig.getMappingConfig(), mirrorDbConfig, dml);
|
||||
dmlList.add(dml);
|
||||
}
|
||||
}
|
||||
syncDml(dmlList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量同步Dml
|
||||
*
|
||||
* @param dmlList Dml列表,不包含DDL
|
||||
*/
|
||||
private void syncDml(List<Dml> dmlList) {
|
||||
if (dmlList == null || dmlList.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
clickHouseBatchSyncService.sync(dmlList, dml -> {
|
||||
MirrorDbConfig mirrorDbConfig = mirrorDbConfigCache.get(dml.getDestination() + "." + dml.getDatabase());
|
||||
if (mirrorDbConfig == null) {
|
||||
return false;
|
||||
}
|
||||
String table = dml.getTable();
|
||||
MappingConfig config = mirrorDbConfig.getTableConfig().get(table);
|
||||
|
||||
if (config == null) {
|
||||
return false;
|
||||
}
|
||||
clickHouseBatchSyncService.appendDmlBufferPartition(config, dml);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化表配置
|
||||
*
|
||||
* @param key 配置key: destination.database.table
|
||||
* @param baseConfigMap db sync config
|
||||
* @param dml DML
|
||||
*/
|
||||
private void initMappingConfig(String key, MappingConfig baseConfigMap, MirrorDbConfig mirrorDbConfig, Dml dml) {
|
||||
MappingConfig mappingConfig = mirrorDbConfig.getTableConfig().get(key);
|
||||
if (mappingConfig == null) {
|
||||
// 构造表配置
|
||||
mappingConfig = new MappingConfig();
|
||||
mappingConfig.setDataSourceKey(baseConfigMap.getDataSourceKey());
|
||||
mappingConfig.setDestination(baseConfigMap.getDestination());
|
||||
mappingConfig.setGroupId(baseConfigMap.getGroupId());
|
||||
mappingConfig.setOuterAdapterKey(baseConfigMap.getOuterAdapterKey());
|
||||
mappingConfig.setConcurrent(baseConfigMap.getConcurrent());
|
||||
MappingConfig.DbMapping dbMapping = new MappingConfig.DbMapping();
|
||||
mappingConfig.setDbMapping(dbMapping);
|
||||
dbMapping.setDatabase(dml.getDatabase());
|
||||
dbMapping.setTable(dml.getTable());
|
||||
dbMapping.setTargetDb(dml.getDatabase());
|
||||
dbMapping.setTargetTable(dml.getTable());
|
||||
dbMapping.setMapAll(true);
|
||||
List<String> pkNames = dml.getPkNames();
|
||||
Map<String, String> pkMapping = new LinkedHashMap<>();
|
||||
pkNames.forEach(pkName -> pkMapping.put(pkName, pkName));
|
||||
dbMapping.setTargetPk(pkMapping);
|
||||
|
||||
mirrorDbConfig.getTableConfig().put(key, mappingConfig);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DDL 操作
|
||||
*
|
||||
* @param ddl DDL
|
||||
*/
|
||||
private void executeDdl(MirrorDbConfig mirrorDbConfig, Dml ddl) {
|
||||
try (Connection conn = dataSource.getConnection(); Statement statement = conn.createStatement()) {
|
||||
// 替换反引号
|
||||
String sql = ddl.getSql();
|
||||
String backtick = SyncUtil.getBacktickByDbType(dataSource.getDbType());
|
||||
if (!"`".equals(backtick)) {
|
||||
sql = sql.replaceAll("`", backtick);
|
||||
}
|
||||
statement.execute(sql);
|
||||
// 移除对应配置
|
||||
mirrorDbConfig.getTableConfig().remove(ddl.getTable());
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Execute DDL sql: {} for database: {}", ddl.getSql(), ddl.getDatabase());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package com.alibaba.otter.canal.client.adapter.clickhouse.support;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.SQLException;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* sql批量执行器
|
||||
*
|
||||
* @author rewerma 2018-11-7 下午06:45:49
|
||||
* @version 1.0.0
|
||||
*/
|
||||
public class BatchExecutor implements Closeable {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(BatchExecutor.class);
|
||||
|
||||
private DataSource dataSource;
|
||||
private Connection conn;
|
||||
private AtomicInteger idx = new AtomicInteger(0);
|
||||
|
||||
public BatchExecutor(DataSource dataSource){
|
||||
this.dataSource = dataSource;
|
||||
}
|
||||
|
||||
public Connection getConn() {
|
||||
if (conn == null) {
|
||||
try {
|
||||
conn = dataSource.getConnection();
|
||||
this.conn.setAutoCommit(false);
|
||||
} catch (SQLException e) {
|
||||
logger.error(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
return conn;
|
||||
}
|
||||
|
||||
public static void setValue(List<Map<String, ?>> values, int type, Object value) {
|
||||
Map<String, Object> valueItem = new HashMap<>();
|
||||
valueItem.put("type", type);
|
||||
valueItem.put("value", value);
|
||||
values.add(valueItem);
|
||||
}
|
||||
|
||||
public void execute(String sql, List<Map<String, ?>> values) throws SQLException {
|
||||
PreparedStatement pstmt = getConn().prepareStatement(sql);
|
||||
int len = values.size();
|
||||
for (int i = 0; i < len; i++) {
|
||||
int type = (Integer) values.get(i).get("type");
|
||||
Object value = values.get(i).get("value");
|
||||
SyncUtil.setPStmt(type, pstmt, value, i + 1);
|
||||
}
|
||||
|
||||
pstmt.execute();
|
||||
idx.incrementAndGet();
|
||||
pstmt.close();
|
||||
}
|
||||
|
||||
public void batchExecute(String sql, List<List<Map<String, ?>>> batchValues) throws SQLException {
|
||||
PreparedStatement pstmt = getConn().prepareStatement(sql);
|
||||
for (int i = 0; i < batchValues.size(); i++) {
|
||||
List<Map<String, ?>> values = batchValues.get(i);
|
||||
for (int j = 0; j < values.size(); j++) {
|
||||
int type = (Integer) values.get(j).get("type");
|
||||
Object value = values.get(j).get("value");
|
||||
SyncUtil.setPStmt(type, pstmt, value, j + 1);
|
||||
}
|
||||
pstmt.addBatch();
|
||||
}
|
||||
|
||||
pstmt.executeBatch();
|
||||
idx.addAndGet(batchValues.size());
|
||||
pstmt.close();
|
||||
}
|
||||
|
||||
public void commit() throws SQLException {
|
||||
getConn().commit();
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Batch executor commit " + idx.get() + " rows");
|
||||
}
|
||||
idx.set(0);
|
||||
}
|
||||
|
||||
public void rollback() throws SQLException {
|
||||
getConn().rollback();
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Batch executor rollback " + idx.get() + " rows");
|
||||
}
|
||||
idx.set(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (conn != null) {
|
||||
try {
|
||||
conn.close();
|
||||
} catch (SQLException e) {
|
||||
logger.error(e.getMessage(), e);
|
||||
} finally {
|
||||
conn = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package com.alibaba.otter.canal.client.adapter.clickhouse.support;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.util.LinkedCaseInsensitiveMap;
|
||||
|
||||
import com.alibaba.otter.canal.client.adapter.support.Dml;
|
||||
|
||||
public class SingleDml {
|
||||
|
||||
private String destination;
|
||||
private String database;
|
||||
private String table;
|
||||
private String type;
|
||||
private Map<String, Object> data;
|
||||
private Map<String, Object> old;
|
||||
|
||||
public String getDestination() {
|
||||
return destination;
|
||||
}
|
||||
|
||||
public void setDestination(String destination) {
|
||||
this.destination = destination;
|
||||
}
|
||||
|
||||
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 String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(String type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public Map<String, Object> getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
public void setData(Map<String, Object> data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public Map<String, Object> getOld() {
|
||||
return old;
|
||||
}
|
||||
|
||||
public void setOld(Map<String, Object> old) {
|
||||
this.old = old;
|
||||
}
|
||||
|
||||
public static List<SingleDml> dml2SingleDmls(Dml dml, boolean caseInsensitive) {
|
||||
List<SingleDml> singleDmls = new ArrayList<>();
|
||||
if (dml.getData() != null) {
|
||||
int size = dml.getData().size();
|
||||
for (int i = 0; i < size; i++) {
|
||||
SingleDml singleDml = new SingleDml();
|
||||
singleDml.setDestination(dml.getDestination());
|
||||
singleDml.setDatabase(dml.getDatabase());
|
||||
singleDml.setTable(dml.getTable());
|
||||
singleDml.setType(dml.getType());
|
||||
Map<String, Object> data = dml.getData().get(i);
|
||||
if (caseInsensitive) {
|
||||
data = toCaseInsensitiveMap(data);
|
||||
}
|
||||
singleDml.setData(data);
|
||||
if (dml.getOld() != null) {
|
||||
Map<String, Object> oldData = dml.getOld().get(i);
|
||||
if (caseInsensitive) {
|
||||
oldData = toCaseInsensitiveMap(oldData);
|
||||
}
|
||||
singleDml.setOld(oldData);
|
||||
}
|
||||
singleDmls.add(singleDml);
|
||||
}
|
||||
} else if ("TRUNCATE".equalsIgnoreCase(dml.getType())) {
|
||||
SingleDml singleDml = new SingleDml();
|
||||
singleDml.setDestination(dml.getDestination());
|
||||
singleDml.setDatabase(dml.getDatabase());
|
||||
singleDml.setTable(dml.getTable());
|
||||
singleDml.setType(dml.getType());
|
||||
singleDmls.add(singleDml);
|
||||
}
|
||||
return singleDmls;
|
||||
}
|
||||
|
||||
public static List<SingleDml> dml2SingleDmls(Dml dml) {
|
||||
return dml2SingleDmls(dml, false);
|
||||
}
|
||||
|
||||
private static <V> LinkedCaseInsensitiveMap<V> toCaseInsensitiveMap(Map<String, V> data) {
|
||||
LinkedCaseInsensitiveMap map = new LinkedCaseInsensitiveMap();
|
||||
map.putAll(data);
|
||||
return map;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
package com.alibaba.otter.canal.client.adapter.clickhouse.support;
|
||||
|
||||
import java.io.Reader;
|
||||
import java.io.StringReader;
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.sql.*;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.alibaba.druid.DbType;
|
||||
import com.alibaba.otter.canal.client.adapter.clickhouse.config.MappingConfig;
|
||||
import com.alibaba.otter.canal.client.adapter.support.Util;
|
||||
|
||||
public class SyncUtil {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(SyncUtil.class);
|
||||
|
||||
public static Map<String, String> getColumnsMap(MappingConfig.DbMapping dbMapping, Map<String, Object> data) {
|
||||
return getColumnsMap(dbMapping, data.keySet());
|
||||
}
|
||||
|
||||
public static Map<String, String> getColumnsMap(MappingConfig.DbMapping dbMapping, Collection<String> columns) {
|
||||
Map<String, String> columnsMap;
|
||||
if (dbMapping.getMapAll()) {
|
||||
if (dbMapping.getAllMapColumns() != null) {
|
||||
return dbMapping.getAllMapColumns();
|
||||
}
|
||||
columnsMap = new LinkedHashMap<>();
|
||||
for (String srcColumn : columns) {
|
||||
boolean flag = true;
|
||||
if (dbMapping.getTargetColumns() != null) {
|
||||
for (Map.Entry<String, String> entry : dbMapping.getTargetColumns().entrySet()) {
|
||||
if (srcColumn.equals(entry.getValue())) {
|
||||
columnsMap.put(entry.getKey(), srcColumn);
|
||||
flag = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (flag) {
|
||||
columnsMap.put(srcColumn, srcColumn);
|
||||
}
|
||||
}
|
||||
dbMapping.setAllMapColumns(columnsMap);
|
||||
} else {
|
||||
columnsMap = dbMapping.getTargetColumns();
|
||||
}
|
||||
return columnsMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 preparedStatement
|
||||
*
|
||||
* @param type sqlType
|
||||
* @param pstmt 需要设置的preparedStatement
|
||||
* @param value 值
|
||||
* @param i 索引号
|
||||
*/
|
||||
public static void setPStmt(int type, PreparedStatement pstmt, Object value, int i) throws SQLException {
|
||||
switch (type) {
|
||||
case Types.BIT:
|
||||
case Types.BOOLEAN:
|
||||
if (value instanceof Boolean) {
|
||||
pstmt.setBoolean(i, (Boolean) value);
|
||||
} else if (value instanceof String) {
|
||||
boolean v = !value.equals("0");
|
||||
pstmt.setBoolean(i, v);
|
||||
} else if (value instanceof Number) {
|
||||
boolean v = ((Number) value).intValue() != 0;
|
||||
pstmt.setBoolean(i, v);
|
||||
} else {
|
||||
pstmt.setNull(i, type);
|
||||
}
|
||||
break;
|
||||
case Types.CHAR:
|
||||
case Types.NCHAR:
|
||||
case Types.VARCHAR:
|
||||
case Types.LONGVARCHAR:
|
||||
if (value instanceof String) {
|
||||
pstmt.setString(i, (String) value);
|
||||
} else if (value == null) {
|
||||
pstmt.setNull(i, type);
|
||||
} else {
|
||||
pstmt.setString(i, value.toString());
|
||||
}
|
||||
break;
|
||||
case Types.TINYINT:
|
||||
// 向上提升一级,处理unsigned情况
|
||||
if (value instanceof Number) {
|
||||
pstmt.setShort(i, ((Number) value).shortValue());
|
||||
} else if (value instanceof String) {
|
||||
pstmt.setShort(i, Short.parseShort((String) value));
|
||||
} else {
|
||||
pstmt.setNull(i, type);
|
||||
}
|
||||
break;
|
||||
case Types.SMALLINT:
|
||||
if (value instanceof Number) {
|
||||
pstmt.setInt(i, ((Number) value).intValue());
|
||||
} else if (value instanceof String) {
|
||||
pstmt.setInt(i, Integer.parseInt((String) value));
|
||||
} else {
|
||||
pstmt.setNull(i, type);
|
||||
}
|
||||
break;
|
||||
case Types.INTEGER:
|
||||
if (value instanceof Number) {
|
||||
pstmt.setLong(i, ((Number) value).longValue());
|
||||
} else if (value instanceof String) {
|
||||
pstmt.setLong(i, Long.parseLong((String) value));
|
||||
} else {
|
||||
pstmt.setNull(i, type);
|
||||
}
|
||||
break;
|
||||
case Types.BIGINT:
|
||||
if (value instanceof Number) {
|
||||
pstmt.setBigDecimal(i, new BigDecimal(value.toString()));
|
||||
} else if (value instanceof String) {
|
||||
pstmt.setBigDecimal(i, new BigDecimal(value.toString()));
|
||||
} else {
|
||||
pstmt.setNull(i, type);
|
||||
}
|
||||
break;
|
||||
case Types.DECIMAL:
|
||||
case Types.NUMERIC:
|
||||
if (value instanceof BigDecimal) {
|
||||
pstmt.setBigDecimal(i, (BigDecimal) value);
|
||||
} else if (value instanceof Byte) {
|
||||
pstmt.setInt(i, ((Byte) value).intValue());
|
||||
} else if (value instanceof Short) {
|
||||
pstmt.setInt(i, ((Short) value).intValue());
|
||||
} else if (value instanceof Integer) {
|
||||
pstmt.setInt(i, (Integer) value);
|
||||
} else if (value instanceof Long) {
|
||||
pstmt.setLong(i, (Long) value);
|
||||
} else if (value instanceof Float) {
|
||||
pstmt.setBigDecimal(i, new BigDecimal((float) value));
|
||||
} else if (value instanceof Double) {
|
||||
pstmt.setBigDecimal(i, new BigDecimal((double) value));
|
||||
} else if (value != null) {
|
||||
pstmt.setBigDecimal(i, new BigDecimal(value.toString()));
|
||||
} else {
|
||||
pstmt.setNull(i, type);
|
||||
}
|
||||
break;
|
||||
case Types.REAL:
|
||||
if (value instanceof Number) {
|
||||
pstmt.setFloat(i, ((Number) value).floatValue());
|
||||
} else if (value instanceof String) {
|
||||
pstmt.setFloat(i, Float.parseFloat((String) value));
|
||||
} else {
|
||||
pstmt.setNull(i, type);
|
||||
}
|
||||
break;
|
||||
case Types.FLOAT:
|
||||
case Types.DOUBLE:
|
||||
if (value instanceof Number) {
|
||||
pstmt.setDouble(i, ((Number) value).doubleValue());
|
||||
} else if (value instanceof String) {
|
||||
pstmt.setDouble(i, Double.parseDouble((String) value));
|
||||
} else {
|
||||
pstmt.setNull(i, type);
|
||||
}
|
||||
break;
|
||||
case Types.BINARY:
|
||||
case Types.VARBINARY:
|
||||
case Types.LONGVARBINARY:
|
||||
case Types.BLOB:
|
||||
if (value instanceof Blob) {
|
||||
pstmt.setBlob(i, (Blob) value);
|
||||
} else if (value instanceof byte[]) {
|
||||
pstmt.setBytes(i, (byte[]) value);
|
||||
} else if (value instanceof String) {
|
||||
pstmt.setBytes(i, ((String) value).getBytes(StandardCharsets.ISO_8859_1));
|
||||
} else {
|
||||
pstmt.setNull(i, type);
|
||||
}
|
||||
break;
|
||||
case Types.CLOB:
|
||||
if (value instanceof Clob) {
|
||||
pstmt.setClob(i, (Clob) value);
|
||||
} else if (value instanceof byte[]) {
|
||||
pstmt.setBytes(i, (byte[]) value);
|
||||
} else if (value instanceof String) {
|
||||
Reader clobReader = new StringReader((String) value);
|
||||
pstmt.setCharacterStream(i, clobReader);
|
||||
} else {
|
||||
pstmt.setNull(i, type);
|
||||
}
|
||||
break;
|
||||
case Types.DATE:
|
||||
if (value instanceof Date) {
|
||||
pstmt.setDate(i, (Date) value);
|
||||
} else if (value instanceof java.util.Date) {
|
||||
pstmt.setDate(i, new Date(((java.util.Date) value).getTime()));
|
||||
} else if (value instanceof String) {
|
||||
String v = (String) value;
|
||||
if (!v.startsWith("0000-00-00")) {
|
||||
java.util.Date date = Util.parseDate(v);
|
||||
if (date != null) {
|
||||
pstmt.setDate(i, new Date(date.getTime()));
|
||||
} else {
|
||||
pstmt.setNull(i, type);
|
||||
}
|
||||
} else {
|
||||
pstmt.setObject(i, value);
|
||||
}
|
||||
} else {
|
||||
pstmt.setNull(i, type);
|
||||
}
|
||||
break;
|
||||
case Types.TIME:
|
||||
if (value instanceof Time) {
|
||||
pstmt.setTime(i, (Time) value);
|
||||
} else if (value instanceof java.util.Date) {
|
||||
pstmt.setTime(i, new Time(((java.util.Date) value).getTime()));
|
||||
} else if (value instanceof String) {
|
||||
String v = (String) value;
|
||||
java.util.Date date = Util.parseDate(v);
|
||||
if (date != null) {
|
||||
pstmt.setTime(i, new Time(date.getTime()));
|
||||
} else {
|
||||
pstmt.setNull(i, type);
|
||||
}
|
||||
} else {
|
||||
pstmt.setNull(i, type);
|
||||
}
|
||||
break;
|
||||
case Types.TIMESTAMP:
|
||||
if (value instanceof Timestamp) {
|
||||
pstmt.setTimestamp(i, (Timestamp) value);
|
||||
} else if (value instanceof java.util.Date) {
|
||||
pstmt.setTimestamp(i, new Timestamp(((java.util.Date) value).getTime()));
|
||||
} else if (value instanceof String) {
|
||||
String v = (String) value;
|
||||
if (!v.startsWith("0000-00-00")) {
|
||||
java.util.Date date = Util.parseDate(v);
|
||||
if (date != null) {
|
||||
pstmt.setTimestamp(i, new Timestamp(date.getTime()));
|
||||
} else {
|
||||
pstmt.setNull(i, type);
|
||||
}
|
||||
} else {
|
||||
pstmt.setObject(i, value);
|
||||
}
|
||||
} else {
|
||||
pstmt.setNull(i, type);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
pstmt.setObject(i, value, type);
|
||||
}
|
||||
}
|
||||
|
||||
public static String getDbTableName(MappingConfig.DbMapping dbMapping, String dbType) {
|
||||
String result = "";
|
||||
String backtick = getBacktickByDbType(dbType);
|
||||
if (StringUtils.isNotEmpty(dbMapping.getTargetDb())) {
|
||||
result += (backtick + dbMapping.getTargetDb() + backtick + ".");
|
||||
}
|
||||
result += (backtick + dbMapping.getTargetTable() + backtick);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static String getSourceDbTableName(MappingConfig.DbMapping dbMapping, String dbType) {
|
||||
String result = "";
|
||||
String backtick = getBacktickByDbType(dbType);
|
||||
if (StringUtils.isNotEmpty(dbMapping.getDatabase())) {
|
||||
result += (backtick + dbMapping.getDatabase() + backtick + ".");
|
||||
}
|
||||
|
||||
result += (backtick + dbMapping.getTable() + backtick);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据DbType返回反引号或空字符串
|
||||
*
|
||||
* @param dbTypeName DbType名称
|
||||
* @return 反引号或空字符串
|
||||
*/
|
||||
public static String getBacktickByDbType(String dbTypeName) {
|
||||
DbType dbType = DbType.of(dbTypeName);
|
||||
if (dbType == null) {
|
||||
dbType = DbType.other;
|
||||
}
|
||||
|
||||
// 只有当dbType为MySQL/MariaDB或OceanBase时返回反引号
|
||||
switch (dbType) {
|
||||
case mysql:
|
||||
case mariadb:
|
||||
case oceanbase:
|
||||
return "`";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
clickhouse=com.alibaba.otter.canal.client.adapter.clickhouse.ClickHouseAdapter
|
||||
@@ -0,0 +1,31 @@
|
||||
dataSourceKey: defaultDS
|
||||
destination: example
|
||||
groupId: g1
|
||||
outerAdapterKey: clickhouse1
|
||||
concurrent: true
|
||||
dbMapping:
|
||||
database: mytest
|
||||
table: user
|
||||
targetTable: mytest.user
|
||||
targetPk:
|
||||
id: id
|
||||
# mapAll: true
|
||||
targetColumns:
|
||||
id:
|
||||
name:
|
||||
role_id:
|
||||
c_time:
|
||||
test1:
|
||||
etlCondition: "where c_time>={}"
|
||||
commitBatch: 3000 # 批量提交的大小
|
||||
|
||||
|
||||
## Mirror schema synchronize config
|
||||
#dataSourceKey: defaultDS
|
||||
#destination: example
|
||||
#groupId: g1
|
||||
#outerAdapterKey: mysql1
|
||||
#concurrent: true
|
||||
#dbMapping:
|
||||
# mirrorDb: true
|
||||
# database: mytest
|
||||
@@ -0,0 +1,109 @@
|
||||
package com.alibaba.otter.canal.client.adapter.clickhouse;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
|
||||
import com.alibaba.otter.canal.client.adapter.clickhouse.sync.Common;
|
||||
import com.alibaba.otter.canal.client.adapter.support.Dml;
|
||||
|
||||
@Ignore
|
||||
public class ClickHouseBatchSyncServiceTest {
|
||||
|
||||
private ClickHouseAdapter clickHouseAdapter;
|
||||
|
||||
@Before
|
||||
public void init() {
|
||||
clickHouseAdapter = Common.init();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void insert() throws InterruptedException {
|
||||
Dml dml = new Dml();
|
||||
dml.setDestination("example");
|
||||
dml.setTs(new Date().getTime());
|
||||
dml.setType("INSERT");
|
||||
dml.setDatabase("mytest");
|
||||
dml.setTable("user");
|
||||
List<Map<String, Object>> dataList = new ArrayList<>();
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
dataList.add(data);
|
||||
data.put("id", 1L);
|
||||
data.put("name", "Eric");
|
||||
data.put("role_id", 1L);
|
||||
data.put("c_time", new Date());
|
||||
data.put("test1", "sdfasdfawe中国asfwef");
|
||||
dml.setData(dataList);
|
||||
|
||||
clickHouseAdapter.sync(Collections.singletonList(dml));
|
||||
Thread.sleep(10000L);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void update() {
|
||||
Dml dml = new Dml();
|
||||
dml.setDestination("example");
|
||||
dml.setTs(new Date().getTime());
|
||||
dml.setType("UPDATE");
|
||||
dml.setDatabase("mytest");
|
||||
dml.setTable("user");
|
||||
List<Map<String, Object>> dataList = new ArrayList<>();
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
dataList.add(data);
|
||||
data.put("id", 1L);
|
||||
data.put("name", "Eric2");
|
||||
dml.setData(dataList);
|
||||
List<Map<String, Object>> oldList = new ArrayList<>();
|
||||
Map<String, Object> old = new LinkedHashMap<>();
|
||||
oldList.add(old);
|
||||
old.put("name", "Eric");
|
||||
dml.setOld(oldList);
|
||||
|
||||
clickHouseAdapter.sync(Collections.singletonList(dml));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void delete() {
|
||||
Dml dml = new Dml();
|
||||
dml.setDestination("example");
|
||||
dml.setTs(new Date().getTime());
|
||||
dml.setType("DELETE");
|
||||
dml.setDatabase("mytest");
|
||||
dml.setTable("user");
|
||||
List<Map<String, Object>> dataList = new ArrayList<>();
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
dataList.add(data);
|
||||
data.put("id", 1L);
|
||||
data.put("name", "Eric2");
|
||||
dml.setData(dataList);
|
||||
|
||||
clickHouseAdapter.sync(Collections.singletonList(dml));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void truncate() throws InterruptedException {
|
||||
Dml dml = new Dml();
|
||||
dml.setDestination("example");
|
||||
dml.setTs(new Date().getTime());
|
||||
dml.setType("TRUNCATE");
|
||||
dml.setDatabase("mytest");
|
||||
dml.setTable("user");
|
||||
List<Map<String, Object>> dataList = new ArrayList<>();
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
dataList.add(data);
|
||||
data.put("id", 1L);
|
||||
data.put("name", "Eric2");
|
||||
dml.setData(dataList);
|
||||
List<Map<String, Object>> oldList = new ArrayList<>();
|
||||
Map<String, Object> old = new LinkedHashMap<>();
|
||||
oldList.add(old);
|
||||
old.put("name", "Eric");
|
||||
dml.setOld(oldList);
|
||||
|
||||
clickHouseAdapter.sync(Collections.singletonList(dml));
|
||||
Thread.sleep(1000L);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package com.alibaba.otter.canal.client.adapter.clickhouse;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
|
||||
import com.alibaba.otter.canal.client.adapter.clickhouse.sync.Common;
|
||||
import com.alibaba.otter.canal.client.adapter.support.Dml;
|
||||
|
||||
import ch.qos.logback.classic.Level;
|
||||
|
||||
/**
|
||||
* @author: Xander
|
||||
* @date: Created in 2023/11/13 22:27
|
||||
* @email: zhrunxin33@gmail.com
|
||||
* @description: Testing thread safe
|
||||
*/
|
||||
|
||||
@Ignore
|
||||
public class ClickHouseBatchSyncThreadSafeTest {
|
||||
|
||||
private ClickHouseAdapter clickHouseAdapter;
|
||||
|
||||
private ExecutorService executorService;
|
||||
|
||||
private String[] operations = new String[] { "INSERT", "UPDATE" };
|
||||
|
||||
private String[] tables = new String[] { "user", "customer" };
|
||||
|
||||
@Before
|
||||
public void init() {
|
||||
clickHouseAdapter = Common.init();
|
||||
Common.setLogLevel(Level.INFO);
|
||||
executorService = Executors.newFixedThreadPool(5);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test01() throws InterruptedException, ExecutionException {
|
||||
ArrayList<Future> list = new ArrayList();
|
||||
AtomicInteger count = new AtomicInteger();
|
||||
for (int i = 0; i < 10; i++) {
|
||||
list.add(executorService.submit(() -> {
|
||||
for (int j = 0; j < 300; j++) {
|
||||
Random random = new Random();
|
||||
int cou = count.incrementAndGet();
|
||||
// test insert
|
||||
String dmlType = operations[random.nextInt(1)];
|
||||
Dml dml = new Dml();
|
||||
dml.setDestination("example");
|
||||
dml.setTs(new Date().getTime());
|
||||
dml.setType(dmlType);
|
||||
dml.setDatabase("mytest");
|
||||
dml.setTable(tables[(int) Math.round(Math.random())]);
|
||||
List<Map<String, Object>> dataList = new ArrayList<>();
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
dataList.add(data);
|
||||
data.put("id", cou);
|
||||
data.put("name", "Eric" + cou);
|
||||
data.put("role_id", cou);
|
||||
data.put("c_time", new Date());
|
||||
data.put("test1", "sdfasdfawe中国asfwef");
|
||||
dml.setData(dataList);
|
||||
clickHouseAdapter.sync(Collections.singletonList(dml));
|
||||
}
|
||||
}));
|
||||
|
||||
}
|
||||
for (Future future : list) {
|
||||
future.get();
|
||||
}
|
||||
Thread.sleep(10000L); // waiting multiple threads execute successfully.
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.alibaba.otter.canal.client.adapter.clickhouse;
|
||||
|
||||
import java.sql.SQLException;
|
||||
|
||||
import com.alibaba.druid.pool.DruidDataSource;
|
||||
|
||||
public class TestConstant {
|
||||
|
||||
public final static String jdbcUrl = "jdbc:mysql://127.0.0.1:3306/mytest?useUnicode=true";
|
||||
public final static String jdbcUser = "root";
|
||||
public final static String jdbcPassword = "121212";
|
||||
|
||||
public final static DruidDataSource dataSource;
|
||||
|
||||
static {
|
||||
dataSource = new DruidDataSource();
|
||||
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
|
||||
dataSource.setUrl(jdbcUrl);
|
||||
dataSource.setUsername(jdbcUser);
|
||||
dataSource.setPassword(jdbcPassword);
|
||||
dataSource.setInitialSize(1);
|
||||
dataSource.setMinIdle(1);
|
||||
dataSource.setMaxActive(1);
|
||||
dataSource.setMaxWait(60000);
|
||||
dataSource.setTimeBetweenEvictionRunsMillis(60000);
|
||||
dataSource.setMinEvictableIdleTimeMillis(300000);
|
||||
dataSource.setPoolPreparedStatements(false);
|
||||
dataSource.setMaxPoolPreparedStatementPerConnectionSize(20);
|
||||
dataSource.setValidationQuery("select 1");
|
||||
try {
|
||||
dataSource.init();
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.alibaba.otter.canal.client.adapter.clickhouse.sync;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.alibaba.otter.canal.client.adapter.clickhouse.ClickHouseAdapter;
|
||||
import com.alibaba.otter.canal.client.adapter.clickhouse.TestConstant;
|
||||
import com.alibaba.otter.canal.client.adapter.support.DatasourceConfig;
|
||||
import com.alibaba.otter.canal.client.adapter.support.OuterAdapterConfig;
|
||||
|
||||
import ch.qos.logback.classic.Level;
|
||||
import ch.qos.logback.classic.Logger;
|
||||
|
||||
/**
|
||||
* @author: Xander
|
||||
* @date: Created in 2023/11/13 0:16
|
||||
* @email: zhrunxin33@gmail.com @description:
|
||||
*/
|
||||
public class Common {
|
||||
|
||||
public static ClickHouseAdapter init() {
|
||||
DatasourceConfig.DATA_SOURCES.put("defaultDS", TestConstant.dataSource);
|
||||
|
||||
OuterAdapterConfig outerAdapterConfig = new OuterAdapterConfig();
|
||||
outerAdapterConfig.setName("clickhouse");
|
||||
outerAdapterConfig.setKey("clickhouse1");
|
||||
Map<String, String> properties = new HashMap<>();
|
||||
properties.put("jdbc.driveClassName", "ru.yandex.clickhouse.ClickHouseDriver");
|
||||
properties.put("jdbc.url", "jdbc:clickhouse://127.0.0.1:8123/default");
|
||||
properties.put("jdbc.username", "default");
|
||||
properties.put("jdbc.password", "123456");
|
||||
outerAdapterConfig.setProperties(properties);
|
||||
|
||||
ClickHouseAdapter adapter = new ClickHouseAdapter();
|
||||
adapter.init(outerAdapterConfig, null);
|
||||
return adapter;
|
||||
}
|
||||
|
||||
/**
|
||||
* set global log level
|
||||
*
|
||||
* @param logLevel
|
||||
*/
|
||||
public static void setLogLevel(Level logLevel) {
|
||||
ch.qos.logback.classic.Logger logger = (ch.qos.logback.classic.Logger) LoggerFactory
|
||||
.getLogger(Logger.ROOT_LOGGER_NAME);
|
||||
logger.setLevel(logLevel);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
dataSourceKey: defaultDS
|
||||
destination: example
|
||||
groupId: g1
|
||||
outerAdapterKey: clickhouse1
|
||||
concurrent: true
|
||||
dbMapping:
|
||||
database: mytest
|
||||
table: customer
|
||||
targetTable: mytest.customer
|
||||
targetPk:
|
||||
id: id
|
||||
# mapAll: true
|
||||
targetColumns:
|
||||
id:
|
||||
name:
|
||||
role_id:
|
||||
c_time:
|
||||
test1:
|
||||
etlCondition: "where c_time>={}"
|
||||
commitBatch: 3000 # 批量提交的大小
|
||||
|
||||
|
||||
## Mirror schema synchronize config
|
||||
#dataSourceKey: defaultDS
|
||||
#destination: example
|
||||
#groupId: g1
|
||||
#outerAdapterKey: mysql1
|
||||
#concurrent: true
|
||||
#dbMapping:
|
||||
# mirrorDb: true
|
||||
# database: mytest
|
||||
@@ -0,0 +1,31 @@
|
||||
dataSourceKey: defaultDS
|
||||
destination: example
|
||||
groupId: g1
|
||||
outerAdapterKey: clickhouse1
|
||||
concurrent: true
|
||||
dbMapping:
|
||||
database: mytest
|
||||
table: user
|
||||
targetTable: mytest.user
|
||||
targetPk:
|
||||
id: id
|
||||
# mapAll: true
|
||||
targetColumns:
|
||||
id:
|
||||
name:
|
||||
role_id:
|
||||
c_time:
|
||||
test1:
|
||||
etlCondition: "where c_time>={}"
|
||||
commitBatch: 3000 # 批量提交的大小
|
||||
|
||||
|
||||
## Mirror schema synchronize config
|
||||
#dataSourceKey: defaultDS
|
||||
#destination: example
|
||||
#groupId: g1
|
||||
#outerAdapterKey: mysql1
|
||||
#concurrent: true
|
||||
#dbMapping:
|
||||
# mirrorDb: true
|
||||
# database: mytest
|
||||
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<artifactId>canal.client-adapter</artifactId>
|
||||
<groupId>com.alibaba.otter</groupId>
|
||||
<version>1.1.7-SNAPSHOT</version>
|
||||
<version>1.1.8</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
@@ -54,11 +54,15 @@
|
||||
<artifactId>junit</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.yaml</groupId>
|
||||
<artifactId>snakeyaml</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.diffblue</groupId>
|
||||
<artifactId>deeptestutils</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
|
||||
@@ -28,9 +28,9 @@ import com.google.common.cache.LoadingCache;
|
||||
|
||||
public class Util {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(Util.class);
|
||||
private static final Logger logger = LoggerFactory.getLogger(Util.class);
|
||||
|
||||
public static final String AUTO_GENERATED_PREFIX = "AUTO_GENERATED_";
|
||||
public static final String AUTO_GENERATED_PREFIX = "AUTO_GENERATED_";
|
||||
|
||||
/**
|
||||
* 通过DS执行sql
|
||||
@@ -165,39 +165,38 @@ public class Util {
|
||||
|
||||
public static ThreadPoolExecutor newFixedDaemonThreadPool(int nThreads, long keepAliveTime) {
|
||||
return new ThreadPoolExecutor(nThreads,
|
||||
nThreads,
|
||||
keepAliveTime,
|
||||
TimeUnit.MILLISECONDS,
|
||||
new SynchronousQueue<>(),
|
||||
DaemonThreadFactory.daemonThreadFactory,
|
||||
(r, exe) -> {
|
||||
if (!exe.isShutdown()) {
|
||||
try {
|
||||
exe.getQueue().put(r);
|
||||
} catch (InterruptedException e) {
|
||||
// ignore
|
||||
}
|
||||
nThreads,
|
||||
keepAliveTime,
|
||||
TimeUnit.MILLISECONDS,
|
||||
new SynchronousQueue<>(),
|
||||
DaemonThreadFactory.daemonThreadFactory,
|
||||
(r, exe) -> {
|
||||
if (!exe.isShutdown()) {
|
||||
try {
|
||||
exe.getQueue().put(r);
|
||||
} catch (InterruptedException e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public static ThreadPoolExecutor newSingleDaemonThreadExecutor(long keepAliveTime) {
|
||||
return new ThreadPoolExecutor(1,
|
||||
1,
|
||||
keepAliveTime,
|
||||
TimeUnit.MILLISECONDS,
|
||||
new SynchronousQueue<>(),
|
||||
DaemonThreadFactory.daemonThreadFactory,
|
||||
(r, exe) -> {
|
||||
if (!exe.isShutdown()) {
|
||||
try {
|
||||
exe.getQueue().put(r);
|
||||
} catch (InterruptedException e) {
|
||||
// ignore
|
||||
}
|
||||
1,
|
||||
keepAliveTime,
|
||||
TimeUnit.MILLISECONDS,
|
||||
new SynchronousQueue<>(),
|
||||
DaemonThreadFactory.daemonThreadFactory,
|
||||
(r, exe) -> {
|
||||
if (!exe.isShutdown()) {
|
||||
try {
|
||||
exe.getQueue().put(r);
|
||||
} catch (InterruptedException e) {
|
||||
// ignore
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public final static String timeZone; // 当前时区
|
||||
@@ -327,4 +326,56 @@ public class Util {
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the time string has millisecond or microsecond units
|
||||
*
|
||||
* @param timeStr time string
|
||||
* @return boolean
|
||||
*/
|
||||
public static boolean isAccuracyOverSecond(String timeStr) {
|
||||
if (StringUtils.isEmpty(timeStr)) {
|
||||
return false;
|
||||
}
|
||||
String[] times = StringUtils.split(timeStr, ".");
|
||||
return times.length > 1 && !times[times.length - 1].isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the datetime string has microsecond or nanosecond units
|
||||
*
|
||||
* @param datetimeStr datetime string
|
||||
* @return boolean
|
||||
*/
|
||||
public static boolean isAccuracyOverMillisecond(String datetimeStr) {
|
||||
if (StringUtils.isEmpty(datetimeStr)) {
|
||||
return false;
|
||||
}
|
||||
String[] times = StringUtils.split(datetimeStr, ".");
|
||||
return times.length > 1 && times[times.length - 1].length() > 3;
|
||||
}
|
||||
|
||||
/**
|
||||
* MySQL has fractional seconds support for TIME, DATETIME, and TIMESTAMP
|
||||
* values, with up to microseconds (6 digits) precision. ISO-8601 standard
|
||||
* format is with up to nanoseconds (9 digits) precision, which is sufficient
|
||||
* for storing MySQL time-related data.
|
||||
*
|
||||
* @param datetimeStr datetime string
|
||||
* @return LocalDateTime
|
||||
*/
|
||||
public static LocalDateTime parseISOLocalDateTime(String datetimeStr) {
|
||||
if (StringUtils.isEmpty(datetimeStr)) {
|
||||
return null;
|
||||
}
|
||||
datetimeStr = datetimeStr.trim();
|
||||
try {
|
||||
// Replace SPACE in middle with `T` to comply with ISO-8601 standard format
|
||||
return LocalDateTime.parse(datetimeStr.replace(" ", "T"));
|
||||
} catch (Exception e) {
|
||||
logger.error("Convert datetime string to ISOLocalDateTime fail:{}", datetimeStr, e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -4,9 +4,11 @@ import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.springframework.boot.context.properties.bind.Bindable;
|
||||
import org.springframework.boot.context.properties.bind.Binder;
|
||||
@@ -18,10 +20,14 @@ import org.springframework.core.env.PropertySource;
|
||||
import org.springframework.core.io.ByteArrayResource;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.util.PropertyPlaceholderHelper;
|
||||
|
||||
public class YamlUtils {
|
||||
|
||||
public static <T> T resourceYmlToObj(String resource, String prefix, Class<T> clazz) {
|
||||
if (!StringUtils.startsWithIgnoreCase(resource, "classpath:")) {
|
||||
resource = "classpath:" + resource;
|
||||
}
|
||||
ClassPathResource classPathResource = new ClassPathResource(resource);
|
||||
|
||||
String content;
|
||||
@@ -73,6 +79,9 @@ public class YamlUtils {
|
||||
PropertySource<?> propertySource = propertySources.get(0);
|
||||
|
||||
Properties properties = new Properties();
|
||||
if (baseProperties != null) {
|
||||
properties.putAll(baseProperties);
|
||||
}
|
||||
|
||||
properties.putAll((Map<?, ?>) propertySource.getSource());
|
||||
|
||||
@@ -83,13 +92,14 @@ public class YamlUtils {
|
||||
}
|
||||
}
|
||||
|
||||
if (baseProperties != null) {
|
||||
baseProperties.putAll(properties);
|
||||
properties = baseProperties;
|
||||
}
|
||||
|
||||
ConfigurationPropertySource sources = new MapConfigurationPropertySource(properties);
|
||||
Binder binder = new Binder(sources);
|
||||
PropertyPlaceholderHelper propertyPlaceholderHelper = new PropertyPlaceholderHelper("${", "}");
|
||||
Binder binder = new Binder(Arrays.asList(sources), value -> {
|
||||
if (value instanceof String) {
|
||||
return propertyPlaceholderHelper.replacePlaceholders((String) value, baseProperties);
|
||||
}
|
||||
return value;
|
||||
});
|
||||
return binder.bind(prefix, Bindable.of(clazz)).get();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
package com.alibaba.otter.canal.client.adapter.support;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
public class YamlUtilsTest {
|
||||
|
||||
|
||||
|
||||
@Test
|
||||
public void testLoadConfigToYml() {
|
||||
String configStr="dataSourceKey: defaultDS\n"
|
||||
@@ -17,6 +20,7 @@ public class YamlUtilsTest {
|
||||
+ "outerAdapterKey: mysql1\n"
|
||||
+ "concurrent: true\n"
|
||||
+ "dbMapping:\n"
|
||||
+ " _id: _id\n"
|
||||
+ " database: mytest\n"
|
||||
+ " table: user\n"
|
||||
+ " targetTable: mytest2.user\n"
|
||||
@@ -35,6 +39,7 @@ public class YamlUtilsTest {
|
||||
MappingConfig config = YamlUtils.ymlToObj(null, configStr, MappingConfig.class, null, new Properties());
|
||||
|
||||
Assert.assertNotNull(config);
|
||||
Assert.assertEquals(config.getDbMapping().getId(), "_id");
|
||||
Assert.assertEquals(config.getDestination(), "example");
|
||||
Assert.assertEquals(config.getOuterAdapterKey(), "mysql1");
|
||||
Assert.assertEquals(config.getDbMapping().getDatabase(), "mytest");
|
||||
@@ -104,6 +109,9 @@ public class YamlUtilsTest {
|
||||
}
|
||||
|
||||
private static class DbMapping {
|
||||
|
||||
@Value("${_id}")
|
||||
private String id ;
|
||||
private boolean mirrorDb = false; // 是否镜像库
|
||||
private String database; // 数据库名或schema名
|
||||
private String table; // 表名
|
||||
@@ -225,5 +233,13 @@ public class YamlUtilsTest {
|
||||
public void setAllMapColumns(Map<String, String> allMapColumns) {
|
||||
this.allMapColumns = allMapColumns;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<artifactId>canal.client-adapter</artifactId>
|
||||
<groupId>com.alibaba.otter</groupId>
|
||||
<version>1.1.7-SNAPSHOT</version>
|
||||
<version>1.1.8</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
@@ -59,12 +59,12 @@ public class ES6xAdapter extends ESAdapter {
|
||||
public Map<String, Object> count(String task) {
|
||||
ESSyncConfig config = esSyncConfig.get(task);
|
||||
ESSyncConfig.ESMapping mapping = config.getEsMapping();
|
||||
SearchResponse response = this.esConnection.new ESSearchRequest(mapping.get_index(), mapping.get_type()).size(0)
|
||||
SearchResponse response = this.esConnection.new ESSearchRequest(mapping.getIndex(), mapping.getType()).size(0)
|
||||
.getResponse();
|
||||
|
||||
long rowCount = response.getHits().getTotalHits();
|
||||
Map<String, Object> res = new LinkedHashMap<>();
|
||||
res.put("esIndex", mapping.get_index());
|
||||
res.put("esIndex", mapping.getIndex());
|
||||
res.put("count", rowCount);
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ public class ESEtlService extends AbstractEtlService {
|
||||
|
||||
public EtlResult importData(List<String> params) {
|
||||
ESMapping mapping = config.getEsMapping();
|
||||
logger.info("start etl to import data to index: {}", mapping.get_index());
|
||||
logger.info("start etl to import data to index: {}", mapping.getIndex());
|
||||
String sql = mapping.getSql();
|
||||
return importData(sql, params);
|
||||
}
|
||||
@@ -78,7 +78,7 @@ public class ESEtlService extends AbstractEtlService {
|
||||
}
|
||||
|
||||
// 如果是主键字段则不插入
|
||||
if (fieldItem.getFieldName().equals(mapping.get_id())) {
|
||||
if (fieldItem.getFieldName().equals(mapping.getId())) {
|
||||
idVal = esTemplate.getValFromRS(mapping, rs, fieldName, fieldName);
|
||||
} else {
|
||||
Object val = esTemplate.getValFromRS(mapping, rs, fieldName, fieldName);
|
||||
@@ -117,8 +117,8 @@ public class ESEtlService extends AbstractEtlService {
|
||||
if (idVal != null) {
|
||||
String parentVal = (String) esFieldData.remove("$parent_routing");
|
||||
if (mapping.isUpsert()) {
|
||||
ESUpdateRequest esUpdateRequest = this.esConnection.new ES6xUpdateRequest(mapping.get_index(),
|
||||
mapping.get_type(),
|
||||
ESUpdateRequest esUpdateRequest = this.esConnection.new ES6xUpdateRequest(mapping.getIndex(),
|
||||
mapping.getType(),
|
||||
idVal.toString()).setDoc(esFieldData).setDocAsUpsert(true);
|
||||
|
||||
if (StringUtils.isNotEmpty(parentVal)) {
|
||||
@@ -127,8 +127,8 @@ public class ESEtlService extends AbstractEtlService {
|
||||
|
||||
esBulkRequest.add(esUpdateRequest);
|
||||
} else {
|
||||
ESIndexRequest esIndexRequest = this.esConnection.new ES6xIndexRequest(mapping.get_index(),
|
||||
mapping.get_type(),
|
||||
ESIndexRequest esIndexRequest = this.esConnection.new ES6xIndexRequest(mapping.getIndex(),
|
||||
mapping.getType(),
|
||||
idVal.toString()).setSource(esFieldData);
|
||||
if (StringUtils.isNotEmpty(parentVal)) {
|
||||
esIndexRequest.setRouting(parentVal);
|
||||
@@ -137,12 +137,12 @@ public class ESEtlService extends AbstractEtlService {
|
||||
}
|
||||
} else {
|
||||
idVal = esFieldData.get(mapping.getPk());
|
||||
ESSearchRequest esSearchRequest = this.esConnection.new ESSearchRequest(mapping.get_index(),
|
||||
mapping.get_type()).setQuery(QueryBuilders.termQuery(mapping.getPk(), idVal)).size(10000);
|
||||
ESSearchRequest esSearchRequest = this.esConnection.new ESSearchRequest(mapping.getIndex(),
|
||||
mapping.getType()).setQuery(QueryBuilders.termQuery(mapping.getPk(), idVal)).size(10000);
|
||||
SearchResponse response = esSearchRequest.getResponse();
|
||||
for (SearchHit hit : response.getHits()) {
|
||||
ESUpdateRequest esUpdateRequest = this.esConnection.new ES6xUpdateRequest(mapping.get_index(),
|
||||
mapping.get_type(),
|
||||
ESUpdateRequest esUpdateRequest = this.esConnection.new ES6xUpdateRequest(mapping.getIndex(),
|
||||
mapping.getType(),
|
||||
hit.getId()).setDoc(esFieldData);
|
||||
esBulkRequest.add(esUpdateRequest);
|
||||
}
|
||||
@@ -160,7 +160,7 @@ public class ESEtlService extends AbstractEtlService {
|
||||
(System.currentTimeMillis() - batchBegin),
|
||||
(System.currentTimeMillis() - esBatchBegin),
|
||||
esBulkRequest.numberOfActions(),
|
||||
mapping.get_index());
|
||||
mapping.getIndex());
|
||||
}
|
||||
batchBegin = System.currentTimeMillis();
|
||||
esBulkRequest.resetBulk();
|
||||
@@ -180,12 +180,12 @@ public class ESEtlService extends AbstractEtlService {
|
||||
(System.currentTimeMillis() - batchBegin),
|
||||
(System.currentTimeMillis() - esBatchBegin),
|
||||
esBulkRequest.numberOfActions(),
|
||||
mapping.get_index());
|
||||
mapping.getIndex());
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error(e.getMessage(), e);
|
||||
errMsg.add(mapping.get_index() + " etl failed! ==>" + e.getMessage());
|
||||
errMsg.add(mapping.getIndex() + " etl failed! ==>" + e.getMessage());
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return count;
|
||||
|
||||
@@ -66,19 +66,19 @@ public class ES6xTemplate implements ESTemplate {
|
||||
|
||||
@Override
|
||||
public void insert(ESSyncConfig.ESMapping mapping, Object pkVal, Map<String, Object> esFieldData) {
|
||||
if (mapping.get_id() != null) {
|
||||
if (mapping.getId() != null) {
|
||||
String parentVal = (String) esFieldData.remove("$parent_routing");
|
||||
if (mapping.isUpsert()) {
|
||||
ESUpdateRequest updateRequest = esConnection.new ES6xUpdateRequest(mapping.get_index(),
|
||||
mapping.get_type(),
|
||||
ESUpdateRequest updateRequest = esConnection.new ES6xUpdateRequest(mapping.getIndex(),
|
||||
mapping.getType(),
|
||||
pkVal.toString()).setDoc(esFieldData).setDocAsUpsert(true);
|
||||
if (StringUtils.isNotEmpty(parentVal)) {
|
||||
updateRequest.setRouting(parentVal);
|
||||
}
|
||||
getBulk().add(updateRequest);
|
||||
} else {
|
||||
ESIndexRequest indexRequest = esConnection.new ES6xIndexRequest(mapping.get_index(),
|
||||
mapping.get_type(),
|
||||
ESIndexRequest indexRequest = esConnection.new ES6xIndexRequest(mapping.getIndex(),
|
||||
mapping.getType(),
|
||||
pkVal.toString()).setSource(esFieldData);
|
||||
if (StringUtils.isNotEmpty(parentVal)) {
|
||||
indexRequest.setRouting(parentVal);
|
||||
@@ -87,13 +87,13 @@ public class ES6xTemplate implements ESTemplate {
|
||||
}
|
||||
commitBulk();
|
||||
} else {
|
||||
ESSearchRequest esSearchRequest = this.esConnection.new ESSearchRequest(mapping.get_index(),
|
||||
mapping.get_type()).setQuery(QueryBuilders.termQuery(mapping.getPk(), pkVal)).size(10000);
|
||||
ESSearchRequest esSearchRequest = this.esConnection.new ESSearchRequest(mapping.getIndex(),
|
||||
mapping.getType()).setQuery(QueryBuilders.termQuery(mapping.getPk(), pkVal)).size(10000);
|
||||
SearchResponse response = esSearchRequest.getResponse();
|
||||
|
||||
for (SearchHit hit : response.getHits()) {
|
||||
ESUpdateRequest esUpdateRequest = this.esConnection.new ES6xUpdateRequest(mapping.get_index(),
|
||||
mapping.get_type(),
|
||||
ESUpdateRequest esUpdateRequest = this.esConnection.new ES6xUpdateRequest(mapping.getIndex(),
|
||||
mapping.getType(),
|
||||
hit.getId()).setDoc(esFieldData);
|
||||
getBulk().add(esUpdateRequest);
|
||||
commitBulk();
|
||||
@@ -150,19 +150,19 @@ public class ES6xTemplate implements ESTemplate {
|
||||
|
||||
@Override
|
||||
public void delete(ESSyncConfig.ESMapping mapping, Object pkVal, Map<String, Object> esFieldData) {
|
||||
if (mapping.get_id() != null) {
|
||||
ESDeleteRequest esDeleteRequest = this.esConnection.new ES6xDeleteRequest(mapping.get_index(),
|
||||
mapping.get_type(),
|
||||
if (mapping.getId() != null) {
|
||||
ESDeleteRequest esDeleteRequest = this.esConnection.new ES6xDeleteRequest(mapping.getIndex(),
|
||||
mapping.getType(),
|
||||
pkVal.toString());
|
||||
getBulk().add(esDeleteRequest);
|
||||
commitBulk();
|
||||
} else {
|
||||
ESSearchRequest esSearchRequest = this.esConnection.new ESSearchRequest(mapping.get_index(),
|
||||
mapping.get_type()).setQuery(QueryBuilders.termQuery(mapping.getPk(), pkVal)).size(10000);
|
||||
ESSearchRequest esSearchRequest = this.esConnection.new ESSearchRequest(mapping.getIndex(),
|
||||
mapping.getType()).setQuery(QueryBuilders.termQuery(mapping.getPk(), pkVal)).size(10000);
|
||||
SearchResponse response = esSearchRequest.getResponse();
|
||||
for (SearchHit hit : response.getHits()) {
|
||||
ESUpdateRequest esUpdateRequest = this.esConnection.new ES6xUpdateRequest(mapping.get_index(),
|
||||
mapping.get_type(),
|
||||
ESUpdateRequest esUpdateRequest = this.esConnection.new ES6xUpdateRequest(mapping.getIndex(),
|
||||
mapping.getType(),
|
||||
hit.getId()).setDoc(esFieldData);
|
||||
getBulk().add(esUpdateRequest);
|
||||
commitBulk();
|
||||
@@ -207,7 +207,7 @@ public class ES6xTemplate implements ESTemplate {
|
||||
public Object getESDataFromRS(ESSyncConfig.ESMapping mapping, ResultSet resultSet,
|
||||
Map<String, Object> esFieldData) throws SQLException {
|
||||
SchemaItem schemaItem = mapping.getSchemaItem();
|
||||
String idFieldName = mapping.get_id() == null ? mapping.getPk() : mapping.get_id();
|
||||
String idFieldName = mapping.getId() == null ? mapping.getPk() : mapping.getId();
|
||||
Object resultIdVal = null;
|
||||
for (FieldItem fieldItem : schemaItem.getSelectFields().values()) {
|
||||
Object value = getValFromRS(mapping, resultSet, fieldItem.getFieldName(), fieldItem.getFieldName());
|
||||
@@ -216,7 +216,7 @@ public class ES6xTemplate implements ESTemplate {
|
||||
resultIdVal = value;
|
||||
}
|
||||
|
||||
if (!fieldItem.getFieldName().equals(mapping.get_id())
|
||||
if (!fieldItem.getFieldName().equals(mapping.getId())
|
||||
&& !mapping.getSkips().contains(fieldItem.getFieldName())) {
|
||||
esFieldData.put(Util.cleanColumn(fieldItem.getFieldName()), value);
|
||||
}
|
||||
@@ -231,7 +231,7 @@ public class ES6xTemplate implements ESTemplate {
|
||||
@Override
|
||||
public Object getIdValFromRS(ESSyncConfig.ESMapping mapping, ResultSet resultSet) throws SQLException {
|
||||
SchemaItem schemaItem = mapping.getSchemaItem();
|
||||
String idFieldName = mapping.get_id() == null ? mapping.getPk() : mapping.get_id();
|
||||
String idFieldName = mapping.getId() == null ? mapping.getPk() : mapping.getId();
|
||||
Object resultIdVal = null;
|
||||
for (FieldItem fieldItem : schemaItem.getSelectFields().values()) {
|
||||
Object value = getValFromRS(mapping, resultSet, fieldItem.getFieldName(), fieldItem.getFieldName());
|
||||
@@ -248,7 +248,7 @@ public class ES6xTemplate implements ESTemplate {
|
||||
public Object getESDataFromRS(ESSyncConfig.ESMapping mapping, ResultSet resultSet, Map<String, Object> dmlOld,
|
||||
Map<String, Object> esFieldData) throws SQLException {
|
||||
SchemaItem schemaItem = mapping.getSchemaItem();
|
||||
String idFieldName = mapping.get_id() == null ? mapping.getPk() : mapping.get_id();
|
||||
String idFieldName = mapping.getId() == null ? mapping.getPk() : mapping.getId();
|
||||
Object resultIdVal = null;
|
||||
for (FieldItem fieldItem : schemaItem.getSelectFields().values()) {
|
||||
if (fieldItem.getFieldName().equals(idFieldName)) {
|
||||
@@ -294,7 +294,7 @@ public class ES6xTemplate implements ESTemplate {
|
||||
public Object getESDataFromDmlData(ESSyncConfig.ESMapping mapping, Map<String, Object> dmlData,
|
||||
Map<String, Object> esFieldData) {
|
||||
SchemaItem schemaItem = mapping.getSchemaItem();
|
||||
String idFieldName = mapping.get_id() == null ? mapping.getPk() : mapping.get_id();
|
||||
String idFieldName = mapping.getId() == null ? mapping.getPk() : mapping.getId();
|
||||
Object resultIdVal = null;
|
||||
for (FieldItem fieldItem : schemaItem.getSelectFields().values()) {
|
||||
String columnName = fieldItem.getColumnItems().iterator().next().getColumnName();
|
||||
@@ -304,7 +304,7 @@ public class ES6xTemplate implements ESTemplate {
|
||||
resultIdVal = value;
|
||||
}
|
||||
|
||||
if (!fieldItem.getFieldName().equals(mapping.get_id())
|
||||
if (!fieldItem.getFieldName().equals(mapping.getId())
|
||||
&& !mapping.getSkips().contains(fieldItem.getFieldName())) {
|
||||
esFieldData.put(Util.cleanColumn(fieldItem.getFieldName()), value);
|
||||
}
|
||||
@@ -319,7 +319,7 @@ public class ES6xTemplate implements ESTemplate {
|
||||
public Object getESDataFromDmlData(ESSyncConfig.ESMapping mapping,String owner, Map<String, Object> dmlData,
|
||||
Map<String, Object> dmlOld, Map<String, Object> esFieldData) {
|
||||
SchemaItem schemaItem = mapping.getSchemaItem();
|
||||
String idFieldName = mapping.get_id() == null ? mapping.getPk() : mapping.get_id();
|
||||
String idFieldName = mapping.getId() == null ? mapping.getPk() : mapping.getId();
|
||||
Object resultIdVal = null;
|
||||
for (FieldItem fieldItem : schemaItem.getSelectFields().values()) {
|
||||
ColumnItem columnItem = fieldItem.getColumnItems().iterator().next();
|
||||
@@ -353,19 +353,19 @@ public class ES6xTemplate implements ESTemplate {
|
||||
}
|
||||
|
||||
private void append4Update(ESMapping mapping, Object pkVal, Map<String, Object> esFieldData) {
|
||||
if (mapping.get_id() != null) {
|
||||
if (mapping.getId() != null) {
|
||||
String parentVal = (String) esFieldData.remove("$parent_routing");
|
||||
if (mapping.isUpsert()) {
|
||||
ESUpdateRequest esUpdateRequest = this.esConnection.new ES6xUpdateRequest(mapping.get_index(),
|
||||
mapping.get_type(),
|
||||
ESUpdateRequest esUpdateRequest = this.esConnection.new ES6xUpdateRequest(mapping.getIndex(),
|
||||
mapping.getType(),
|
||||
pkVal.toString()).setDoc(esFieldData).setDocAsUpsert(true);
|
||||
if (StringUtils.isNotEmpty(parentVal)) {
|
||||
esUpdateRequest.setRouting(parentVal);
|
||||
}
|
||||
getBulk().add(esUpdateRequest);
|
||||
} else {
|
||||
ESUpdateRequest esUpdateRequest = this.esConnection.new ES6xUpdateRequest(mapping.get_index(),
|
||||
mapping.get_type(),
|
||||
ESUpdateRequest esUpdateRequest = this.esConnection.new ES6xUpdateRequest(mapping.getIndex(),
|
||||
mapping.getType(),
|
||||
pkVal.toString()).setDoc(esFieldData);
|
||||
if (StringUtils.isNotEmpty(parentVal)) {
|
||||
esUpdateRequest.setRouting(parentVal);
|
||||
@@ -373,12 +373,12 @@ public class ES6xTemplate implements ESTemplate {
|
||||
getBulk().add(esUpdateRequest);
|
||||
}
|
||||
} else {
|
||||
ESSearchRequest esSearchRequest = this.esConnection.new ESSearchRequest(mapping.get_index(),
|
||||
mapping.get_type()).setQuery(QueryBuilders.termQuery(mapping.getPk(), pkVal)).size(10000);
|
||||
ESSearchRequest esSearchRequest = this.esConnection.new ESSearchRequest(mapping.getIndex(),
|
||||
mapping.getType()).setQuery(QueryBuilders.termQuery(mapping.getPk(), pkVal)).size(10000);
|
||||
SearchResponse response = esSearchRequest.getResponse();
|
||||
for (SearchHit hit : response.getHits()) {
|
||||
ESUpdateRequest esUpdateRequest = this.esConnection.new ES6xUpdateRequest(mapping.get_index(),
|
||||
mapping.get_type(),
|
||||
ESUpdateRequest esUpdateRequest = this.esConnection.new ES6xUpdateRequest(mapping.getIndex(),
|
||||
mapping.getType(),
|
||||
hit.getId()).setDoc(esFieldData);
|
||||
getBulk().add(esUpdateRequest);
|
||||
}
|
||||
@@ -394,15 +394,15 @@ public class ES6xTemplate implements ESTemplate {
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private String getEsType(ESMapping mapping, String fieldName) {
|
||||
String key = mapping.get_index() + "-" + mapping.get_type();
|
||||
String key = mapping.getIndex() + "-" + mapping.getType();
|
||||
Map<String, String> fieldType = esFieldTypes.get(key);
|
||||
if (fieldType != null) {
|
||||
return fieldType.get(fieldName);
|
||||
} else {
|
||||
MappingMetaData mappingMetaData = esConnection.getMapping(mapping.get_index(), mapping.get_type());
|
||||
MappingMetaData mappingMetaData = esConnection.getMapping(mapping.getIndex(), mapping.getType());
|
||||
|
||||
if (mappingMetaData == null) {
|
||||
throw new IllegalArgumentException("Not found the mapping info of index: " + mapping.get_index());
|
||||
throw new IllegalArgumentException("Not found the mapping info of index: " + mapping.getIndex());
|
||||
}
|
||||
|
||||
fieldType = new LinkedHashMap<>();
|
||||
|
||||
@@ -29,9 +29,9 @@ public class ConfigLoadTest {
|
||||
Assert.assertNotNull(config);
|
||||
Assert.assertEquals("defaultDS", config.getDataSourceKey());
|
||||
ESSyncConfig.ESMapping esMapping = config.getEsMapping();
|
||||
Assert.assertEquals("mytest_user", esMapping.get_index());
|
||||
Assert.assertEquals("_doc", esMapping.get_type());
|
||||
Assert.assertEquals("id", esMapping.get_id());
|
||||
Assert.assertEquals("mytest_user", esMapping.getIndex());
|
||||
Assert.assertEquals("_doc", esMapping.getType());
|
||||
Assert.assertEquals("id", esMapping.getId());
|
||||
Assert.assertNotNull(esMapping.getSql());
|
||||
|
||||
// Map<String, List<ESSyncConfig>> dbTableEsSyncConfig =
|
||||
|
||||
@@ -5,6 +5,7 @@ import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
@@ -26,7 +27,7 @@ public class Common {
|
||||
outerAdapterConfig.setProperties(properties);
|
||||
|
||||
ES6xAdapter esAdapter = new ES6xAdapter();
|
||||
esAdapter.init(outerAdapterConfig, null);
|
||||
esAdapter.init(outerAdapterConfig, new Properties());
|
||||
return esAdapter;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<artifactId>canal.client-adapter</artifactId>
|
||||
<groupId>com.alibaba.otter</groupId>
|
||||
<version>1.1.7-SNAPSHOT</version>
|
||||
<version>1.1.8</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
@@ -59,11 +59,11 @@ public class ES7xAdapter extends ESAdapter {
|
||||
public Map<String, Object> count(String task) {
|
||||
ESSyncConfig config = esSyncConfig.get(task);
|
||||
ESSyncConfig.ESMapping mapping = config.getEsMapping();
|
||||
SearchResponse response = this.esConnection.new ESSearchRequest(mapping.get_index()).size(0).getResponse();
|
||||
SearchResponse response = this.esConnection.new ESSearchRequest(mapping.getIndex()).size(0).getResponse();
|
||||
|
||||
long rowCount = response.getHits().getTotalHits().value;
|
||||
Map<String, Object> res = new LinkedHashMap<>();
|
||||
res.put("esIndex", mapping.get_index());
|
||||
res.put("esIndex", mapping.getIndex());
|
||||
res.put("count", rowCount);
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ public class ESEtlService extends AbstractEtlService {
|
||||
|
||||
public EtlResult importData(List<String> params) {
|
||||
ESMapping mapping = config.getEsMapping();
|
||||
logger.info("start etl to import data to index: {}", mapping.get_index());
|
||||
logger.info("start etl to import data to index: {}", mapping.getIndex());
|
||||
String sql = mapping.getSql();
|
||||
return importData(sql, params);
|
||||
}
|
||||
@@ -78,7 +78,7 @@ public class ESEtlService extends AbstractEtlService {
|
||||
}
|
||||
|
||||
// 如果是主键字段则不插入
|
||||
if (fieldItem.getFieldName().equals(mapping.get_id())) {
|
||||
if (fieldItem.getFieldName().equals(mapping.getId())) {
|
||||
idVal = esTemplate.getValFromRS(mapping, rs, fieldName, fieldName);
|
||||
} else {
|
||||
Object val = esTemplate.getValFromRS(mapping, rs, fieldName, fieldName);
|
||||
@@ -118,7 +118,7 @@ public class ESEtlService extends AbstractEtlService {
|
||||
String parentVal = (String) esFieldData.remove("$parent_routing");
|
||||
if (mapping.isUpsert()) {
|
||||
ESUpdateRequest esUpdateRequest = this.esConnection.new ES7xUpdateRequest(
|
||||
mapping.get_index(),
|
||||
mapping.getIndex(),
|
||||
idVal.toString()).setDoc(esFieldData).setDocAsUpsert(true);
|
||||
|
||||
if (StringUtils.isNotEmpty(parentVal)) {
|
||||
@@ -128,7 +128,7 @@ public class ESEtlService extends AbstractEtlService {
|
||||
esBulkRequest.add(esUpdateRequest);
|
||||
} else {
|
||||
ESIndexRequest esIndexRequest = this.esConnection.new ES7xIndexRequest(
|
||||
mapping.get_index(),
|
||||
mapping.getIndex(),
|
||||
idVal.toString()).setSource(esFieldData);
|
||||
if (StringUtils.isNotEmpty(parentVal)) {
|
||||
esIndexRequest.setRouting(parentVal);
|
||||
@@ -137,13 +137,13 @@ public class ESEtlService extends AbstractEtlService {
|
||||
}
|
||||
} else {
|
||||
idVal = esFieldData.get(mapping.getPk());
|
||||
ESSearchRequest esSearchRequest = this.esConnection.new ESSearchRequest(mapping.get_index())
|
||||
ESSearchRequest esSearchRequest = this.esConnection.new ESSearchRequest(mapping.getIndex())
|
||||
.setQuery(QueryBuilders.termQuery(mapping.getPk(), idVal))
|
||||
.size(10000);
|
||||
SearchResponse response = esSearchRequest.getResponse();
|
||||
for (SearchHit hit : response.getHits()) {
|
||||
ESUpdateRequest esUpdateRequest = this.esConnection.new ES7xUpdateRequest(
|
||||
mapping.get_index(),
|
||||
mapping.getIndex(),
|
||||
hit.getId()).setDoc(esFieldData);
|
||||
esBulkRequest.add(esUpdateRequest);
|
||||
}
|
||||
@@ -162,7 +162,7 @@ public class ESEtlService extends AbstractEtlService {
|
||||
(System.currentTimeMillis() - batchBegin),
|
||||
(System.currentTimeMillis() - esBatchBegin),
|
||||
esBulkRequest.numberOfActions(),
|
||||
mapping.get_index());
|
||||
mapping.getIndex());
|
||||
}
|
||||
batchBegin = System.currentTimeMillis();
|
||||
esBulkRequest.resetBulk();
|
||||
@@ -182,12 +182,12 @@ public class ESEtlService extends AbstractEtlService {
|
||||
(System.currentTimeMillis() - batchBegin),
|
||||
(System.currentTimeMillis() - esBatchBegin),
|
||||
esBulkRequest.numberOfActions(),
|
||||
mapping.get_index());
|
||||
mapping.getIndex());
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error(e.getMessage(), e);
|
||||
errMsg.add(mapping.get_index() + " etl failed! ==>" + e.getMessage());
|
||||
errMsg.add(mapping.getIndex() + " etl failed! ==>" + e.getMessage());
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return count;
|
||||
|
||||
@@ -2,11 +2,7 @@ package com.alibaba.otter.canal.client.adapter.es7x.support;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
|
||||
@@ -66,17 +62,17 @@ public class ES7xTemplate implements ESTemplate {
|
||||
|
||||
@Override
|
||||
public void insert(ESMapping mapping, Object pkVal, Map<String, Object> esFieldData) {
|
||||
if (mapping.get_id() != null) {
|
||||
if (mapping.getId() != null) {
|
||||
String parentVal = (String) esFieldData.remove("$parent_routing");
|
||||
if (mapping.isUpsert()) {
|
||||
ESUpdateRequest updateRequest = esConnection.new ES7xUpdateRequest(mapping.get_index(),
|
||||
ESUpdateRequest updateRequest = esConnection.new ES7xUpdateRequest(mapping.getIndex(),
|
||||
pkVal.toString()).setDoc(esFieldData).setDocAsUpsert(true);
|
||||
if (StringUtils.isNotEmpty(parentVal)) {
|
||||
updateRequest.setRouting(parentVal);
|
||||
}
|
||||
getBulk().add(updateRequest);
|
||||
} else {
|
||||
ESIndexRequest indexRequest = esConnection.new ES7xIndexRequest(mapping.get_index(), pkVal.toString())
|
||||
ESIndexRequest indexRequest = esConnection.new ES7xIndexRequest(mapping.getIndex(), pkVal.toString())
|
||||
.setSource(esFieldData);
|
||||
if (StringUtils.isNotEmpty(parentVal)) {
|
||||
indexRequest.setRouting(parentVal);
|
||||
@@ -85,13 +81,13 @@ public class ES7xTemplate implements ESTemplate {
|
||||
}
|
||||
commitBulk();
|
||||
} else {
|
||||
ESSearchRequest esSearchRequest = this.esConnection.new ESSearchRequest(mapping.get_index())
|
||||
ESSearchRequest esSearchRequest = this.esConnection.new ESSearchRequest(mapping.getIndex())
|
||||
.setQuery(QueryBuilders.termQuery(mapping.getPk(), pkVal))
|
||||
.size(10000);
|
||||
SearchResponse response = esSearchRequest.getResponse();
|
||||
|
||||
for (SearchHit hit : response.getHits()) {
|
||||
ESUpdateRequest esUpdateRequest = this.esConnection.new ES7xUpdateRequest(mapping.get_index(),
|
||||
ESUpdateRequest esUpdateRequest = this.esConnection.new ES7xUpdateRequest(mapping.getIndex(),
|
||||
hit.getId()).setDoc(esFieldData);
|
||||
getBulk().add(esUpdateRequest);
|
||||
commitBulk();
|
||||
@@ -148,18 +144,18 @@ public class ES7xTemplate implements ESTemplate {
|
||||
|
||||
@Override
|
||||
public void delete(ESMapping mapping, Object pkVal, Map<String, Object> esFieldData) {
|
||||
if (mapping.get_id() != null) {
|
||||
ESDeleteRequest esDeleteRequest = this.esConnection.new ES7xDeleteRequest(mapping.get_index(),
|
||||
if (mapping.getId() != null) {
|
||||
ESDeleteRequest esDeleteRequest = this.esConnection.new ES7xDeleteRequest(mapping.getIndex(),
|
||||
pkVal.toString());
|
||||
getBulk().add(esDeleteRequest);
|
||||
commitBulk();
|
||||
} else {
|
||||
ESSearchRequest esSearchRequest = this.esConnection.new ESSearchRequest(mapping.get_index())
|
||||
ESSearchRequest esSearchRequest = this.esConnection.new ESSearchRequest(mapping.getIndex())
|
||||
.setQuery(QueryBuilders.termQuery(mapping.getPk(), pkVal))
|
||||
.size(10000);
|
||||
SearchResponse response = esSearchRequest.getResponse();
|
||||
for (SearchHit hit : response.getHits()) {
|
||||
ESUpdateRequest esUpdateRequest = this.esConnection.new ES7xUpdateRequest(mapping.get_index(),
|
||||
ESUpdateRequest esUpdateRequest = this.esConnection.new ES7xUpdateRequest(mapping.getIndex(),
|
||||
hit.getId()).setDoc(esFieldData);
|
||||
getBulk().add(esUpdateRequest);
|
||||
commitBulk();
|
||||
@@ -204,7 +200,7 @@ public class ES7xTemplate implements ESTemplate {
|
||||
public Object getESDataFromRS(ESMapping mapping, ResultSet resultSet,
|
||||
Map<String, Object> esFieldData) throws SQLException {
|
||||
SchemaItem schemaItem = mapping.getSchemaItem();
|
||||
String idFieldName = mapping.get_id() == null ? mapping.getPk() : mapping.get_id();
|
||||
String idFieldName = mapping.getId() == null ? mapping.getPk() : mapping.getId();
|
||||
Object resultIdVal = null;
|
||||
for (FieldItem fieldItem : schemaItem.getSelectFields().values()) {
|
||||
Object value = getValFromRS(mapping, resultSet, fieldItem.getFieldName(), fieldItem.getFieldName());
|
||||
@@ -213,7 +209,7 @@ public class ES7xTemplate implements ESTemplate {
|
||||
resultIdVal = value;
|
||||
}
|
||||
|
||||
if (!fieldItem.getFieldName().equals(mapping.get_id())
|
||||
if (!fieldItem.getFieldName().equals(mapping.getId())
|
||||
&& !mapping.getSkips().contains(fieldItem.getFieldName())) {
|
||||
esFieldData.put(Util.cleanColumn(fieldItem.getFieldName()), value);
|
||||
}
|
||||
@@ -228,7 +224,7 @@ public class ES7xTemplate implements ESTemplate {
|
||||
@Override
|
||||
public Object getIdValFromRS(ESMapping mapping, ResultSet resultSet) throws SQLException {
|
||||
SchemaItem schemaItem = mapping.getSchemaItem();
|
||||
String idFieldName = mapping.get_id() == null ? mapping.getPk() : mapping.get_id();
|
||||
String idFieldName = mapping.getId() == null ? mapping.getPk() : mapping.getId();
|
||||
Object resultIdVal = null;
|
||||
for (FieldItem fieldItem : schemaItem.getSelectFields().values()) {
|
||||
Object value = getValFromRS(mapping, resultSet, fieldItem.getFieldName(), fieldItem.getFieldName());
|
||||
@@ -245,7 +241,7 @@ public class ES7xTemplate implements ESTemplate {
|
||||
public Object getESDataFromRS(ESMapping mapping, ResultSet resultSet, Map<String, Object> dmlOld,
|
||||
Map<String, Object> esFieldData) throws SQLException {
|
||||
SchemaItem schemaItem = mapping.getSchemaItem();
|
||||
String idFieldName = mapping.get_id() == null ? mapping.getPk() : mapping.get_id();
|
||||
String idFieldName = mapping.getId() == null ? mapping.getPk() : mapping.getId();
|
||||
Object resultIdVal = null;
|
||||
for (FieldItem fieldItem : schemaItem.getSelectFields().values()) {
|
||||
if (fieldItem.getFieldName().equals(idFieldName)) {
|
||||
@@ -289,7 +285,7 @@ public class ES7xTemplate implements ESTemplate {
|
||||
@Override
|
||||
public Object getESDataFromDmlData(ESMapping mapping, Map<String, Object> dmlData, Map<String, Object> esFieldData) {
|
||||
SchemaItem schemaItem = mapping.getSchemaItem();
|
||||
String idFieldName = mapping.get_id() == null ? mapping.getPk() : mapping.get_id();
|
||||
String idFieldName = mapping.getId() == null ? mapping.getPk() : mapping.getId();
|
||||
Object resultIdVal = null;
|
||||
for (FieldItem fieldItem : schemaItem.getSelectFields().values()) {
|
||||
String columnName = fieldItem.getColumnItems().iterator().next().getColumnName();
|
||||
@@ -299,7 +295,7 @@ public class ES7xTemplate implements ESTemplate {
|
||||
resultIdVal = value;
|
||||
}
|
||||
|
||||
if (!fieldItem.getFieldName().equals(mapping.get_id())
|
||||
if (!fieldItem.getFieldName().equals(mapping.getId())
|
||||
&& !mapping.getSkips().contains(fieldItem.getFieldName())) {
|
||||
esFieldData.put(Util.cleanColumn(fieldItem.getFieldName()), value);
|
||||
}
|
||||
@@ -314,15 +310,19 @@ public class ES7xTemplate implements ESTemplate {
|
||||
public Object getESDataFromDmlData(ESMapping mapping,String owner, Map<String, Object> dmlData, Map<String, Object> dmlOld,
|
||||
Map<String, Object> esFieldData) {
|
||||
SchemaItem schemaItem = mapping.getSchemaItem();
|
||||
String idFieldName = mapping.get_id() == null ? mapping.getPk() : mapping.get_id();
|
||||
String idFieldName = mapping.getId() == null ? mapping.getPk() : mapping.getId();
|
||||
Object resultIdVal = null;
|
||||
for (FieldItem fieldItem : schemaItem.getSelectFields().values()) {
|
||||
ColumnItem columnItem = fieldItem.getColumnItems().iterator().next();
|
||||
if (columnItem.getOwner() == null || columnItem.getColumnName() == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!columnItem.getOwner().equals(owner)) {
|
||||
continue;
|
||||
}
|
||||
String columnName = columnItem.getColumnName();
|
||||
|
||||
String columnName = columnItem.getColumnName();
|
||||
if (fieldItem.getFieldName().equals(idFieldName)) {
|
||||
resultIdVal = getValFromData(mapping, dmlData, fieldItem.getFieldName(), columnName);
|
||||
}
|
||||
@@ -348,17 +348,17 @@ public class ES7xTemplate implements ESTemplate {
|
||||
}
|
||||
|
||||
private void append4Update(ESMapping mapping, Object pkVal, Map<String, Object> esFieldData) {
|
||||
if (mapping.get_id() != null) {
|
||||
if (mapping.getId() != null) {
|
||||
String parentVal = (String) esFieldData.remove("$parent_routing");
|
||||
if (mapping.isUpsert()) {
|
||||
ESUpdateRequest esUpdateRequest = this.esConnection.new ES7xUpdateRequest(mapping.get_index(),
|
||||
ESUpdateRequest esUpdateRequest = this.esConnection.new ES7xUpdateRequest(mapping.getIndex(),
|
||||
pkVal.toString()).setDoc(esFieldData).setDocAsUpsert(true);
|
||||
if (StringUtils.isNotEmpty(parentVal)) {
|
||||
esUpdateRequest.setRouting(parentVal);
|
||||
}
|
||||
getBulk().add(esUpdateRequest);
|
||||
} else {
|
||||
ESUpdateRequest esUpdateRequest = this.esConnection.new ES7xUpdateRequest(mapping.get_index(),
|
||||
ESUpdateRequest esUpdateRequest = this.esConnection.new ES7xUpdateRequest(mapping.getIndex(),
|
||||
pkVal.toString()).setDoc(esFieldData);
|
||||
if (StringUtils.isNotEmpty(parentVal)) {
|
||||
esUpdateRequest.setRouting(parentVal);
|
||||
@@ -366,12 +366,12 @@ public class ES7xTemplate implements ESTemplate {
|
||||
getBulk().add(esUpdateRequest);
|
||||
}
|
||||
} else {
|
||||
ESSearchRequest esSearchRequest = this.esConnection.new ESSearchRequest(mapping.get_index())
|
||||
ESSearchRequest esSearchRequest = this.esConnection.new ESSearchRequest(mapping.getIndex())
|
||||
.setQuery(QueryBuilders.termQuery(mapping.getPk(), pkVal))
|
||||
.size(10000);
|
||||
SearchResponse response = esSearchRequest.getResponse();
|
||||
for (SearchHit hit : response.getHits()) {
|
||||
ESUpdateRequest esUpdateRequest = this.esConnection.new ES7xUpdateRequest(mapping.get_index(),
|
||||
ESUpdateRequest esUpdateRequest = this.esConnection.new ES7xUpdateRequest(mapping.getIndex(),
|
||||
hit.getId()).setDoc(esFieldData);
|
||||
getBulk().add(esUpdateRequest);
|
||||
}
|
||||
@@ -387,15 +387,15 @@ public class ES7xTemplate implements ESTemplate {
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private String getEsType(ESMapping mapping, String fieldName) {
|
||||
String key = mapping.get_index() + "-" + mapping.get_type();
|
||||
String key = mapping.getIndex() + "-" + mapping.getType();
|
||||
Map<String, String> fieldType = esFieldTypes.get(key);
|
||||
if (fieldType != null) {
|
||||
return fieldType.get(fieldName);
|
||||
} else {
|
||||
MappingMetaData mappingMetaData = esConnection.getMapping(mapping.get_index());
|
||||
MappingMetaData mappingMetaData = esConnection.getMapping(mapping.getIndex());
|
||||
|
||||
if (mappingMetaData == null) {
|
||||
throw new IllegalArgumentException("Not found the mapping info of index: " + mapping.get_index());
|
||||
throw new IllegalArgumentException("Not found the mapping info of index: " + mapping.getIndex());
|
||||
}
|
||||
|
||||
fieldType = new LinkedHashMap<>();
|
||||
|
||||
93
client-adapter/es8x/pom.xml
Normal file
93
client-adapter/es8x/pom.xml
Normal file
@@ -0,0 +1,93 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<artifactId>canal.client-adapter</artifactId>
|
||||
<groupId>com.alibaba.otter</groupId>
|
||||
<version>1.1.8</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>com.alibaba.otter</groupId>
|
||||
<artifactId>client-adapter.es8x</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<name>canal client adapter es v8x module for otter ${project.version}</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.alibaba.otter</groupId>
|
||||
<artifactId>client-adapter.common</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.alibaba.otter</groupId>
|
||||
<artifactId>client-adapter.escore</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>co.elastic.clients</groupId>
|
||||
<artifactId>elasticsearch-java</artifactId>
|
||||
<version>8.6.2</version>
|
||||
</dependency>
|
||||
<!--<dependency>
|
||||
<groupId>org.elasticsearch.client</groupId>
|
||||
<artifactId>elasticsearch-rest-client</artifactId>
|
||||
<version>8.6.2</version>
|
||||
</dependency>-->
|
||||
<dependency>
|
||||
<groupId>org.elasticsearch.client</groupId>
|
||||
<artifactId>elasticsearch-rest-high-level-client</artifactId>
|
||||
<version>7.17.9</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-assembly-plugin</artifactId>
|
||||
<version>2.4</version>
|
||||
<configuration>
|
||||
<descriptorRefs>
|
||||
<descriptorRef>jar-with-dependencies</descriptorRef>
|
||||
</descriptorRefs>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>make-assembly</id>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>single</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-antrun-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>run</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<tasks>
|
||||
<copy todir="${project.basedir}/../launcher/target/classes/es8" overwrite="true">
|
||||
<fileset dir="${project.basedir}/target/classes/es8" erroronmissingdir="true">
|
||||
<include name="*.yml" />
|
||||
</fileset>
|
||||
</copy>
|
||||
</tasks>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,117 @@
|
||||
package com.alibaba.otter.canal.client.adapter.es8x;
|
||||
|
||||
import com.alibaba.otter.canal.client.adapter.es.core.ESAdapter;
|
||||
import com.alibaba.otter.canal.client.adapter.es.core.config.ESSyncConfig;
|
||||
import com.alibaba.otter.canal.client.adapter.es8x.etl.ESEtlService;
|
||||
import com.alibaba.otter.canal.client.adapter.es8x.support.ES8xTemplate;
|
||||
import com.alibaba.otter.canal.client.adapter.es8x.support.ESConnection;
|
||||
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.OuterAdapterConfig;
|
||||
import com.alibaba.otter.canal.client.adapter.support.SPI;
|
||||
import org.elasticsearch.action.search.SearchResponse;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
|
||||
/**
|
||||
* ES 8.x 外部适配器
|
||||
*
|
||||
* @author ymz 2013-02-23
|
||||
* @version 1.0.0
|
||||
*/
|
||||
@SPI("es8")
|
||||
public class ES8xAdapter extends ESAdapter {
|
||||
|
||||
private ESConnection esConnection;
|
||||
|
||||
public ESConnection getEsConnection() {
|
||||
return esConnection;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(OuterAdapterConfig configuration, Properties envProperties) {
|
||||
try {
|
||||
Map<String, String> properties = configuration.getProperties();
|
||||
|
||||
String[] hostArray = configuration.getHosts().split(",");
|
||||
esConnection = new ESConnection(hostArray, properties);
|
||||
|
||||
this.esTemplate = new ES8xTemplate(esConnection);
|
||||
|
||||
envProperties.put("es.version", "es8");
|
||||
super.init(configuration, envProperties);
|
||||
} catch (Throwable e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> count(String task) {
|
||||
ESSyncConfig config = esSyncConfig.get(task);
|
||||
ESSyncConfig.ESMapping mapping = config.getEsMapping();
|
||||
SearchResponse response = this.esConnection.new ESSearchRequest(mapping.getIndex()).size(0).getResponse();
|
||||
|
||||
long rowCount = response.getHits().getTotalHits().value;
|
||||
Map<String, Object> res = new LinkedHashMap<>();
|
||||
res.put("esIndex", mapping.getIndex());
|
||||
res.put("count", rowCount);
|
||||
return res;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EtlResult etl(String task, List<String> params) {
|
||||
EtlResult etlResult = new EtlResult();
|
||||
ESSyncConfig config = esSyncConfig.get(task);
|
||||
if (config != null) {
|
||||
DataSource dataSource = DatasourceConfig.DATA_SOURCES.get(config.getDataSourceKey());
|
||||
ESEtlService esEtlService = new ESEtlService(esConnection, config);
|
||||
if (dataSource != null) {
|
||||
return esEtlService.importData(params);
|
||||
} else {
|
||||
etlResult.setSucceeded(false);
|
||||
etlResult.setErrorMessage("DataSource not found");
|
||||
return etlResult;
|
||||
}
|
||||
} else {
|
||||
StringBuilder resultMsg = new StringBuilder();
|
||||
boolean resSuccess = true;
|
||||
for (ESSyncConfig configTmp : esSyncConfig.values()) {
|
||||
// 取所有的destination为task的配置
|
||||
if (configTmp.getDestination().equals(task)) {
|
||||
ESEtlService esEtlService = new ESEtlService(esConnection, configTmp);
|
||||
EtlResult etlRes = esEtlService.importData(params);
|
||||
if (!etlRes.getSucceeded()) {
|
||||
resSuccess = false;
|
||||
resultMsg.append(etlRes.getErrorMessage()).append("\n");
|
||||
} else {
|
||||
resultMsg.append(etlRes.getResultMessage()).append("\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
if (resultMsg.length() > 0) {
|
||||
etlResult.setSucceeded(resSuccess);
|
||||
if (resSuccess) {
|
||||
etlResult.setResultMessage(resultMsg.toString());
|
||||
} else {
|
||||
etlResult.setErrorMessage(resultMsg.toString());
|
||||
}
|
||||
return etlResult;
|
||||
}
|
||||
}
|
||||
etlResult.setSucceeded(false);
|
||||
etlResult.setErrorMessage("Task not found");
|
||||
return etlResult;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
super.destroy();
|
||||
if (esConnection != null) {
|
||||
esConnection.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
package com.alibaba.otter.canal.client.adapter.es8x.etl;
|
||||
|
||||
import java.sql.SQLException;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.elasticsearch.action.search.SearchResponse;
|
||||
import org.elasticsearch.index.query.QueryBuilders;
|
||||
import org.elasticsearch.search.SearchHit;
|
||||
|
||||
import com.alibaba.otter.canal.client.adapter.es.core.config.ESSyncConfig;
|
||||
import com.alibaba.otter.canal.client.adapter.es.core.config.ESSyncConfig.ESMapping;
|
||||
import com.alibaba.otter.canal.client.adapter.es.core.config.SchemaItem.FieldItem;
|
||||
import com.alibaba.otter.canal.client.adapter.es.core.support.ESBulkRequest;
|
||||
import com.alibaba.otter.canal.client.adapter.es.core.support.ESBulkRequest.ESBulkResponse;
|
||||
import com.alibaba.otter.canal.client.adapter.es.core.support.ESBulkRequest.ESIndexRequest;
|
||||
import com.alibaba.otter.canal.client.adapter.es.core.support.ESBulkRequest.ESUpdateRequest;
|
||||
import com.alibaba.otter.canal.client.adapter.es.core.support.ESTemplate;
|
||||
import com.alibaba.otter.canal.client.adapter.es8x.support.ES8xTemplate;
|
||||
import com.alibaba.otter.canal.client.adapter.es8x.support.ESConnection;
|
||||
import com.alibaba.otter.canal.client.adapter.es8x.support.ESConnection.ESSearchRequest;
|
||||
import com.alibaba.otter.canal.client.adapter.support.AbstractEtlService;
|
||||
import com.alibaba.otter.canal.client.adapter.support.AdapterConfig;
|
||||
import com.alibaba.otter.canal.client.adapter.support.EtlResult;
|
||||
import com.alibaba.otter.canal.client.adapter.support.Util;
|
||||
|
||||
/**
|
||||
* ES ETL Service
|
||||
*
|
||||
* @author rewerma 2018-11-01
|
||||
* @version 1.0.0
|
||||
*/
|
||||
public class ESEtlService extends AbstractEtlService {
|
||||
|
||||
private ESConnection esConnection;
|
||||
private ESTemplate esTemplate;
|
||||
private ESSyncConfig config;
|
||||
|
||||
public ESEtlService(ESConnection esConnection, ESSyncConfig config){
|
||||
super("ES", config);
|
||||
this.esConnection = esConnection;
|
||||
this.esTemplate = new ES8xTemplate(esConnection);
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
public EtlResult importData(List<String> params) {
|
||||
ESMapping mapping = config.getEsMapping();
|
||||
logger.info("start etl to import data to index: {}", mapping.getIndex());
|
||||
String sql = mapping.getSql();
|
||||
return importData(sql, params);
|
||||
}
|
||||
|
||||
protected boolean executeSqlImport(DataSource ds, String sql, List<Object> values,
|
||||
AdapterConfig.AdapterMapping adapterMapping, AtomicLong impCount,
|
||||
List<String> errMsg) {
|
||||
try {
|
||||
ESMapping mapping = (ESMapping) adapterMapping;
|
||||
Util.sqlRS(ds, sql, values, rs -> {
|
||||
int count = 0;
|
||||
try {
|
||||
ESBulkRequest esBulkRequest = this.esConnection.new ES8xBulkRequest();
|
||||
|
||||
long batchBegin = System.currentTimeMillis();
|
||||
while (rs.next()) {
|
||||
Map<String, Object> esFieldData = new LinkedHashMap<>();
|
||||
Object idVal = null;
|
||||
for (FieldItem fieldItem : mapping.getSchemaItem().getSelectFields().values()) {
|
||||
|
||||
String fieldName = fieldItem.getFieldName();
|
||||
if (mapping.getSkips().contains(fieldName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 如果是主键字段则不插入
|
||||
if (fieldItem.getFieldName().equals(mapping.getId())) {
|
||||
idVal = esTemplate.getValFromRS(mapping, rs, fieldName, fieldName);
|
||||
} else {
|
||||
Object val = esTemplate.getValFromRS(mapping, rs, fieldName, fieldName);
|
||||
esFieldData.put(Util.cleanColumn(fieldName), val);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (!mapping.getRelations().isEmpty()) {
|
||||
mapping.getRelations().forEach((relationField, relationMapping) -> {
|
||||
Map<String, Object> relations = new HashMap<>();
|
||||
relations.put("name", relationMapping.getName());
|
||||
if (StringUtils.isNotEmpty(relationMapping.getParent())) {
|
||||
FieldItem parentFieldItem = mapping.getSchemaItem()
|
||||
.getSelectFields()
|
||||
.get(relationMapping.getParent());
|
||||
Object parentVal;
|
||||
try {
|
||||
parentVal = esTemplate.getValFromRS(mapping,
|
||||
rs,
|
||||
parentFieldItem.getFieldName(),
|
||||
parentFieldItem.getFieldName());
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
if (parentVal != null) {
|
||||
relations.put("parent", parentVal.toString());
|
||||
esFieldData.put("$parent_routing", parentVal.toString());
|
||||
|
||||
}
|
||||
}
|
||||
esFieldData.put(Util.cleanColumn(relationField), relations);
|
||||
});
|
||||
}
|
||||
|
||||
if (idVal != null) {
|
||||
String parentVal = (String) esFieldData.remove("$parent_routing");
|
||||
if (mapping.isUpsert()) {
|
||||
ESUpdateRequest esUpdateRequest = this.esConnection.new ES8xUpdateRequest(
|
||||
mapping.getIndex(),
|
||||
idVal.toString()).setDoc(esFieldData).setDocAsUpsert(true);
|
||||
|
||||
if (StringUtils.isNotEmpty(parentVal)) {
|
||||
esUpdateRequest.setRouting(parentVal);
|
||||
}
|
||||
|
||||
esBulkRequest.add(esUpdateRequest);
|
||||
} else {
|
||||
ESIndexRequest esIndexRequest = this.esConnection.new ES8xIndexRequest(
|
||||
mapping.getIndex(),
|
||||
idVal.toString()).setSource(esFieldData);
|
||||
if (StringUtils.isNotEmpty(parentVal)) {
|
||||
esIndexRequest.setRouting(parentVal);
|
||||
}
|
||||
esBulkRequest.add(esIndexRequest);
|
||||
}
|
||||
} else {
|
||||
idVal = esFieldData.get(mapping.getPk());
|
||||
ESSearchRequest esSearchRequest = this.esConnection.new ESSearchRequest(mapping.getIndex())
|
||||
.setQuery(QueryBuilders.termQuery(mapping.getPk(), idVal))
|
||||
.size(10000);
|
||||
SearchResponse response = esSearchRequest.getResponse();
|
||||
for (SearchHit hit : response.getHits()) {
|
||||
ESUpdateRequest esUpdateRequest = this.esConnection.new ES8xUpdateRequest(
|
||||
mapping.getIndex(),
|
||||
hit.getId()).setDoc(esFieldData);
|
||||
esBulkRequest.add(esUpdateRequest);
|
||||
}
|
||||
}
|
||||
|
||||
if (esBulkRequest.numberOfActions() % mapping.getCommitBatch() == 0
|
||||
&& esBulkRequest.numberOfActions() > 0) {
|
||||
long esBatchBegin = System.currentTimeMillis();
|
||||
ESBulkResponse rp = esBulkRequest.bulk();
|
||||
if (rp.hasFailures()) {
|
||||
rp.processFailBulkResponse("全量数据 etl 异常 ");
|
||||
}
|
||||
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("全量数据批量导入批次耗时: {}, es执行时间: {}, 批次大小: {}, index; {}",
|
||||
(System.currentTimeMillis() - batchBegin),
|
||||
(System.currentTimeMillis() - esBatchBegin),
|
||||
esBulkRequest.numberOfActions(),
|
||||
mapping.getIndex());
|
||||
}
|
||||
batchBegin = System.currentTimeMillis();
|
||||
esBulkRequest.resetBulk();
|
||||
}
|
||||
count++;
|
||||
impCount.incrementAndGet();
|
||||
}
|
||||
|
||||
if (esBulkRequest.numberOfActions() > 0) {
|
||||
long esBatchBegin = System.currentTimeMillis();
|
||||
ESBulkResponse rp = esBulkRequest.bulk();
|
||||
if (rp.hasFailures()) {
|
||||
rp.processFailBulkResponse("全量数据 etl 异常 ");
|
||||
}
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("全量数据批量导入最后批次耗时: {}, es执行时间: {}, 批次大小: {}, index; {}",
|
||||
(System.currentTimeMillis() - batchBegin),
|
||||
(System.currentTimeMillis() - esBatchBegin),
|
||||
esBulkRequest.numberOfActions(),
|
||||
mapping.getIndex());
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error(e.getMessage(), e);
|
||||
errMsg.add(mapping.getIndex() + " etl failed! ==>" + e.getMessage());
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return count;
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
logger.error(e.getMessage(), e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,467 @@
|
||||
package com.alibaba.otter.canal.client.adapter.es8x.support;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.elasticsearch.action.search.SearchResponse;
|
||||
import org.elasticsearch.cluster.metadata.MappingMetadata;
|
||||
import org.elasticsearch.index.query.BoolQueryBuilder;
|
||||
import org.elasticsearch.index.query.QueryBuilders;
|
||||
import org.elasticsearch.search.SearchHit;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.alibaba.otter.canal.client.adapter.es.core.config.ESSyncConfig;
|
||||
import com.alibaba.otter.canal.client.adapter.es.core.config.ESSyncConfig.ESMapping;
|
||||
import com.alibaba.otter.canal.client.adapter.es.core.config.SchemaItem;
|
||||
import com.alibaba.otter.canal.client.adapter.es.core.config.SchemaItem.ColumnItem;
|
||||
import com.alibaba.otter.canal.client.adapter.es.core.config.SchemaItem.FieldItem;
|
||||
import com.alibaba.otter.canal.client.adapter.es.core.support.ESBulkRequest;
|
||||
import com.alibaba.otter.canal.client.adapter.es.core.support.ESBulkRequest.ESBulkResponse;
|
||||
import com.alibaba.otter.canal.client.adapter.es.core.support.ESBulkRequest.ESDeleteRequest;
|
||||
import com.alibaba.otter.canal.client.adapter.es.core.support.ESBulkRequest.ESIndexRequest;
|
||||
import com.alibaba.otter.canal.client.adapter.es.core.support.ESBulkRequest.ESUpdateRequest;
|
||||
import com.alibaba.otter.canal.client.adapter.es.core.support.ESSyncUtil;
|
||||
import com.alibaba.otter.canal.client.adapter.es.core.support.ESTemplate;
|
||||
import com.alibaba.otter.canal.client.adapter.es8x.support.ESConnection.ESSearchRequest;
|
||||
import com.alibaba.otter.canal.client.adapter.support.DatasourceConfig;
|
||||
import com.alibaba.otter.canal.client.adapter.support.Util;
|
||||
|
||||
public class ES8xTemplate implements ESTemplate {
|
||||
|
||||
private static final Logger logger = LoggerFactory
|
||||
.getLogger(ESTemplate.class);
|
||||
|
||||
private static final int MAX_BATCH_SIZE = 1000;
|
||||
|
||||
private ESConnection esConnection;
|
||||
|
||||
private ESBulkRequest esBulkRequest;
|
||||
|
||||
// es 字段类型本地缓存
|
||||
private static ConcurrentMap<String, Map<String, String>> esFieldTypes = new ConcurrentHashMap<>();
|
||||
|
||||
public ES8xTemplate(ESConnection esConnection){
|
||||
this.esConnection = esConnection;
|
||||
this.esBulkRequest = this.esConnection.new ES8xBulkRequest();
|
||||
}
|
||||
|
||||
public ESBulkRequest getBulk() {
|
||||
return esBulkRequest;
|
||||
}
|
||||
|
||||
public void resetBulkRequestBuilder() {
|
||||
this.esBulkRequest.resetBulk();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void insert(ESMapping mapping, Object pkVal, Map<String, Object> esFieldData) {
|
||||
if (mapping.getId() != null) {
|
||||
String parentVal = (String) esFieldData.remove("$parent_routing");
|
||||
if (mapping.isUpsert()) {
|
||||
ESUpdateRequest updateRequest = esConnection.new ES8xUpdateRequest(mapping.getIndex(), pkVal.toString())
|
||||
.setDoc(esFieldData)
|
||||
.setDocAsUpsert(true);
|
||||
if (StringUtils.isNotEmpty(parentVal)) {
|
||||
updateRequest.setRouting(parentVal);
|
||||
}
|
||||
getBulk().add(updateRequest);
|
||||
} else {
|
||||
ESIndexRequest indexRequest = esConnection.new ES8xIndexRequest(mapping.getIndex(), pkVal.toString())
|
||||
.setSource(esFieldData);
|
||||
if (StringUtils.isNotEmpty(parentVal)) {
|
||||
indexRequest.setRouting(parentVal);
|
||||
}
|
||||
getBulk().add(indexRequest);
|
||||
}
|
||||
commitBulk();
|
||||
} else {
|
||||
ESSearchRequest esSearchRequest = this.esConnection.new ESSearchRequest(mapping.getIndex())
|
||||
.setQuery(QueryBuilders.termQuery(mapping.getPk(), pkVal))
|
||||
.size(10000);
|
||||
SearchResponse response = esSearchRequest.getResponse();
|
||||
|
||||
for (SearchHit hit : response.getHits()) {
|
||||
ESUpdateRequest esUpdateRequest = this.esConnection.new ES8xUpdateRequest(mapping.getIndex(),
|
||||
hit.getId()).setDoc(esFieldData);
|
||||
getBulk().add(esUpdateRequest);
|
||||
commitBulk();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(ESMapping mapping, Object pkVal, Map<String, Object> esFieldData) {
|
||||
Map<String, Object> esFieldDataTmp = new LinkedHashMap<>(esFieldData.size());
|
||||
esFieldData.forEach((k, v) -> esFieldDataTmp.put(Util.cleanColumn(k), v));
|
||||
append4Update(mapping, pkVal, esFieldDataTmp);
|
||||
commitBulk();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateByQuery(ESSyncConfig config, Map<String, Object> paramsTmp, Map<String, Object> esFieldData) {
|
||||
if (paramsTmp.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
ESMapping mapping = config.getEsMapping();
|
||||
BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery();
|
||||
paramsTmp.forEach((fieldName, value) -> queryBuilder.must(QueryBuilders.termsQuery(fieldName, value)));
|
||||
|
||||
// 查询sql批量更新
|
||||
DataSource ds = DatasourceConfig.DATA_SOURCES.get(config.getDataSourceKey());
|
||||
StringBuilder sql = new StringBuilder("SELECT * FROM (" + mapping.getSql() + ") _v WHERE ");
|
||||
List<Object> values = new ArrayList<>();
|
||||
paramsTmp.forEach((fieldName, value) -> {
|
||||
sql.append("_v.").append(fieldName).append("=? AND ");
|
||||
values.add(value);
|
||||
});
|
||||
// TODO 直接外部包裹sql会导致全表扫描性能低, 待优化拼接内部where条件
|
||||
int len = sql.length();
|
||||
sql.delete(len - 4, len);
|
||||
Integer syncCount = (Integer) Util.sqlRS(ds, sql.toString(), values, rs -> {
|
||||
int count = 0;
|
||||
try {
|
||||
while (rs.next()) {
|
||||
Object idVal = getIdValFromRS(mapping, rs);
|
||||
append4Update(mapping, idVal, esFieldData);
|
||||
commitBulk();
|
||||
count++;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return count;
|
||||
});
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Update ES by query affected {} records", syncCount);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(ESMapping mapping, Object pkVal, Map<String, Object> esFieldData) {
|
||||
if (mapping.getId() != null) {
|
||||
ESDeleteRequest esDeleteRequest = this.esConnection.new ES8xDeleteRequest(mapping.getIndex(),
|
||||
pkVal.toString());
|
||||
getBulk().add(esDeleteRequest);
|
||||
commitBulk();
|
||||
} else {
|
||||
ESSearchRequest esSearchRequest = this.esConnection.new ESSearchRequest(mapping.getIndex())
|
||||
.setQuery(QueryBuilders.termQuery(mapping.getPk(), pkVal))
|
||||
.size(10000);
|
||||
SearchResponse response = esSearchRequest.getResponse();
|
||||
for (SearchHit hit : response.getHits()) {
|
||||
ESUpdateRequest esUpdateRequest = this.esConnection.new ES8xUpdateRequest(mapping.getIndex(),
|
||||
hit.getId()).setDoc(esFieldData);
|
||||
getBulk().add(esUpdateRequest);
|
||||
commitBulk();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void commit() {
|
||||
if (getBulk().numberOfActions() > 0) {
|
||||
ESBulkResponse response = getBulk().bulk();
|
||||
if (response.hasFailures()) {
|
||||
response.processFailBulkResponse("ES sync commit error ");
|
||||
}
|
||||
resetBulkRequestBuilder();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getValFromRS(ESMapping mapping, ResultSet resultSet, String fieldName,
|
||||
String columnName) throws SQLException {
|
||||
fieldName = Util.cleanColumn(fieldName);
|
||||
columnName = Util.cleanColumn(columnName);
|
||||
String esType = getEsType(mapping, fieldName);
|
||||
|
||||
Object value = resultSet.getObject(columnName);
|
||||
if (value instanceof Boolean) {
|
||||
if (!"boolean".equals(esType)) {
|
||||
value = resultSet.getByte(columnName);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果是对象类型
|
||||
if (mapping.getObjFields().containsKey(fieldName)) {
|
||||
return ESSyncUtil.convertToEsObj(value, mapping.getObjFields().get(fieldName));
|
||||
} else {
|
||||
return ESSyncUtil.typeConvert(value, esType);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getESDataFromRS(ESMapping mapping, ResultSet resultSet,
|
||||
Map<String, Object> esFieldData) throws SQLException {
|
||||
SchemaItem schemaItem = mapping.getSchemaItem();
|
||||
String idFieldName = mapping.getId() == null ? mapping.getPk() : mapping.getId();
|
||||
Object resultIdVal = null;
|
||||
for (FieldItem fieldItem : schemaItem.getSelectFields().values()) {
|
||||
Object value = getValFromRS(mapping, resultSet, fieldItem.getFieldName(), fieldItem.getFieldName());
|
||||
|
||||
if (fieldItem.getFieldName().equals(idFieldName)) {
|
||||
resultIdVal = value;
|
||||
}
|
||||
|
||||
if (!fieldItem.getFieldName().equals(mapping.getId())
|
||||
&& !mapping.getSkips().contains(fieldItem.getFieldName())) {
|
||||
esFieldData.put(Util.cleanColumn(fieldItem.getFieldName()), value);
|
||||
}
|
||||
}
|
||||
|
||||
// 添加父子文档关联信息
|
||||
putRelationDataFromRS(mapping, schemaItem, resultSet, esFieldData);
|
||||
|
||||
return resultIdVal;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getIdValFromRS(ESMapping mapping, ResultSet resultSet) throws SQLException {
|
||||
SchemaItem schemaItem = mapping.getSchemaItem();
|
||||
String idFieldName = mapping.getId() == null ? mapping.getPk() : mapping.getId();
|
||||
Object resultIdVal = null;
|
||||
for (FieldItem fieldItem : schemaItem.getSelectFields().values()) {
|
||||
Object value = getValFromRS(mapping, resultSet, fieldItem.getFieldName(), fieldItem.getFieldName());
|
||||
|
||||
if (fieldItem.getFieldName().equals(idFieldName)) {
|
||||
resultIdVal = value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return resultIdVal;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getESDataFromRS(ESMapping mapping, ResultSet resultSet, Map<String, Object> dmlOld,
|
||||
Map<String, Object> esFieldData) throws SQLException {
|
||||
SchemaItem schemaItem = mapping.getSchemaItem();
|
||||
String idFieldName = mapping.getId() == null ? mapping.getPk() : mapping.getId();
|
||||
Object resultIdVal = null;
|
||||
for (FieldItem fieldItem : schemaItem.getSelectFields().values()) {
|
||||
if (fieldItem.getFieldName().equals(idFieldName)) {
|
||||
resultIdVal = getValFromRS(mapping, resultSet, fieldItem.getFieldName(), fieldItem.getFieldName());
|
||||
}
|
||||
|
||||
for (ColumnItem columnItem : fieldItem.getColumnItems()) {
|
||||
if (dmlOld.containsKey(columnItem.getColumnName())
|
||||
&& !mapping.getSkips().contains(fieldItem.getFieldName())) {
|
||||
esFieldData.put(Util.cleanColumn(fieldItem.getFieldName()),
|
||||
getValFromRS(mapping, resultSet, fieldItem.getFieldName(), fieldItem.getFieldName()));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 添加父子文档关联信息
|
||||
putRelationDataFromRS(mapping, schemaItem, resultSet, esFieldData);
|
||||
|
||||
return resultIdVal;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getValFromData(ESMapping mapping, Map<String, Object> dmlData, String fieldName, String columnName) {
|
||||
String esType = getEsType(mapping, fieldName);
|
||||
Object value = dmlData.get(columnName);
|
||||
if (value instanceof Byte) {
|
||||
if ("boolean".equals(esType)) {
|
||||
value = ((Byte) value).intValue() != 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果是对象类型
|
||||
if (mapping.getObjFields().containsKey(fieldName)) {
|
||||
return ESSyncUtil.convertToEsObj(value, mapping.getObjFields().get(fieldName));
|
||||
} else {
|
||||
return ESSyncUtil.typeConvert(value, esType);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getESDataFromDmlData(ESMapping mapping, Map<String, Object> dmlData,
|
||||
Map<String, Object> esFieldData) {
|
||||
SchemaItem schemaItem = mapping.getSchemaItem();
|
||||
String idFieldName = mapping.getId() == null ? mapping.getPk() : mapping.getId();
|
||||
Object resultIdVal = null;
|
||||
for (FieldItem fieldItem : schemaItem.getSelectFields().values()) {
|
||||
String columnName = fieldItem.getColumnItems().iterator().next().getColumnName();
|
||||
Object value = getValFromData(mapping, dmlData, fieldItem.getFieldName(), columnName);
|
||||
|
||||
if (fieldItem.getFieldName().equals(idFieldName)) {
|
||||
resultIdVal = value;
|
||||
}
|
||||
|
||||
if (!fieldItem.getFieldName().equals(mapping.getId())
|
||||
&& !mapping.getSkips().contains(fieldItem.getFieldName())) {
|
||||
esFieldData.put(Util.cleanColumn(fieldItem.getFieldName()), value);
|
||||
}
|
||||
}
|
||||
|
||||
// 添加父子文档关联信息
|
||||
putRelationData(mapping, schemaItem, dmlData, esFieldData);
|
||||
return resultIdVal;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getESDataFromDmlData(ESMapping mapping, String owner, Map<String, Object> dmlData,
|
||||
Map<String, Object> dmlOld, Map<String, Object> esFieldData) {
|
||||
SchemaItem schemaItem = mapping.getSchemaItem();
|
||||
String idFieldName = mapping.getId() == null ? mapping.getPk() : mapping.getId();
|
||||
Object resultIdVal = null;
|
||||
for (FieldItem fieldItem : schemaItem.getSelectFields().values()) {
|
||||
ColumnItem columnItem = fieldItem.getColumnItems().iterator().next();
|
||||
if (!columnItem.getOwner().equals(owner)) {
|
||||
continue;
|
||||
}
|
||||
String columnName = columnItem.getColumnName();
|
||||
|
||||
if (fieldItem.getFieldName().equals(idFieldName)) {
|
||||
resultIdVal = getValFromData(mapping, dmlData, fieldItem.getFieldName(), columnName);
|
||||
}
|
||||
|
||||
if (dmlOld.containsKey(columnName) && !mapping.getSkips().contains(fieldItem.getFieldName())) {
|
||||
esFieldData.put(Util.cleanColumn(fieldItem.getFieldName()),
|
||||
getValFromData(mapping, dmlData, fieldItem.getFieldName(), columnName));
|
||||
}
|
||||
}
|
||||
|
||||
// 添加父子文档关联信息
|
||||
putRelationData(mapping, schemaItem, dmlOld, esFieldData);
|
||||
return resultIdVal;
|
||||
}
|
||||
|
||||
/**
|
||||
* 如果大于批量数则提交批次
|
||||
*/
|
||||
private void commitBulk() {
|
||||
if (getBulk().numberOfActions() >= MAX_BATCH_SIZE) {
|
||||
commit();
|
||||
}
|
||||
}
|
||||
|
||||
private void append4Update(ESMapping mapping, Object pkVal, Map<String, Object> esFieldData) {
|
||||
if (mapping.getId() != null) {
|
||||
String parentVal = (String) esFieldData.remove("$parent_routing");
|
||||
if (mapping.isUpsert()) {
|
||||
ESUpdateRequest esUpdateRequest = this.esConnection.new ES8xUpdateRequest(mapping.getIndex(),
|
||||
pkVal.toString()).setDoc(esFieldData).setDocAsUpsert(true);
|
||||
if (StringUtils.isNotEmpty(parentVal)) {
|
||||
esUpdateRequest.setRouting(parentVal);
|
||||
}
|
||||
getBulk().add(esUpdateRequest);
|
||||
} else {
|
||||
ESUpdateRequest esUpdateRequest = this.esConnection.new ES8xUpdateRequest(mapping.getIndex(),
|
||||
pkVal.toString()).setDoc(esFieldData);
|
||||
if (StringUtils.isNotEmpty(parentVal)) {
|
||||
esUpdateRequest.setRouting(parentVal);
|
||||
}
|
||||
getBulk().add(esUpdateRequest);
|
||||
}
|
||||
} else {
|
||||
ESSearchRequest esSearchRequest = this.esConnection.new ESSearchRequest(mapping.getIndex())
|
||||
.setQuery(QueryBuilders.termQuery(mapping.getPk(), pkVal))
|
||||
.size(10000);
|
||||
SearchResponse response = esSearchRequest.getResponse();
|
||||
for (SearchHit hit : response.getHits()) {
|
||||
ESUpdateRequest esUpdateRequest = this.esConnection.new ES8xUpdateRequest(mapping.getIndex(),
|
||||
hit.getId()).setDoc(esFieldData);
|
||||
getBulk().add(esUpdateRequest);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取es mapping中的属性类型
|
||||
*
|
||||
* @param mapping mapping配置
|
||||
* @param fieldName 属性名
|
||||
* @return 类型
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private String getEsType(ESMapping mapping, String fieldName) {
|
||||
String key = mapping.getIndex() + "-" + mapping.getType();
|
||||
Map<String, String> fieldType = esFieldTypes.get(key);
|
||||
if (fieldType != null) {
|
||||
return fieldType.get(fieldName);
|
||||
} else {
|
||||
MappingMetadata mappingMetaData = esConnection.getMapping(mapping.getIndex());
|
||||
|
||||
if (mappingMetaData == null) {
|
||||
throw new IllegalArgumentException("Not found the mapping info of index: " + mapping.getIndex());
|
||||
}
|
||||
|
||||
fieldType = new LinkedHashMap<>();
|
||||
|
||||
Map<String, Object> sourceMap = mappingMetaData.getSourceAsMap();
|
||||
Map<String, Object> esMapping = (Map<String, Object>) sourceMap.get("properties");
|
||||
for (Map.Entry<String, Object> entry : esMapping.entrySet()) {
|
||||
Map<String, Object> value = (Map<String, Object>) entry.getValue();
|
||||
if (value.containsKey("properties")) {
|
||||
fieldType.put(entry.getKey(), "object");
|
||||
} else {
|
||||
fieldType.put(entry.getKey(), (String) value.get("type"));
|
||||
}
|
||||
}
|
||||
esFieldTypes.put(key, fieldType);
|
||||
|
||||
return fieldType.get(fieldName);
|
||||
}
|
||||
}
|
||||
|
||||
private void putRelationDataFromRS(ESMapping mapping, SchemaItem schemaItem, ResultSet resultSet,
|
||||
Map<String, Object> esFieldData) {
|
||||
// 添加父子文档关联信息
|
||||
if (!mapping.getRelations().isEmpty()) {
|
||||
mapping.getRelations().forEach((relationField, relationMapping) -> {
|
||||
Map<String, Object> relations = new HashMap<>();
|
||||
relations.put("name", relationMapping.getName());
|
||||
if (StringUtils.isNotEmpty(relationMapping.getParent())) {
|
||||
FieldItem parentFieldItem = schemaItem.getSelectFields().get(relationMapping.getParent());
|
||||
Object parentVal;
|
||||
try {
|
||||
parentVal = getValFromRS(mapping,
|
||||
resultSet,
|
||||
parentFieldItem.getFieldName(),
|
||||
parentFieldItem.getFieldName());
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
if (parentVal != null) {
|
||||
relations.put("parent", parentVal.toString());
|
||||
esFieldData.put("$parent_routing", parentVal.toString());
|
||||
|
||||
}
|
||||
}
|
||||
esFieldData.put(relationField, relations);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void putRelationData(ESMapping mapping, SchemaItem schemaItem, Map<String, Object> dmlData,
|
||||
Map<String, Object> esFieldData) {
|
||||
// 添加父子文档关联信息
|
||||
if (!mapping.getRelations().isEmpty()) {
|
||||
mapping.getRelations().forEach((relationField, relationMapping) -> {
|
||||
Map<String, Object> relations = new HashMap<>();
|
||||
relations.put("name", relationMapping.getName());
|
||||
if (StringUtils.isNotEmpty(relationMapping.getParent())) {
|
||||
FieldItem parentFieldItem = schemaItem.getSelectFields().get(relationMapping.getParent());
|
||||
String columnName = parentFieldItem.getColumnItems().iterator().next().getColumnName();
|
||||
Object parentVal = getValFromData(mapping, dmlData, parentFieldItem.getFieldName(), columnName);
|
||||
if (parentVal != null) {
|
||||
relations.put("parent", parentVal.toString());
|
||||
esFieldData.put("$parent_routing", parentVal.toString());
|
||||
|
||||
}
|
||||
}
|
||||
esFieldData.put(relationField, relations);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,449 @@
|
||||
package com.alibaba.otter.canal.client.adapter.es8x.support;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.UnknownHostException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.security.KeyStore;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.apache.http.HttpHost;
|
||||
import org.apache.http.auth.AuthScope;
|
||||
import org.apache.http.auth.UsernamePasswordCredentials;
|
||||
import org.apache.http.client.CredentialsProvider;
|
||||
import org.apache.http.impl.client.BasicCredentialsProvider;
|
||||
import org.apache.http.ssl.SSLContextBuilder;
|
||||
import org.apache.http.ssl.SSLContexts;
|
||||
import org.elasticsearch.action.bulk.BulkItemResponse;
|
||||
import org.elasticsearch.action.bulk.BulkRequest;
|
||||
import org.elasticsearch.action.bulk.BulkRequestBuilder;
|
||||
import org.elasticsearch.action.bulk.BulkResponse;
|
||||
import org.elasticsearch.action.delete.DeleteRequest;
|
||||
import org.elasticsearch.action.delete.DeleteRequestBuilder;
|
||||
import org.elasticsearch.action.index.IndexRequest;
|
||||
import org.elasticsearch.action.index.IndexRequestBuilder;
|
||||
import org.elasticsearch.action.search.SearchRequest;
|
||||
import org.elasticsearch.action.search.SearchRequestBuilder;
|
||||
import org.elasticsearch.action.search.SearchResponse;
|
||||
import org.elasticsearch.action.update.UpdateRequest;
|
||||
import org.elasticsearch.action.update.UpdateRequestBuilder;
|
||||
import org.elasticsearch.client.*;
|
||||
import org.elasticsearch.client.indices.GetMappingsRequest;
|
||||
import org.elasticsearch.client.indices.GetMappingsResponse;
|
||||
import org.elasticsearch.cluster.metadata.MappingMetadata;
|
||||
import org.elasticsearch.index.query.QueryBuilder;
|
||||
import org.elasticsearch.rest.RestStatus;
|
||||
import org.elasticsearch.search.builder.SearchSourceBuilder;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.alibaba.otter.canal.client.adapter.es.core.support.ESBulkRequest;
|
||||
|
||||
/**
|
||||
* ES 连接器, 只支持 Rest 方式
|
||||
*
|
||||
* @author ymz 2023-03-02
|
||||
* @version 1.0.0
|
||||
*/
|
||||
public class ESConnection {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ESConnection.class);
|
||||
|
||||
private RestHighLevelClient restHighLevelClient;
|
||||
|
||||
public ESConnection(String[] hosts, Map<String, String> properties) throws UnknownHostException{
|
||||
String caPath = properties.get("security.ca.path");
|
||||
if (StringUtils.isNotEmpty(caPath)) {
|
||||
connectEsWithCa(hosts, properties, caPath);
|
||||
} else {
|
||||
connectEsWithoutCa(hosts, properties);
|
||||
}
|
||||
}
|
||||
|
||||
private void connectEsWithCa(String[] hosts, Map<String, String> properties, String caPath) {
|
||||
Path caCertificatePath = Paths.get(caPath);
|
||||
try (InputStream is = Files.newInputStream(caCertificatePath)) {
|
||||
CertificateFactory factory = CertificateFactory.getInstance("X.509");
|
||||
Certificate trustedCa = factory.generateCertificate(is);
|
||||
KeyStore trustStore = KeyStore.getInstance("pkcs12");
|
||||
trustStore.load(null, null);
|
||||
trustStore.setCertificateEntry("ca", trustedCa);
|
||||
SSLContextBuilder sslContextBuilder = SSLContexts.custom().loadTrustMaterial(trustStore, null);
|
||||
final SSLContext sslContext = sslContextBuilder.build();
|
||||
|
||||
HttpHost[] httpHosts = Arrays.stream(hosts).map(this::createHttpHost).toArray(HttpHost[]::new);
|
||||
RestClientBuilder restClientBuilder = RestClient.builder(httpHosts);
|
||||
String nameAndPwd = properties.get("security.auth");
|
||||
if (StringUtils.isNotEmpty(nameAndPwd) && nameAndPwd.contains(":")) {
|
||||
String[] nameAndPwdArr = nameAndPwd.split(":");
|
||||
final CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
|
||||
credentialsProvider.setCredentials(AuthScope.ANY,
|
||||
new UsernamePasswordCredentials(nameAndPwdArr[0], nameAndPwdArr[1]));
|
||||
restClientBuilder.setHttpClientConfigCallback(httpClientBuilder -> {
|
||||
httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider);
|
||||
return httpClientBuilder.setSSLContext(sslContext);
|
||||
});
|
||||
}
|
||||
restHighLevelClient = new RestHighLevelClientBuilder(restClientBuilder.build())
|
||||
.setApiCompatibilityMode(true)
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void connectEsWithoutCa(String[] hosts, Map<String, String> properties) {
|
||||
HttpHost[] httpHosts = Arrays.stream(hosts).map(this::createHttpHost).toArray(HttpHost[]::new);
|
||||
RestClientBuilder restClientBuilder = RestClient.builder(httpHosts);
|
||||
String nameAndPwd = properties.get("security.auth");
|
||||
if (StringUtils.isNotEmpty(nameAndPwd) && nameAndPwd.contains(":")) {
|
||||
String[] nameAndPwdArr = nameAndPwd.split(":");
|
||||
final CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
|
||||
credentialsProvider.setCredentials(AuthScope.ANY,
|
||||
new UsernamePasswordCredentials(nameAndPwdArr[0], nameAndPwdArr[1]));
|
||||
restClientBuilder.setHttpClientConfigCallback(
|
||||
httpClientBuilder -> httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider));
|
||||
}
|
||||
restHighLevelClient = new RestHighLevelClientBuilder(restClientBuilder.build()).setApiCompatibilityMode(true)
|
||||
.build();
|
||||
}
|
||||
|
||||
public void close() {
|
||||
try {
|
||||
restHighLevelClient.close();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public MappingMetadata getMapping(String index) {
|
||||
MappingMetadata mappingMetaData = null;
|
||||
|
||||
Map<String, MappingMetadata> mappings;
|
||||
try {
|
||||
GetMappingsRequest request = new GetMappingsRequest();
|
||||
request.indices(index);
|
||||
GetMappingsResponse response = restHighLevelClient.indices().getMapping(request, RequestOptions.DEFAULT);
|
||||
|
||||
mappings = response.mappings();
|
||||
} catch (NullPointerException e) {
|
||||
throw new IllegalArgumentException("Not found the mapping info of index: " + index);
|
||||
} catch (IOException e) {
|
||||
logger.error(e.getMessage(), e);
|
||||
return null;
|
||||
}
|
||||
mappingMetaData = mappings.get(index);
|
||||
|
||||
return mappingMetaData;
|
||||
}
|
||||
|
||||
public class ES8xIndexRequest implements ESBulkRequest.ESIndexRequest {
|
||||
|
||||
private IndexRequestBuilder indexRequestBuilder;
|
||||
|
||||
private IndexRequest indexRequest;
|
||||
|
||||
public ES8xIndexRequest(String index, String id){
|
||||
indexRequest = new IndexRequest(index);
|
||||
indexRequest.id(id);
|
||||
|
||||
}
|
||||
|
||||
public ES8xIndexRequest setSource(Map<String, ?> source) {
|
||||
|
||||
indexRequest.source(source);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public ES8xIndexRequest setRouting(String routing) {
|
||||
|
||||
indexRequest.routing(routing);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public IndexRequestBuilder getIndexRequestBuilder() {
|
||||
return indexRequestBuilder;
|
||||
}
|
||||
|
||||
public void setIndexRequestBuilder(IndexRequestBuilder indexRequestBuilder) {
|
||||
this.indexRequestBuilder = indexRequestBuilder;
|
||||
}
|
||||
|
||||
public IndexRequest getIndexRequest() {
|
||||
return indexRequest;
|
||||
}
|
||||
|
||||
public void setIndexRequest(IndexRequest indexRequest) {
|
||||
this.indexRequest = indexRequest;
|
||||
}
|
||||
}
|
||||
|
||||
public class ES8xUpdateRequest implements ESBulkRequest.ESUpdateRequest {
|
||||
|
||||
private UpdateRequestBuilder updateRequestBuilder;
|
||||
|
||||
private UpdateRequest updateRequest;
|
||||
|
||||
public ES8xUpdateRequest(String index, String id){
|
||||
|
||||
updateRequest = new UpdateRequest(index, id);
|
||||
}
|
||||
|
||||
public ES8xUpdateRequest setDoc(Map source) {
|
||||
|
||||
updateRequest.doc(source);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public ES8xUpdateRequest setDocAsUpsert(boolean shouldUpsertDoc) {
|
||||
|
||||
updateRequest.docAsUpsert(shouldUpsertDoc);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public ES8xUpdateRequest setRouting(String routing) {
|
||||
|
||||
updateRequest.routing(routing);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public UpdateRequestBuilder getUpdateRequestBuilder() {
|
||||
return updateRequestBuilder;
|
||||
}
|
||||
|
||||
public void setUpdateRequestBuilder(UpdateRequestBuilder updateRequestBuilder) {
|
||||
this.updateRequestBuilder = updateRequestBuilder;
|
||||
}
|
||||
|
||||
public UpdateRequest getUpdateRequest() {
|
||||
return updateRequest;
|
||||
}
|
||||
|
||||
public void setUpdateRequest(UpdateRequest updateRequest) {
|
||||
this.updateRequest = updateRequest;
|
||||
}
|
||||
}
|
||||
|
||||
public class ES8xDeleteRequest implements ESBulkRequest.ESDeleteRequest {
|
||||
|
||||
private DeleteRequestBuilder deleteRequestBuilder;
|
||||
|
||||
private DeleteRequest deleteRequest;
|
||||
|
||||
public ES8xDeleteRequest(String index, String id){
|
||||
|
||||
deleteRequest = new DeleteRequest(index, id);
|
||||
|
||||
}
|
||||
|
||||
public DeleteRequestBuilder getDeleteRequestBuilder() {
|
||||
return deleteRequestBuilder;
|
||||
}
|
||||
|
||||
public void setDeleteRequestBuilder(DeleteRequestBuilder deleteRequestBuilder) {
|
||||
this.deleteRequestBuilder = deleteRequestBuilder;
|
||||
}
|
||||
|
||||
public DeleteRequest getDeleteRequest() {
|
||||
return deleteRequest;
|
||||
}
|
||||
|
||||
public void setDeleteRequest(DeleteRequest deleteRequest) {
|
||||
this.deleteRequest = deleteRequest;
|
||||
}
|
||||
}
|
||||
|
||||
public class ESSearchRequest {
|
||||
|
||||
private SearchRequestBuilder searchRequestBuilder;
|
||||
|
||||
private SearchRequest searchRequest;
|
||||
|
||||
private SearchSourceBuilder sourceBuilder;
|
||||
|
||||
public ESSearchRequest(String index){
|
||||
|
||||
searchRequest = new SearchRequest(index);
|
||||
sourceBuilder = new SearchSourceBuilder();
|
||||
|
||||
}
|
||||
|
||||
public ESSearchRequest setQuery(QueryBuilder queryBuilder) {
|
||||
|
||||
sourceBuilder.query(queryBuilder);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public ESSearchRequest size(int size) {
|
||||
|
||||
sourceBuilder.size(size);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public SearchResponse getResponse() {
|
||||
|
||||
searchRequest.source(sourceBuilder);
|
||||
try {
|
||||
return restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public SearchRequestBuilder getSearchRequestBuilder() {
|
||||
return searchRequestBuilder;
|
||||
}
|
||||
|
||||
public void setSearchRequestBuilder(SearchRequestBuilder searchRequestBuilder) {
|
||||
this.searchRequestBuilder = searchRequestBuilder;
|
||||
}
|
||||
|
||||
public SearchRequest getSearchRequest() {
|
||||
return searchRequest;
|
||||
}
|
||||
|
||||
public void setSearchRequest(SearchRequest searchRequest) {
|
||||
this.searchRequest = searchRequest;
|
||||
}
|
||||
}
|
||||
|
||||
public class ES8xBulkRequest implements ESBulkRequest {
|
||||
|
||||
private BulkRequestBuilder bulkRequestBuilder;
|
||||
|
||||
private BulkRequest bulkRequest;
|
||||
|
||||
public ES8xBulkRequest(){
|
||||
bulkRequest = new BulkRequest();
|
||||
}
|
||||
|
||||
public void resetBulk() {
|
||||
|
||||
bulkRequest = new BulkRequest();
|
||||
|
||||
}
|
||||
|
||||
public ES8xBulkRequest add(ESIndexRequest esIndexRequest) {
|
||||
ES8xIndexRequest eir = (ES8xIndexRequest) esIndexRequest;
|
||||
|
||||
bulkRequest.add(eir.indexRequest);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public ES8xBulkRequest add(ESUpdateRequest esUpdateRequest) {
|
||||
ES8xUpdateRequest eur = (ES8xUpdateRequest) esUpdateRequest;
|
||||
|
||||
bulkRequest.add(eur.updateRequest);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public ES8xBulkRequest add(ESDeleteRequest esDeleteRequest) {
|
||||
ES8xDeleteRequest edr = (ES8xDeleteRequest) esDeleteRequest;
|
||||
|
||||
bulkRequest.add(edr.deleteRequest);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public int numberOfActions() {
|
||||
return bulkRequest.numberOfActions();
|
||||
}
|
||||
|
||||
public ESBulkResponse bulk() {
|
||||
try {
|
||||
BulkResponse responses = restHighLevelClient.bulk(bulkRequest, RequestOptions.DEFAULT);
|
||||
return new ES8xBulkResponse(responses);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public BulkRequestBuilder getBulkRequestBuilder() {
|
||||
return bulkRequestBuilder;
|
||||
}
|
||||
|
||||
public void setBulkRequestBuilder(BulkRequestBuilder bulkRequestBuilder) {
|
||||
this.bulkRequestBuilder = bulkRequestBuilder;
|
||||
}
|
||||
|
||||
public BulkRequest getBulkRequest() {
|
||||
return bulkRequest;
|
||||
}
|
||||
|
||||
public void setBulkRequest(BulkRequest bulkRequest) {
|
||||
this.bulkRequest = bulkRequest;
|
||||
}
|
||||
}
|
||||
|
||||
public static class ES8xBulkResponse implements ESBulkRequest.ESBulkResponse {
|
||||
|
||||
private BulkResponse bulkResponse;
|
||||
|
||||
public ES8xBulkResponse(BulkResponse bulkResponse){
|
||||
this.bulkResponse = bulkResponse;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasFailures() {
|
||||
return bulkResponse.hasFailures();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processFailBulkResponse(String errorMsg) {
|
||||
for (BulkItemResponse itemResponse : bulkResponse.getItems()) {
|
||||
if (!itemResponse.isFailed()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (itemResponse.getFailure().getStatus() == RestStatus.NOT_FOUND) {
|
||||
logger.error(itemResponse.getFailureMessage());
|
||||
} else {
|
||||
throw new RuntimeException(errorMsg + itemResponse.getFailureMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public RestHighLevelClient getRestHighLevelClient() {
|
||||
return restHighLevelClient;
|
||||
}
|
||||
|
||||
public void setRestHighLevelClient(RestHighLevelClient restHighLevelClient) {
|
||||
this.restHighLevelClient = restHighLevelClient;
|
||||
}
|
||||
|
||||
private HttpHost createHttpHost(String uriStr) {
|
||||
URI uri = URI.create(uriStr);
|
||||
if (!org.springframework.util.StringUtils.hasLength(uri.getUserInfo())) {
|
||||
return HttpHost.create(uri.toString());
|
||||
}
|
||||
try {
|
||||
return HttpHost.create(new URI(uri
|
||||
.getScheme(), null, uri.getHost(), uri.getPort(), uri.getPath(), uri.getQuery(), uri.getFragment())
|
||||
.toString());
|
||||
} catch (URISyntaxException ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
es8=com.alibaba.otter.canal.client.adapter.es8x.ES8xAdapter
|
||||
20
client-adapter/es8x/src/main/resources/es8/biz_order.yml
Normal file
20
client-adapter/es8x/src/main/resources/es8/biz_order.yml
Normal file
@@ -0,0 +1,20 @@
|
||||
dataSourceKey: defaultDS
|
||||
destination: example
|
||||
groupId: g1
|
||||
esMapping:
|
||||
_index: customer
|
||||
_id: _id
|
||||
relations:
|
||||
customer_order:
|
||||
name: order
|
||||
parent: customer_id
|
||||
sql: "select concat('oid_', t.id) as _id,
|
||||
t.customer_id,
|
||||
t.id as order_id,
|
||||
t.serial_code as order_serial,
|
||||
t.c_time as order_time
|
||||
from biz_order t"
|
||||
skips:
|
||||
- customer_id
|
||||
etlCondition: "where t.c_time>={}"
|
||||
commitBatch: 3000
|
||||
46
client-adapter/es8x/src/main/resources/es8/customer.yml
Normal file
46
client-adapter/es8x/src/main/resources/es8/customer.yml
Normal file
@@ -0,0 +1,46 @@
|
||||
dataSourceKey: defaultDS
|
||||
destination: example
|
||||
groupId: g1
|
||||
esMapping:
|
||||
_index: customer
|
||||
_id: id
|
||||
relations:
|
||||
customer_order:
|
||||
name: customer
|
||||
sql: "select t.id, t.name, t.email from customer t"
|
||||
etlCondition: "where t.c_time>={}"
|
||||
commitBatch: 3000
|
||||
|
||||
|
||||
#{
|
||||
# "mappings":{
|
||||
# "_doc":{
|
||||
# "properties":{
|
||||
# "id": {
|
||||
# "type": "long"
|
||||
# },
|
||||
# "name": {
|
||||
# "type": "text"
|
||||
# },
|
||||
# "email": {
|
||||
# "type": "text"
|
||||
# },
|
||||
# "order_id": {
|
||||
# "type": "long"
|
||||
# },
|
||||
# "order_serial": {
|
||||
# "type": "text"
|
||||
# },
|
||||
# "order_time": {
|
||||
# "type": "date"
|
||||
# },
|
||||
# "customer_order":{
|
||||
# "type":"join",
|
||||
# "relations":{
|
||||
# "customer":"order"
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
#}
|
||||
15
client-adapter/es8x/src/main/resources/es8/mytest_user.yml
Normal file
15
client-adapter/es8x/src/main/resources/es8/mytest_user.yml
Normal file
@@ -0,0 +1,15 @@
|
||||
dataSourceKey: defaultDS
|
||||
destination: example
|
||||
groupId: g1
|
||||
esMapping:
|
||||
_index: mytest_user
|
||||
_id: _id
|
||||
# upsert: true
|
||||
# pk: id
|
||||
sql: "select a.id as _id, a.name, a.role_id, b.role_name,
|
||||
a.c_time from user a
|
||||
left join role b on b.id=a.role_id"
|
||||
# objFields:
|
||||
# _labels: array:;
|
||||
etlCondition: "where a.c_time>={}"
|
||||
commitBatch: 3000
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.alibaba.otter.canal.client.adapter.es8x.test;
|
||||
|
||||
import com.alibaba.otter.canal.client.adapter.es8x.support.ESConnection;
|
||||
import org.elasticsearch.cluster.metadata.MappingMetadata;
|
||||
import org.junit.Before;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Ignore
|
||||
public class ESConnectionTest {
|
||||
|
||||
ESConnection esConnection;
|
||||
|
||||
@Before
|
||||
public void init() throws UnknownHostException {
|
||||
String[] hosts = new String[]{"127.0.0.1:9200"};
|
||||
Map<String, String> properties = new HashMap<>();
|
||||
properties.put("cluster.name", "elasticsearch");
|
||||
esConnection = new ESConnection(hosts, properties);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test01() {
|
||||
MappingMetadata mappingMetaData = esConnection.getMapping("mytest_user");
|
||||
|
||||
Map<String, Object> sourceMap = mappingMetaData.getSourceAsMap();
|
||||
Map<String, Object> esMapping = (Map<String, Object>) sourceMap.get("properties");
|
||||
for (Map.Entry<String, Object> entry : esMapping.entrySet()) {
|
||||
Map<String, Object> value = (Map<String, Object>) entry.getValue();
|
||||
if (value.containsKey("properties")) {
|
||||
System.out.println(entry.getKey() + " object");
|
||||
} else {
|
||||
System.out.println(entry.getKey() + " " + value.get("type"));
|
||||
Assert.notNull(entry.getKey(), "null column name");
|
||||
Assert.notNull(value.get("type"), "null column type");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.alibaba.otter.canal.client.adapter.es8x.test;
|
||||
|
||||
import com.alibaba.druid.pool.DruidDataSource;
|
||||
|
||||
import java.sql.SQLException;
|
||||
|
||||
public class TestConstant {
|
||||
|
||||
public final static String jdbcUrl = "jdbc:mysql://127.0.0.1:3306/mytest?useUnicode=true";
|
||||
public final static String jdbcUser = "root";
|
||||
public final static String jdbcPassword = "121212";
|
||||
|
||||
public final static String esHosts = "127.0.0.1:9300";
|
||||
public final static String clusterName = "elasticsearch";
|
||||
|
||||
public final static DruidDataSource dataSource;
|
||||
|
||||
static {
|
||||
dataSource = new DruidDataSource();
|
||||
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
|
||||
dataSource.setUrl(jdbcUrl);
|
||||
dataSource.setUsername(jdbcUser);
|
||||
dataSource.setPassword(jdbcPassword);
|
||||
dataSource.setInitialSize(1);
|
||||
dataSource.setMinIdle(1);
|
||||
dataSource.setMaxActive(1);
|
||||
dataSource.setMaxWait(60000);
|
||||
dataSource.setTimeBetweenEvictionRunsMillis(60000);
|
||||
dataSource.setMinEvictableIdleTimeMillis(300000);
|
||||
dataSource.setPoolPreparedStatements(false);
|
||||
dataSource.setMaxPoolPreparedStatementPerConnectionSize(20);
|
||||
dataSource.setValidationQuery("select 1");
|
||||
try {
|
||||
dataSource.init();
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<artifactId>canal.client-adapter</artifactId>
|
||||
<groupId>com.alibaba.otter</groupId>
|
||||
<version>1.1.7-SNAPSHOT</version>
|
||||
<version>1.1.8</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
@@ -6,6 +6,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.alibaba.otter.canal.client.adapter.support.AdapterConfig;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
|
||||
/**
|
||||
* ES 映射配置
|
||||
@@ -28,13 +29,13 @@ public class ESSyncConfig implements AdapterConfig {
|
||||
private String esVersion = "es6";
|
||||
|
||||
public void validate() {
|
||||
if (esMapping._index == null) {
|
||||
if (esMapping.index == null) {
|
||||
throw new NullPointerException("esMapping._index");
|
||||
}
|
||||
if ("es6".equals(esVersion) && esMapping._type == null) {
|
||||
if ("es6".equals(esVersion) && esMapping.type == null) {
|
||||
throw new NullPointerException("esMapping._type");
|
||||
}
|
||||
if (esMapping._id == null && esMapping.getPk() == null) {
|
||||
if (esMapping.id == null && esMapping.getPk() == null) {
|
||||
throw new NullPointerException("esMapping._id or esMapping.pk");
|
||||
}
|
||||
if (esMapping.sql == null) {
|
||||
@@ -96,9 +97,14 @@ public class ESSyncConfig implements AdapterConfig {
|
||||
|
||||
public static class ESMapping implements AdapterMapping {
|
||||
|
||||
private String _index;
|
||||
private String _type;
|
||||
private String _id;
|
||||
@Value("${_index}")
|
||||
private String index;
|
||||
|
||||
@Value("${_type}")
|
||||
private String type;
|
||||
|
||||
@Value("${_id}")
|
||||
private String id;
|
||||
private boolean upsert = false;
|
||||
private String pk;
|
||||
private Map<String, RelationMapping> relations = new LinkedHashMap<>();
|
||||
@@ -114,28 +120,28 @@ public class ESSyncConfig implements AdapterConfig {
|
||||
|
||||
private SchemaItem schemaItem; // sql解析结果模型
|
||||
|
||||
public String get_index() {
|
||||
return _index;
|
||||
public String getIndex() {
|
||||
return index;
|
||||
}
|
||||
|
||||
public void set_index(String _index) {
|
||||
this._index = _index;
|
||||
public void setIndex(String index) {
|
||||
this.index = index;
|
||||
}
|
||||
|
||||
public String get_type() {
|
||||
return _type;
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void set_type(String _type) {
|
||||
this._type = _type;
|
||||
public void setType(String type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public String get_id() {
|
||||
return _id;
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void set_id(String _id) {
|
||||
this._id = _id;
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public boolean isUpsert() {
|
||||
|
||||
@@ -136,8 +136,8 @@ public class SchemaItem {
|
||||
}
|
||||
|
||||
public FieldItem getIdFieldItem(ESSyncConfig.ESMapping mapping) {
|
||||
if (mapping.get_id() != null) {
|
||||
return getSelectFields().get(mapping.get_id());
|
||||
if (mapping.getId() != null) {
|
||||
return getSelectFields().get(mapping.getId());
|
||||
} else {
|
||||
return getSelectFields().get(mapping.getPk());
|
||||
}
|
||||
|
||||
@@ -54,13 +54,13 @@ public class ESSyncService {
|
||||
for (ESSyncConfig config : esSyncConfigs) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Prepared to sync index: {}, destination: {}",
|
||||
config.getEsMapping().get_index(),
|
||||
config.getEsMapping().getIndex(),
|
||||
dml.getDestination());
|
||||
}
|
||||
this.sync(config, dml);
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Sync completed: {}, destination: {}",
|
||||
config.getEsMapping().get_index(),
|
||||
config.getEsMapping().getIndex(),
|
||||
dml.getDestination());
|
||||
}
|
||||
}
|
||||
@@ -73,7 +73,7 @@ public class ESSyncService {
|
||||
if (logger.isDebugEnabled()) {
|
||||
StringBuilder configIndexes = new StringBuilder();
|
||||
esSyncConfigs
|
||||
.forEach(esSyncConfig -> configIndexes.append(esSyncConfig.getEsMapping().get_index()).append(" "));
|
||||
.forEach(esSyncConfig -> configIndexes.append(esSyncConfig.getEsMapping().getIndex()).append(" "));
|
||||
logger.debug("DML: {} \nAffected indexes: {}",
|
||||
JSON.toJSONString(dml, JSONWriter.Feature.WriteNulls),
|
||||
configIndexes.toString());
|
||||
@@ -105,10 +105,10 @@ public class ESSyncService {
|
||||
logger.trace("Sync elapsed time: {} ms,destination: {}, es index: {}",
|
||||
(System.currentTimeMillis() - begin),
|
||||
dml.getDestination(),
|
||||
config.getEsMapping().get_index());
|
||||
config.getEsMapping().getIndex());
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
logger.error("sync error, es index: {}, DML : {}", config.getEsMapping().get_index(), dml);
|
||||
logger.error("sync error, es index: {}, DML : {}", config.getEsMapping().getIndex(), dml);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
@@ -210,7 +210,7 @@ public class ESSyncService {
|
||||
// ------主表 查询sql来更新------
|
||||
if (schemaItem.getMainTable().getTableName().equalsIgnoreCase(dml.getTable())) {
|
||||
ESMapping mapping = config.getEsMapping();
|
||||
String idFieldName = mapping.get_id() == null ? mapping.getPk() : mapping.get_id();
|
||||
String idFieldName = mapping.getId() == null ? mapping.getPk() : mapping.getId();
|
||||
FieldItem idFieldItem = schemaItem.getSelectFields().get(idFieldName);
|
||||
|
||||
boolean idFieldSimple = true;
|
||||
@@ -336,7 +336,7 @@ public class ESSyncService {
|
||||
|
||||
// ------是主表------
|
||||
if (schemaItem.getMainTable().getTableName().equalsIgnoreCase(dml.getTable())) {
|
||||
if (mapping.get_id() != null) {
|
||||
if (mapping.getId() != null) {
|
||||
FieldItem idFieldItem = schemaItem.getIdFieldItem(mapping);
|
||||
// 主键为简单字段
|
||||
if (!idFieldItem.isMethod() && !idFieldItem.isBinaryOp()) {
|
||||
@@ -349,7 +349,7 @@ public class ESSyncService {
|
||||
logger.trace("Main table delete es index, destination:{}, table: {}, index: {}, id: {}",
|
||||
config.getDestination(),
|
||||
dml.getTable(),
|
||||
mapping.get_index(),
|
||||
mapping.getIndex(),
|
||||
idVal);
|
||||
}
|
||||
esTemplate.delete(mapping, idVal, null);
|
||||
@@ -368,7 +368,7 @@ public class ESSyncService {
|
||||
logger.trace("Main table delete es index, destination:{}, table: {}, index: {}, pk: {}",
|
||||
config.getDestination(),
|
||||
dml.getTable(),
|
||||
mapping.get_index(),
|
||||
mapping.getIndex(),
|
||||
pkVal);
|
||||
}
|
||||
esFieldData.remove(pkFieldItem.getFieldName());
|
||||
@@ -438,7 +438,7 @@ public class ESSyncService {
|
||||
logger.trace("Single table insert to es index, destination:{}, table: {}, index: {}, id: {}",
|
||||
config.getDestination(),
|
||||
dml.getTable(),
|
||||
mapping.get_index(),
|
||||
mapping.getIndex(),
|
||||
idVal);
|
||||
}
|
||||
esTemplate.insert(mapping, idVal, esFieldData);
|
||||
@@ -461,7 +461,7 @@ public class ESSyncService {
|
||||
logger.trace("Main table insert to es index by query sql, destination:{}, table: {}, index: {}, sql: {}",
|
||||
config.getDestination(),
|
||||
dml.getTable(),
|
||||
mapping.get_index(),
|
||||
mapping.getIndex(),
|
||||
sql.replace("\n", " "));
|
||||
}
|
||||
Util.sqlRS(ds, sql, rs -> {
|
||||
@@ -475,7 +475,7 @@ public class ESSyncService {
|
||||
"Main table insert to es index by query sql, destination:{}, table: {}, index: {}, id: {}",
|
||||
config.getDestination(),
|
||||
dml.getTable(),
|
||||
mapping.get_index(),
|
||||
mapping.getIndex(),
|
||||
idVal);
|
||||
}
|
||||
esTemplate.insert(mapping, idVal, esFieldData);
|
||||
@@ -497,7 +497,7 @@ public class ESSyncService {
|
||||
logger.trace("Main table delete es index by query sql, destination:{}, table: {}, index: {}, sql: {}",
|
||||
config.getDestination(),
|
||||
dml.getTable(),
|
||||
mapping.get_index(),
|
||||
mapping.getIndex(),
|
||||
sql.replace("\n", " "));
|
||||
}
|
||||
Util.sqlRS(ds, sql, rs -> {
|
||||
@@ -519,7 +519,7 @@ public class ESSyncService {
|
||||
"Main table delete to es index by query sql, destination:{}, table: {}, index: {}, id: {}",
|
||||
config.getDestination(),
|
||||
dml.getTable(),
|
||||
mapping.get_index(),
|
||||
mapping.getIndex(),
|
||||
idVal);
|
||||
}
|
||||
esTemplate.delete(mapping, idVal, esFieldData);
|
||||
@@ -554,7 +554,7 @@ public class ESSyncService {
|
||||
|
||||
String fieldName = fieldItem.getFieldName();
|
||||
// 判断是否是主键
|
||||
if (fieldName.equals(mapping.get_id())) {
|
||||
if (fieldName.equals(mapping.getId())) {
|
||||
fieldName = "_id";
|
||||
}
|
||||
paramsTmp.put(fieldName, value);
|
||||
@@ -566,7 +566,7 @@ public class ESSyncService {
|
||||
logger.trace("Join table update es index by foreign key, destination:{}, table: {}, index: {}",
|
||||
config.getDestination(),
|
||||
dml.getTable(),
|
||||
mapping.get_index());
|
||||
mapping.getIndex());
|
||||
}
|
||||
esTemplate.updateByQuery(config, paramsTmp, esFieldData);
|
||||
}
|
||||
@@ -617,7 +617,7 @@ public class ESSyncService {
|
||||
logger.trace("Join table update es index by query sql, destination:{}, table: {}, index: {}, sql: {}",
|
||||
config.getDestination(),
|
||||
dml.getTable(),
|
||||
mapping.get_index(),
|
||||
mapping.getIndex(),
|
||||
sql.toString().replace("\n", " "));
|
||||
}
|
||||
Util.sqlRS(ds, sql.toString(), values, rs -> {
|
||||
@@ -661,7 +661,7 @@ public class ESSyncService {
|
||||
entry.getKey().getColumn().getColumnName());
|
||||
String fieldName = fieldItem.getFieldName();
|
||||
// 判断是否是主键
|
||||
if (fieldName.equals(mapping.get_id())) {
|
||||
if (fieldName.equals(mapping.getId())) {
|
||||
fieldName = "_id";
|
||||
}
|
||||
paramsTmp.put(fieldName, value);
|
||||
@@ -673,7 +673,7 @@ public class ESSyncService {
|
||||
logger.trace("Join table update es index by query sql, destination:{}, table: {}, index: {}",
|
||||
config.getDestination(),
|
||||
dml.getTable(),
|
||||
mapping.get_index());
|
||||
mapping.getIndex());
|
||||
}
|
||||
esTemplate.updateByQuery(config, paramsTmp, esFieldData);
|
||||
}
|
||||
@@ -721,7 +721,7 @@ public class ESSyncService {
|
||||
logger.trace("Join table update es index by query whole sql, destination:{}, table: {}, index: {}, sql: {}",
|
||||
config.getDestination(),
|
||||
dml.getTable(),
|
||||
mapping.get_index(),
|
||||
mapping.getIndex(),
|
||||
sql.toString().replace("\n", " "));
|
||||
}
|
||||
Util.sqlRS(ds, sql.toString(), rs -> {
|
||||
@@ -775,7 +775,7 @@ public class ESSyncService {
|
||||
.getValFromRS(mapping, rs, fieldItem.getFieldName(), fieldItem.getFieldName());
|
||||
String fieldName = fieldItem.getFieldName();
|
||||
// 判断是否是主键
|
||||
if (fieldName.equals(mapping.get_id())) {
|
||||
if (fieldName.equals(mapping.getId())) {
|
||||
fieldName = "_id";
|
||||
}
|
||||
paramsTmp.put(fieldName, value);
|
||||
@@ -787,7 +787,7 @@ public class ESSyncService {
|
||||
"Join table update es index by query whole sql, destination:{}, table: {}, index: {}",
|
||||
config.getDestination(),
|
||||
dml.getTable(),
|
||||
mapping.get_index());
|
||||
mapping.getIndex());
|
||||
}
|
||||
esTemplate.updateByQuery(config, paramsTmp, esFieldData);
|
||||
}
|
||||
@@ -817,7 +817,7 @@ public class ESSyncService {
|
||||
logger.trace("Main table update to es index, destination:{}, table: {}, index: {}, id: {}",
|
||||
config.getDestination(),
|
||||
dml.getTable(),
|
||||
mapping.get_index(),
|
||||
mapping.getIndex(),
|
||||
idVal);
|
||||
}
|
||||
esTemplate.update(mapping, idVal, esFieldData);
|
||||
@@ -840,7 +840,7 @@ public class ESSyncService {
|
||||
logger.trace("Main table update to es index by query sql, destination:{}, table: {}, index: {}, sql: {}",
|
||||
config.getDestination(),
|
||||
dml.getTable(),
|
||||
mapping.get_index(),
|
||||
mapping.getIndex(),
|
||||
sql.replace("\n", " "));
|
||||
}
|
||||
Util.sqlRS(ds, sql, rs -> {
|
||||
@@ -854,7 +854,7 @@ public class ESSyncService {
|
||||
"Main table update to es index by query sql, destination:{}, table: {}, index: {}, id: {}",
|
||||
config.getDestination(),
|
||||
dml.getTable(),
|
||||
mapping.get_index(),
|
||||
mapping.getIndex(),
|
||||
idVal);
|
||||
}
|
||||
esTemplate.update(mapping, idVal, esFieldData);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<artifactId>canal.client-adapter</artifactId>
|
||||
<groupId>com.alibaba.otter</groupId>
|
||||
<version>1.1.7-SNAPSHOT</version>
|
||||
<version>1.1.8</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
@@ -21,7 +21,6 @@
|
||||
<dependency>
|
||||
<groupId>org.apache.hbase</groupId>
|
||||
<artifactId>hbase-shaded-client</artifactId>
|
||||
<version>1.1.2</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.slf4j</groupId>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<artifactId>canal.client-adapter</artifactId>
|
||||
<groupId>com.alibaba.otter</groupId>
|
||||
<version>1.1.7-SNAPSHOT</version>
|
||||
<version>1.1.8</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
@@ -21,18 +21,15 @@
|
||||
<dependency>
|
||||
<groupId>org.apache.kudu</groupId>
|
||||
<artifactId>kudu-client</artifactId>
|
||||
<version>1.6.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>4.12</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>mysql</groupId>
|
||||
<artifactId>mysql-connector-java</artifactId>
|
||||
<version>5.1.48</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
@@ -2,11 +2,7 @@ package com.alibaba.otter.canal.client.adapter.kudu.service;
|
||||
|
||||
import java.sql.ResultSetMetaData;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
@@ -45,13 +41,13 @@ public class KuduEtlService extends AbstractEtlService {
|
||||
boolean flag = kuduTemplate.tableExists(kuduMapping.getTargetTable());
|
||||
// 表不存在,停止导入
|
||||
if (!flag) {
|
||||
logger.info("{} is don't hava,please check your kudu table !", kuduMapping.getTargetTable());
|
||||
logger.info("{} is don't exist , please check your kudu table !", kuduMapping.getTargetTable());
|
||||
errMsg.add(kuduMapping.getTargetTable() + " is don't hava,please check your kudu table !");
|
||||
etlResult.setErrorMessage(Joiner.on("\n").join(errMsg));
|
||||
return etlResult;
|
||||
}
|
||||
logger.info("{} etl is starting!", kuduMapping.getTargetTable());
|
||||
String sql = "SELECT * FROM " + kuduMapping.getDatabase() + "." + kuduMapping.getTable();
|
||||
String sql = "SELECT * FROM " + SyncUtil.getDbTableName(kuduMapping);
|
||||
return importData(sql, params);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<artifactId>canal.client-adapter</artifactId>
|
||||
<groupId>com.alibaba.otter</groupId>
|
||||
<version>1.1.7-SNAPSHOT</version>
|
||||
<version>1.1.8</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
@@ -17,7 +17,7 @@
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-dependencies</artifactId>
|
||||
<version>2.0.1.RELEASE</version>
|
||||
<version>2.5.15</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
@@ -74,6 +74,10 @@
|
||||
<artifactId>hbase-shaded-client</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ru.yandex.clickhouse</groupId>
|
||||
<artifactId>clickhouse-jdbc</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- outer adapter jar with dependencies-->
|
||||
<dependency>
|
||||
@@ -128,6 +132,19 @@
|
||||
<classifier>jar-with-dependencies</classifier>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.alibaba.otter</groupId>
|
||||
<artifactId>client-adapter.es8x</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<artifactId>*</artifactId>
|
||||
<groupId>*</groupId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
<classifier>jar-with-dependencies</classifier>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.alibaba.otter</groupId>
|
||||
<artifactId>client-adapter.rdb</artifactId>
|
||||
@@ -154,6 +171,19 @@
|
||||
<classifier>jar-with-dependencies</classifier>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.alibaba.otter</groupId>
|
||||
<artifactId>client-adapter.clickhouse</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<artifactId>*</artifactId>
|
||||
<groupId>*</groupId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
<classifier>jar-with-dependencies</classifier>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<!-- connector plugin -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba.otter</groupId>
|
||||
|
||||
@@ -43,6 +43,13 @@
|
||||
<include>**/*</include>
|
||||
</includes>
|
||||
</fileSet>
|
||||
<fileSet>
|
||||
<directory>../es8x/src/main/resources/es8</directory>
|
||||
<outputDirectory>/conf/es8</outputDirectory>
|
||||
<includes>
|
||||
<include>**/*</include>
|
||||
</includes>
|
||||
</fileSet>
|
||||
<fileSet>
|
||||
<directory>../hbase/src/main/resources/hbase</directory>
|
||||
<outputDirectory>/conf/hbase</outputDirectory>
|
||||
|
||||
@@ -44,6 +44,13 @@
|
||||
<include>**/*</include>
|
||||
</includes>
|
||||
</fileSet>
|
||||
<fileSet>
|
||||
<directory>../es8x/src/main/resources/es8</directory>
|
||||
<outputDirectory>/conf/es8</outputDirectory>
|
||||
<includes>
|
||||
<include>**/*</include>
|
||||
</includes>
|
||||
</fileSet>
|
||||
<fileSet>
|
||||
<directory>../hbase/src/main/resources/hbase</directory>
|
||||
<outputDirectory>/conf/hbase</outputDirectory>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
args=$@
|
||||
|
||||
case $(uname) in
|
||||
Linux)
|
||||
bin_abs_path=$(readlink -f $(dirname $0))
|
||||
@@ -9,5 +11,5 @@ Linux)
|
||||
;;
|
||||
esac
|
||||
|
||||
sh "$bin_abs_path"/stop.sh
|
||||
sh "$bin_abs_path"/startup.sh
|
||||
sh "$bin_abs_path"/stop.sh $args
|
||||
sh "$bin_abs_path"/startup.sh $args
|
||||
|
||||
@@ -59,7 +59,7 @@ esac
|
||||
JavaVersion=`$JAVA -version 2>&1 |awk 'NR==1{ gsub(/"/,""); print $3 }' | awk -F '.' '{print $1}'`
|
||||
str=`file -L $JAVA | grep 64-bit`
|
||||
|
||||
JAVA_OPTS="$JAVA_OPTS -Xss256k -XX:+AggressiveOpts -XX:-UseBiasedLocking -XX:-OmitStackTraceInFastThrow -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=$base/logs"
|
||||
JAVA_OPTS="$JAVA_OPTS -Xss1m -XX:+AggressiveOpts -XX:-UseBiasedLocking -XX:-OmitStackTraceInFastThrow -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=$base/logs"
|
||||
if [ $JavaVersion -ge 11 ] ; then
|
||||
#JAVA_OPTS="$JAVA_OPTS -Xlog:gc*:$base_log/gc.log:time "
|
||||
JAVA_OPTS="$JAVA_OPTS"
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.alibaba.otter.canal.adapter.launcher;
|
||||
import org.springframework.boot.Banner;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
|
||||
|
||||
/**
|
||||
* 启动入口
|
||||
@@ -10,7 +11,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
* @author rewerma @ 2018-10-20
|
||||
* @version 1.0.0
|
||||
*/
|
||||
@SpringBootApplication
|
||||
@SpringBootApplication(exclude= { DataSourceAutoConfiguration.class})
|
||||
public class CanalAdapterApplication {
|
||||
public static void main(String[] args) {
|
||||
// 支持rocketmq client 配置日志路径
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user