数据增量抽取项目重演

前言

在之前的项目开发中,就有使用过增量抽取的经历;但因为并没有总结。

在实习过程中,参加的项目其中之一,就是数据增量抽取;虽然只是两个人开发;在开发时,并没有多大的参与感,而且比较当前的项目建设,觉得可以完成得更好;所以对整个项目进行重演,模拟真实的双方接口数据增量抽取。当然,会有以下几点工作内容:

  1. 所有的数据都是素材数据,这可以保证不违反公司的保密协议;使用的技术栈和处理方式也有所不同。
  2. 项目的工作量主要在于接口方,这也是参与感没那么强烈的原因之一。
  3. 接口方实现批处理

所以整个项目将会分为2部分,以接口方接口调用方进行重演;

参考链接

https://developer.aliyun.com/article/464065

时间线

2021-01-08~2021-01-15—完成初稿

项目介绍

项目场景

一个系统A运行了一段时间,产生了大量的数据。在某个时间,另一个系统B在开发中,需要使用到系统A的某些数据。

所以联系系统A的开发人员,进行沟通后,确定某个时间段可以开放某个接口,用于接口增量抽取操作。

项目需求

维护系统A的开发者为接口方,维护系统B的开发者为接口调用方;

接口方根据接口调用方的数据需求进行接口设计,有以下工作:

  • 接口的设计(开放时间段,接口参数,获取数据逻辑)
  • 接口数据的加密和解密

接口调用方调用接口方提供的接口,有以下工作

  • 编写定时任务,调用接口
  • 对接口返回数据进行解密
  • 持久化到数据库中

项目回顾

问题

增量抽取项目并不难,难的是双方的沟通,以及接口方的不规范的数据设计。

接口增量抽取有很多种方式进行处理,使用时间作为依据可以说是主流且简单的方式之一。

但是接口方对于时间的处理上存在很大的问题,以至于徒增麻烦。

例如在对时间的处理上,

  1. 问题:接口返回的数据关于时间的描述,有些是时间戳,有些是格式化时间字符串。

这个问题需要进行复杂操作(根据接口中时间字段的数据,进行不同的处理方式)才能够解决这个问题。

在接口调用方进行数据持久化时,需要进行相关映射,时间字符串可以插入数据库,而时间戳的数据只能转换后才可以插入数据库中。

  1. 问题:存储记录中对于时间的精确度仅仅只是精确到day

yyyy-MM-dd HH:mm:ss精确到天,如果使用一天作为根据进行增量抽取。那么当一天的数据量过大时,接口返回的数据将会过多。

而可行的解决方案是根据时间转换成时间戳后,进行分页。每获取一页数据后对当前节点进行标记,并将数据和标记进行持久化。


例如有1000条数据,每一页分100条数据,第一次使用{timestamp:1xxx0000,page:0}作为参数,调用接口;

第100条数据对应的时间戳是1xxx0100 ,获取100条数据库后,再将1xxx0100持久化数据库中。

完成这100条数据的持久化后,再次使用{timestamp:1xxx0100,page:0}作为参数,调用接口。


这是由于精确度的问题而导致无法实施。

  1. 问题:没有统一的时间标记字段,例如createtime,updatetime,尤其是updatetime式的字段,没有记录跟踪数据是否更改

如果说以上2个问题可以通过复杂操作或者牺牲性能为代价来解决问题。这个问题是致命的。

在没有字段记录数据是否进行更改的情况下,当数据更改时,是无法跟踪的,而接口增量的数据只能是新增的数据。将会导致数据不一致的问题。

如果存在updatetime等字段时,接口只要根据该字段获取数据返回给调用方即可。

当然,在不对原数据库进行修改的条件下,也存在解决方案,即对数据的操作进行记录,当数据增量抽取时,再根据记录进行处理。

新增操作,直接插入数据库。出现修改操作时,记录字段的主键,

其它

数据增量抽取与数据库同步的区别

数据增量抽取和数据库同步虽然区别很大,但还是存在许多共同点。

首先两者目的都是保持数据一致,数据增量抽取是从一个数据库中获取数据,存储到另一个数据库中,这个过程的基本要求就是保持数据一致。

数据库同步是数据库读写分离操作产生的名词。“读”的数据库和“写”的数据库的数据要保持一致。

管理权

两者也有很大的区别,数据库同步是一方维护的,即负责读和写的数据库的管理都在一方的开发者上。而数据增量抽取,并不是如此。

可能一位开发者开发的系统使用一个数据库正常运行,某一天,另一位开发者,觉得需要这个系统所产生的数据进行开发,经过与系统开发者沟通后,对数据进行增量抽取。

