数据迁移——将数据由SQLServer迁移到PostgreSQL中

开发背景

需求背景

  由于项目重构需要,现将数据由SQLServer迁移到PostgreSQL中,由于原项目是单工程项目(也就是说工程表中永远只有一条数据),现将多个工程项目迁移合并到一个数据库中,这样工程表中就会出现多个项目(此时id必然会冲突)。

分析解决

解决数据库不同源问题

  由于此时的数据迁移不仅仅是由一个数据库迁移到另一个相同数据库类型的另一个数据库,而是由SQLServer迁移到PostgreSQL中(数据库不同源)。此时我们首先面临的是数据库不同所带来的表结构语句差异性问题。

  而为了解决该问题,我才去如下的解决方案:

  • 采用JPA,通过使用Hibernate的JPA屏蔽数据库底层的差异性,以此来解决两者数据库不同所带来的数据操作问题。
  • 采用读写分离的实现方案,将该实现拆分成两个相互独立的微服务模块,分别是spring-sqlserverspring-postgresql。前者只用于数据的读取(从SQLServer中),后者只用于数据的写入(到PostgreSQL中)。

  备注,这里的spring-sqlserver指的是前一篇文章读取数据表中的文件字节流,因而与spring-sqlserver模块相关的源码直接在前一篇文章中,这里就不再列出。

解决数据量过大问题

  由于需要导入的数据量很大,约有30G,所以我这里采用了与之前相同的分页查询策略,这里我们可以通过配置文件手动设置每次分页查询的信息条数,这里我设置成了100条,也就是说,每次从spring-sqlserver中读取100条信息,通过JPA批量插入到spring-postgresql中,直至将表中所有的信息全部遍历完毕。

解决写入比读取慢的问题

  为了解决写入比读取慢的问题,我这里采用的是FixedThreadPool线程池,通过手动配置线程(测试中使用了10条线程),不过我还是编写了不使用线程池的方法。

  换言之,我这里一共有两套方案,一套是使用线程池,另一套是不使用线程池。

  这里需要注意的是,由于FixedThreadPool的固定缺陷,也就是说假设我将其活跃线程数量设置成2,则当前活跃的线程数量确实是2,但是其线程总数并不为2,而是占满整个线程池。换言之,多余的线程会在线程池内排队,直到等到其执行为止,这里存在一个很大的问题,那就是Feign的调用超时问题,也就是说,当spring-postgresql请求发生后,前面的信息请求能够正常进行,但是后面的请求则会出现连接超时(因为请求在FixedThreadPool线程池中不断等待,直到超时),这会导致后续所有的请求全部都出现请求失败的情况。因而我们需要将Hystrix、ribbon等尽可能关闭,以防信息请求不完整。这里为了安全起见,还是推荐使用非线程池的方式。

解决主键冲突问题

  为了解决主键冲突问题,我这里采用了如下做法:

  • 生成新的主键

  将原有的主键id更改为id_v,并生成新的主键id_k

  • 添加时间戳

  由于数据库的导入是一个一个进行的,因而我们在所有的表中都添加时间字段,记录当前该条信息被导入的时间戳。这样一旦数据插入失败,方便我们定位出错位置。

  • 添加projectId字段

  每个项目在添加时,都在每一张表中添加一个projectId,该projectId也是一个时间戳(Long类型),与之前时间戳不同之处在于,之前的时间戳记录的是每条信息导入的时间,也就是说,单独一个项目数据的导入,每条录入信息的时间戳可能是不同的。而projectId记录的是该项目开始导入的时间戳,一个项目数据库确定,该时间戳在每一张表的每一条数据中都是相同的,以此来区分不同的项目数据库版本。

源码

核心依赖

<dependencies>
    <!-- eureka注册中心 -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-eureka</artifactId>
    </dependency>
    <!-- ribbon负载均衡 -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-ribbon</artifactId>
    </dependency>
    <!-- hystrix熔断机制 -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-hystrix</artifactId>
    </dependency>
    <!-- jpa持久化 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <!-- postgresql驱动 -->
    <dependency>
        <groupId>org.postgresql</groupId>
        <artifactId>postgresql</artifactId>
    </dependency>
    <!-- mybatis -->
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>1.3.1</version>
    </dependency>
    <!-- feign -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-feign</artifactId>
    </dependency>
    <!-- okhttp -->
    <dependency>
        <groupId>com.squareup.okhttp3</groupId>
        <artifactId>okhttp</artifactId>
    </dependency>
</dependencies>

Java源码

工具类

  TimeFormatUtil

package com.lyc.postgresql.util;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;

/**
 * @author: zhangzhenyi
 * @date: 2019/4/11 16:52
 * @description: 格式化时间字符串
 **/
public class TimeFormatUtil {
   

    // 开始时间
    private long startTime = 0;
    // 截止时间
    private long endTime = 0;

