数据同步过程中的数据转换——DataX Transformer实践
前言
最近遇到一个需求,需要从一个表里读取数据,并解密其中一个字段,然后写入另一个表中,表的数据量大概是一千多万,听上去是一个很简单的需求。我手上正好有一个日常在用的Springboot + MyBatis-Plus的框架,我想着直接用MyBatis-Plus3.5.4开始支持的一个流式查询来完成这一功能,但是实践起来发现数据同步的效率十分低。后来想到可不可以用DataX来做数据同步,因为DataX作为一个数据同步工具,效率十分高,去看了DataX的官网,发现它支持数据转换功能,它提供了Transformer来帮助我们实现一些数据传输过程中定制化的需求。
原理
Transformer在DataX的架构中介于Reader和Writer之间,每一条数据记录(Record)从Reader进入Transformer过程后,根据配置文件中指定的字段,对字段进行一些逻辑处理,并将处理后的数据更新到Record对应字段中,然后输出到Writer。我们可以自定义那部分处理逻辑,以实现我们的定制化需求。
实践
第一步:拉取DataX源码、下载DataX
- 进入到阿里DataX的官网
- 通过git拉取DataX源码
- 下载官方已经给我们提供的DataX(文档往下滑,有个DataX下载地址)
第二步:源码解读
- 一步步的解读源码可以帮助我们快速分析出他的代码结构,并帮助我们找到 我们应该在哪里开发我们自己的定制化代码
- 在拉下来的代码中找到transformer模块,我们可以看到里面只有两个抽象类,一个是复杂型Transformer,一个是普通的Transformer
- 我们点到普通的Transformer类中,发现这是一个抽象类,其中evaluate方法的注释告诉我们这个方法就是核心的处理逻辑,于是我们猜测想要实现自定义的transformer得继承这个类并实现里面的evaluate方法。双击类名,
Ctrl + H
打开该类的血缘结构,可以看到下面有很多的实现类,是官方已经写好的可以直接提供给我们使用的transformer,我们随便点一个双击进去。
- 可以看到这个FilterTransformer实现类继承了Transformer父类,同时实现了evaluate方法,并在里面写了具体的处理逻辑,我们之前的猜想也得到了应验。现在我们需要定位FilterTransformer这个类文件的位置,点击左侧目录上面的定位按钮,找到这个文件在core模块下的其中一个叫transformer文件夹中,这里面还有其他的Transformer实现类,我们后面要自定义的逻辑代码也将写在这一部分。
第三步:自定义Transformer实现类
- 这里我的业务逻辑是需要实现一个加密解密,我需要用到hutool中的SecureUtil工具类进行我的加密解密操作,因此在core模块的pom文件中添加了hutool的依赖
- 同时找到util文件夹下,创建我们自己的工具类,将加密解密的逻辑提取到一个工具类中,与业务解耦
package com.alibaba.datax.core.util;
import cn.hutool.crypto.SecureUtil;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
public class AesUtil {
public final static byte[] KEY = "123456".getBytes(StandardCharsets.UTF_8);
//加密
public static String encrypt(String value) {
if (Objects.isNull(value)){
return null;
}
return SecureUtil.aes(KEY).encryptHex(value);
}
//解密
public static String decrypt(String value) {
if (Objects.isNull(value)){
return null;
}
return SecureUtil.aes(KEY).decryptStr(value);
}
}
- 在transformer文件夹下创建我们的实现类并继承Transformer,在构造函数中指定我们的transformer函数名字,在evaluate中实现具体逻辑
package com.alibaba.datax.core.transport.transformer;
import com.alibaba.datax.common.element.Column;
import com.alibaba.datax.common.element.Record;
import com.alibaba.datax.common.element.StringColumn;
import com.alibaba.datax.common.exception.DataXException;
import com.alibaba.datax.core.util.AesUtil;
import com.alibaba.datax.transformer.Transformer;
import java.util.Arrays;
import java.util.Objects;
public class AesTransformer extends Transformer {
final static String AES_TYPE_ENCODE = "encode";
final static String AES_TYPE_DECODE = "decode";
public AesTransformer() {
//定义该Transformer的函数名字,在后面配置文件中通过该名字来让datax知道我们需要使用哪个transformer
setTransformerName("dx_aes");
}
//record参数表示一条数据记录
//paras数组表示传入的参数列表,paras数组至少为一个,表示要操作的字段的索引(字段索引从0开始);其它的数组元素可以自己规定
//在这里我们希望paras有两个参数,第一个表示要加密解密的字段的索引值,第二个表示要进行哪种操作,加密还是解密
@Override
public Record evaluate(Record record, Object... paras) {
int columnIndex;
String aesType;
//校验传入的参数列表,并接收参数
try {
if (paras.length != 2) {
throw new RuntimeException("dx_aes 的参数个数必须为2");
}
columnIndex = (Integer) paras[0];
aesType = (String) paras[1];
if (!Objects.equals(aesType, AES_TYPE_ENCODE) && !Objects.equals(aesType, AES_TYPE_DECODE)){
throw new RuntimeException("dx_aes 的第二个参数必须为encode或decode");
}
} catch (Exception e) {
throw DataXException.asDataXException(TransformerErrorCode.TRANSFORMER_ILLEGAL_PARAMETER, "paras:" + Arrays.asList(paras).toString() + " => " + e.getMessage());
}
//处理数据
//1.获取到要操作的字段
Column column = record.getColumn(columnIndex);
try {
//2.获取到字段值
String oriValue = column.asString();
if (oriValue == null) {
return record;
}
//3.判断操作类型,并执行加密或者解密,然后把字段的新值重新设置回record中
if(column.getType() == Column.Type.STRING && aesType.equals(AES_TYPE_DECODE)) {
String newValue = AesUtil.decrypt(oriValue);
record.setColumn(columnIndex, new StringColumn(newValue));
} else if (column.getType() == Column.Type.STRING && aesType.equals(AES_TYPE_ENCODE)) {
String newValue = AesUtil.encrypt(oriValue);
record.setColumn(columnIndex, new StringColumn(newValue));
}
} catch (Exception e) {
throw DataXException.asDataXException(TransformerErrorCode.TRANSFORMER_RUN_EXCEPTION, e.getMessage(), e);
}
//4.返回record
return record;
}
}
- 注册我们编写好的的transformer类,找到TransformerRegistry类,并在静态代码块中模仿他的代码加一行我们自己的就好了
第四步:编译打包
- 在这一步中可能会遇到些问题,我也会把我遇到的问题及解决思路写出来给大家参考。
- 由于我们在整个过程中只改动了core模块的代码,因此我们在打包的时候只需要打包公共模块,别的reader和writer组件不需要打包。我们找到整个项目的pom文件,在其中的modules标签中,只保留图中这三个module,这三个是公共模块,必须要编译打包的,别的模块由于我们没有改动,因此不需要编译打包,全部注释掉即可,可以节约很多时间。
- 重新加载maven项目后,找到maven管理界面,在datax-all下的Lifecycle中,先执行clean,执行完后执行package
- 这里我遇到了一个问题,我们找到对应出错的位置,也就是transformer模块下,有个assembly目录,打开里面的xml文件,我们在他的id标签中随便加个文本就可以解决问题
- 再重新执行clean和package,发现又是一样的错误,只不过这次出现在了core模块,和上步一样的解决思路,我们找到core模块的assembly目录,打开里面的xml文件,我们在他的id标签中随便加个文本
- 等出现这个就代表打包成功了,我们找到core模块下target目录,这个jar包就是我们要用的(为什么别的两个不需要,因为我们只改了core的代码,所以只需要这个)
第五步:替换jar包
- 打开我们之前下载的DataX,解压,找到里面的lib目录
- 把我们准备的core的jar包直接拖进这个里面,这时候会跳出一个同名文件是否替换,选择是,替换掉它原来的jar包就好了
第六步:编写配置文件运行DataX
- 打开job目录,一般配置文件都写在job中
- 配置文件编写示例,在这里我们对
name
字段需要进行解密,在transformer配置中,name指定我们定义的函数名,表示要使用哪个transformer,parameter输入我们的参数
{
"job": {
"setting": {
"speed": {
"channel":1
},
"errorLimit": {
"record": 0,
"percentage": 0.02
}
},
"content": [
{
"transformer": [
{
"name": "dx_aes",
"parameter":
{
"columnIndex":1,
"paras":["decode"]
}
}
],
"reader": {
"name": "mysqlreader",
"parameter": {
"username": "***",
"password": "*******",
"column" : [
"id",
"name"
],
"connection": [
{
"jdbcUrl": ["jdbc:mysql://localhost:3306/test"],
"table": ["test1"]
}
]
}
},
"writer": {
"name": "mysqlwriter",
"parameter": {
"username": "***",
"password": "********",
"writeMode": "insert",
"column" : [
"id",
"name"
],
"connection": [
{
"jdbcUrl": "jdbc:mysql://localhost:3306/test",
"table": ["test2"]
}
]
}
}
}
]
}
}
- 打开cmd控制台,在datax目录下直接输入cmd,回车
- 根据自己的配置文件名,修改下命令即可
python .\bin\datax.py .\job\job.json