当然,如果没有经过沟通,对数据进行增量抽取,其实就是数据爬虫

因为出于安全等因素,数据增量抽取是无法通过数据库同步的解决方案完成的。

数据量

数据库同步是整个数据库的同步,即负责“写”的数据库产生了什么变化,负责“读”的数据库也需要进行相应的变化;

而数据增量抽取只是某些表的同步,甚至是某些需要的数据字段的同步。

项目架构图

在这里插入图片描述

接口方

任务

编写一个接口,返回接口调用方需要的数据,返回前进行加密,以保证接口数据的安全

步骤

使用MyBatisPlus对数据进行查询

使用的第三方库

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.0</version>
        </dependency>

相关操作

// mapper
public interface ExpressMapper extends BaseMapper<Express> {
}

// service
@Service
public class ExpressService {

    @Autowired
    private ExpressMapper expressMapper;

    public IPage<Express> selectByPageAndTimeStamp(int size,String timeStamp){
        QueryWrapper<Express> queryWrapper = new QueryWrapper<>();
        queryWrapper.ge("update_time",timeStamp);
        Page<Express> expressPage = new Page<>(0,size);
        return expressMapper.selectPage(expressPage,queryWrapper);
    }
}

对数据进行处理

使用stream对接口数据进行排序
expresses = expresses.stream()
    .sorted(Comparator.comparing(Express::getUpdateTime).reversed())
    .collect(Collectors.toList());
使用fastJson对数据进行序列化
String dataStr = JSON.toJSONString(res);
使用AESUtil对接口数据进行加密
AESUtil.encrypt(dataStr,passwd);

编写接口资料,提供给接口调用方

接口地址
{
    "url" : "http://localhost:8080/express",
    "params" :{
                  "size" : size,
                  "timestamp" : timestamp
                },
    "method" : Method.Get
}
加密解密工具
package zhj.pro.work.util;

import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.spec.SecretKeySpec;

/**
 * description AES加密解密工具
 * author zhj1121
 * createTime 2021/1/10 20:00
 **/
public class AESUtil {

    // 默认编码
    private static final String CHARSET = "utf-8";
    // AES算法
    private static final String ALGORITHM = "AES";
    // 密钥长度
    private static final Integer KEY_LENGTH = 128;

    /**
     * description 
     * param [password]
     * return javax.crypto.spec.SecretKeySpec
     **/
    private static SecretKeySpec generateKey(String password) throws NoSuchAlgorithmException {
        // 创建AES的KEY生产者
        KeyGenerator keyGenerator = KeyGenerator.getInstance(ALGORITHM);
        // 利用密码参数作为随机数随机生成
        keyGenerator.init(KEY_LENGTH,new SecureRandom(password.getBytes()));
        // 返回byte类型的密钥
        byte[] byteRes = keyGenerator.generateKey().getEncoded();
        // 返回AES专用密钥
        return new SecretKeySpec(byteRes,ALGORITHM);
    }

    /**
     * description 二进制转换成十六进制
     * param [buf]
     * return java.lang.String
     **/
    private static String parseByte2HexStr(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (byte aByte : bytes) {
            String hex = Integer.toHexString(aByte & 0xFF);
            if (hex.length() == 1) {
                hex = '0' + hex;
            }
            sb.append(hex.toUpperCase());
        }
        return sb.toString();
    }

    /**
     * description 十六进制字符串转换成二进制byte
     * param [hexStr]
     * return byte[]
     **/
    private static byte[] parseHexStr2Byte(String hexStr) {
        if (hexStr.length() < 1)
            return null;
        byte[] result = new byte[hexStr.length()/2];
        for (int i = 0;i< hexStr.length()/2; i++) {
            int high = Integer.parseInt(hexStr.substring(i*2, i*2+1), 16);
            int low = Integer.parseInt(hexStr.substring(i*2+1, i*2+2), 16);
            result[i] = (byte) (high * 16 + low);
        }
        return result;
    }