    /**
     * 创建TimeFormatUtil实例
     * @return
     */
    public static TimeFormatUtil newTimeFormatUtil(){
   
        return new TimeFormatUtil();
    }

    /**
     * 设置开始时间
     * @return
     */
    public void setStartTime(){
   
        this.startTime = new Date().getTime();
    }

    /**
     * 设置截止时间
     * @return
     */
    public void setEndTime(){
   
        this.endTime = new Date().getTime();
    }

    /**
     * 计算程序运行总耗时
     * @return
     */
    public String getSpendTime(){
   
        // 获取耗时时间
        long spendTime = this.endTime - this.startTime;
        // 将耗时时间转换成时间字符串
        return timeToString(spendTime);
    }

    /**
     * 将long类型的时间转换成时间字符串
     * @param spendTime 消耗时间
     * @return
     */
    private String timeToString(long spendTime) {
   
        // 毫秒
        long millis = spendTime % 1000;
        // 将用时全部转换成秒
        long secondTemp = spendTime / 1000;
        // 时
        long hour = secondTemp / 3600;
        // 剩余的秒
        secondTemp = secondTemp % 3600;
        // 分
        long minutes = secondTemp / 60;
        // 秒
        long second = secondTemp % 60;
        // 返回计算的最终结果
        return "本次执行耗时:" + hour + "小时" + minutes + "分钟" + second + "秒" + millis + "毫秒";
    }

    /**
     * 将时间字符串格式化成时间对象
     * @param dateString 时间字符串
     * @return
     */
    public String toDate(String dateString) throws ParseException {
   
        Locale localeUS = new Locale("en","US");
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("EEE MMM dd HH:mm:ss Z yyyy",localeUS);
        Date date = simpleDateFormat.parse(dateString);
        //2019-03-19 07:14:04
        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date);
    }

}

  FieldFormatUtil

package com.lyc.postgresql.util;

import com.google.common.collect.Maps;
import lombok.var;

import java.util.Date;
import java.util.Map;

/**
 * @author: zhangzhenyi
 * @date: 2019/3/29 11:22
 * @description: Map<String,Object> 类型的数据格式化成对应的格式并返回
 **/
public class FieldFormatUtil {
   

    private final Map<String,Object> map;
    private final DataConvertUtil dataConvertUtil;


    /**
     * FieldFormatUtil 构造函数
     * @param map
     */
    public FieldFormatUtil(Map<String,Object> map) {
   
        if(null != map) {
      // 如果不为空,则获取数据
            this.map = map;
        } else {
     // 否则设置一个默认的空对象
            this.map = Maps.newHashMap();
        }
        dataConvertUtil = DataConvertUtil.newDataConvertUtil();
    }

    /**
     * 创建一个FieldFormatUtil 对象
     * @param map
     * @return
     */
    public static FieldFormatUtil newFieldFormatUtil(Map<String,Object> map){
   
        return new FieldFormatUtil(map);
    }

    /**
     * 获取整型数据
     * @param k
     * @return
     */
    public int getInt(String k){
   
        var obj = this.map.get(k);
        return dataConvertUtil.getInt(obj);
    }

    /**
     * 获取Long型数据
     * @param k
     * @return
     */
    public Long getLong(String k) {
   
        var obj = this.map.get(k);
        return dataConvertUtil.getLong(obj);
    }

    /**
     * 获取String型数据
     * @param k
     * @return
     */
    public String getString(String k) {
   
        var obj = this.map.get(k);
        return dataConvertUtil.getString(obj);
    }

    /**
     * 获取Double类型的数据
     * @param k
     * @return
     */
    public Double getDouble(String k) {
   
        var obj = this.map.get(k);
        return dataConvertUtil.getDouble(obj);
    }

    /**
     * 获取时间类型的数据
     * @param k
     * @return
     */
    public Date getDate(String k) {
   
        var obj = this.map.get(k);
        return dataConvertUtil.getDate(obj);
    }

    /**
     * 获取double类型的数据
     * @param k
     * @return
     */
    public float getFloat(String k) {
   
        var obj = this.map.get(k);
        return dataConvertUtil.getFloat(obj);
    }

}

  DataConvertUtil

package com.lyc.postgresql.util;

import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.joda.time.DateTime;

import java.util.Date;

/**
 * @author: zhangzhenyi
 * @date: 2019/3/29 11:37
 * @description: 数据类型转换工具
 **/
@Slf4j
@NoArgsConstructor
public class DataConvertUtil {
   

    /**
     * 创建一个DataConvertUtil对象
     * @return
     */
    public static DataConvertUtil newDataConvertUtil(){
   
        return new DataConvertUtil();
    }

    /**
     * 将对象转换成字符串
     * @param o
     * @return
     */
    public String getString(Object o){
   
        String str = String.valueOf(o);
        if("null".equals(str) ){
   
            return null;
        }
        return String.valueOf(o);
    }

    /**
     * 将对象转换成字符串
     * @param o
     * @return
     */
    public Long getLong
  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值