    /**
     * description AES加密方法
     * param [content, password]
     * return java.lang.String 密文
     **/
    public static String encrypt(String content, String password) {
        try {
            SecretKeySpec key = generateKey(password);
            // 创建密码器
            Cipher cipher = Cipher.getInstance(ALGORITHM);
            byte[] byteContent = content.getBytes(CHARSET);
            // 初始化为加密模式的密码器
            cipher.init(Cipher.ENCRYPT_MODE, key);
            // 加密
            byte[] result = cipher.doFinal(byteContent);
            // 二进制转换成16进制字符串
            return parseByte2HexStr(result);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }



    /**
     * description AES解密方法
     * param [content, password]
     * return java.lang.String 明文
     **/
    public static String decrypt(String content, String password) {
        try {
            // 十六进制字符串转换成二进制字节数组
            byte[] byteArr = parseHexStr2Byte(content);
            SecretKeySpec key = generateKey(password);
            Cipher cipher = Cipher.getInstance(ALGORITHM);
            cipher.init(Cipher.DECRYPT_MODE, key);
            // 解密
            byte[] result = cipher.doFinal(byteArr != null ? byteArr : new byte[0]);
            return new String(result,CHARSET);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
    
//    public static void main(String[] args) throws Exception {
//        String content = "测试一下";
//        String encrypt = encrypt(content, KEY);
//        System.out.println(encrypt);
//        String decrypt = decrypt(encrypt, KEY);
//        System.out.println(decrypt);
//    }
}
加密解密密码
passwd1121

补充

当然,在安全方面,还可以增加header等验证,由于这里已经保证了接口安全,所以出于工作量考虑,不进行过多验证

接口调用方

任务

根据接口方提供的资料,对接口方提供的接口进行先全量,后增量调用,进行持久化。

并编写定时任务,在约定的时间进行调用

步骤

调用接口

使用Httpclient对接口进行调用

使用的第三方库

        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
            <version>4.5.9</version>
        </dependency>

相关调用工具代码

使用之前封装好的工具方法

   public static String doGetByUrl(String url){
        CloseableHttpClient closeableHttpClient = HttpClientBuilder.create().build();

        //get or post block
        HttpGet httpGet = new HttpGet(url);
        //HttpPost httpPost = new HttpPost(url);
        httpGet.setHeader("Content-Type", "application/json;charset=utf8");
        CloseableHttpResponse closeableHttpResponse = null;
        try {
            closeableHttpResponse = closeableHttpClient.execute(httpGet);
            if (closeableHttpResponse.getStatusLine().getStatusCode()==200){
//                System.out.println("响应成功,进行相关处理");
                closeableHttpResponse.getEntity().getContent();
                return EntityUtils.toString(
                        closeableHttpResponse.getEntity(),"utf-8");//AESUtil.decrypt(res,passwd);
            } else{
//                System.out.println("响应失败,进行相关处理");
                return null;
            }
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        } finally {
            try {
                if(closeableHttpClient != null) {
                    closeableHttpClient.close();
//                    System.out.println("关闭closeableHttpClient");
                }
                if(closeableHttpResponse != null) {
                    closeableHttpResponse.close();
//                    System.out.println("关闭closeableHttpResponse");
                }
            } catch (IOException e) {
                e.printStackTrace();
//                System.out.println("IO出现异常");
            }
        }
    }
参数

size

记录条数,可以对请求数据量进行限定

由于考虑到接口方的请求资源问题,所以每次请求返回一定量的数据

updatetime

数据更新时间,当数据的值发生改变时,updatetime也需要进行相应的更新

可以保证前天的数据,如果在今天发生更新时,也可以对数据库进行更新,避免数据不一致性

使用AESUtil对接口数据进行解密

与接口方协调,接口方使用AESUtil对接口数据进行加密;接口调用方则使用AESUtil工具对接口返回的加密数据进行解密

详细见接口方模块笔记

使用fastjson对接口数据进行反序列化

这里使用的是阿里的fastjson

JSONObject jsonObject = JSON.parseObject(decrStr);\\decrStr是解密后的接口数据
String dataStr = jsonObject.get("data").toString();
List<Express> expresses = JSON.parseArray(dataStr,Express.class);
持久化
设计数据库
// 接口数据表
id	int	主键
title	varchar	标题
type	varchar	类型(0-5)
pickupaddr	varchar	取件地址
consigneeaddr	varchar	收件地址
remark	varchar	备注
createtime	varchar	创建时间戳
updatetime	varchar	修改时间戳
completeflag	varchar	是否完成(0:未完成,1:完成)
deleteflag	varchar	删除标记
// 工作表
id	varchar	工作名
timestamp	varchar	时间戳
字段映射

由于请求接口的数据,如下格式:

{
    xxXxx : ""
}

即驼峰命名法,而mysql数据库是大小写不区分;

在工作中的使用的是jdbcTemplate,所以必须进行字段映射,有两种方式

  • 对接口数据的字段进行处理,获取与数据库字段一致的字段

  • 对接口数据的字段与数据库字段的关系配置在数据库中,持久化时,查询数据库即可

在这次总结中,由于使用的是MybatisPlus,所以只要在实体类,如下添加注解即可完成映射:

// 数据库
pickupaddr
// 实体类
@TableField("pickupaddr")
private String pickupAddr;
使用MyBatisPlus进行持久化工作

由于主要编码太多,所以对整个逻辑进行描述:

  • 获取工作表的工作时间节点,使用该时间戳作为参数请求接口
  • 对接口数据进行批量插入或者更新
  • 获取接口数据的最新的一条的时间戳,并更新到工作表中,作为标记,用于下次查询
  • 当请求的数据少于批量数时,停止请求;否则,重复以上步骤
使用定时任务Scheduled

与接口方进行协调,接口方定时开放接口;接口调用方定时请求,进行增量抽取

    @Scheduled(cron="0 0 4 * * ?")//每日凌晨4点进行请求
    public void express(){
        expressJob.start(size,passwd);//作业
    }

总结

主要技术栈

javafacker制造测试数据

制造测试数据。在之前的系统测试时,往往会手动加入测试数据。但当需要的数据量过大时,就非常麻烦;

使用facker+for就可以非常简单地制造数据。

AES加密解密

AES加密解密的使用,是为了保证接口数据的安全。当截获接口返回的数据,例如抓包等方式,得到的数据往往不是明文,而是密文;

当然,AES还可以对接口参数进行加密,当接口方获取接口参数与约定的参数相悖时,直接返回错误即可,而不会造成资源浪费;

在这一次项目中,也只是停留在用的层面;所以尝试了网上各种实现方式,最终选取了这一款,并进行改造,因为,这个工具类在以后也可以使用的。

之前,有过自己实现一款简单的加密解密算法的想法,但最终还是无法跳脱安全的顾虑。

MyBatisPlus批量更新插入

使用MyBatisPlus是因为想要尝试一下新的数据持久化工具。之前使用的是spring jpa,spring data jpa,hibernate和mybatis。在实际工作中的数据增量抽取项目中,使用的是jdbcTemplate。而使用MyBatisPlus,是因为想要尝试一下新的数据持久化工具。

关于MyBatisPlus的使用感受,感觉MyBatisPlus是MyBatis和Hibernate之间的结合体。在一篇博客后面,有个人说,“MyBatisPlus是要走hibernate的老路了”。的确,MyBatisPlus也走的是面向对象的思维。


其实,从学习数据持久化工具的历程上,就可以知道学习编程的成长历程。

首先是,jdbc,每一次使用,处于资源考虑,都需要连接,请求,释放连接等一系列操作。

当时,为了不重复造轮子,根据平常的使用习惯,封装了一个工具类,类似于jdbctemplate。当然,并没有做到jdbctemplate那样妥善处理。在之后,有很长的一段时间一直使用

在后来,学习到了Jpa,Hibernate等等,当时因为初学,所以对面向对象的持久化非常感兴趣,但也因为初学原因,在一些处理方式上,并不熟悉,导致只是学学而已;

因为当时对于SQL熟悉,所以相比之下,JdbcTemplate是在面向对象持久化的阶段的实战替代品。

然后,最后接触到了MyBatis,MyBatisPlus等等。

在各种博客中,对各种持久化工具进行各种比较;但才发现,原来工具只是一种工具而已,更重要的是“思想”。

HttpClient

其实有想过使用RestTemplate等等其它方式实现,但由于时间关系,在之前的笔记中,完成了对HttpClient的封装。所以这里使用了HttpClient;


项目总结

完成这一次数据增量抽取的重演与总结,花费的时间并不长,一方面是因为有之前的经验,有思路;另一方面,是因为接口方和接口调用方的工作量都在自己手里。但间隔时间比较久。最初想要完整的完成,但最后因为各种事情断断续续,所以有很多细节没来得及处理,就草草完工。

所以,一个项目切忌断断续续,要规划好日程


当然,在技术选型上,也要慎重考虑

例如,在工作中的增量抽取,当时使用的JdbcTemplate,在对象的处理上,不是构造对象,而是使用Object代替,大量的代码都是相关处理。当然,在编码方面,非常方便,但在维护上,需要很多的时间阅读代码;

又是使用数据库存储配置信息,完成相关映射;浪费资源不说,项目工作量也大;


另外,这一次项目总结的目的并不只是重演;重演只是验证想法的一个过程;

一个好的处理方式,好的想法往往是最重要的

例如以什么作为增量标记,在工作项目中,使用的是一天作为增量标记,即在今天获取昨天的所有记录进行。

这样做,主要是因为接口方的数据不规范,时间字段的精确度问题;

而在这里,使用数据的更新的时间戳作为增量标记,不仅可以控制接口的数据返回量,减少接口的调用压力压力;还可以保证数据的一致性。当数据发生更新时,数据将会因为更新时间戳而被返回。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值