MySQL大表歷史數據遷移這麼做

歷史數據遷移

  • 項目地址:https://gitee.com/xl-echo/dataMigration

歷史遷移解決方案。微服務的架構爲基礎,使用多種設計模式,如:單利、橋接、工廠、模板、策略等。其中涉及的核心技術有,多線程、過濾器等。致力於解決mysql大表遷移的問題。提供多種遷移模式,如:庫到庫、庫到文件再到庫等!
Historical migration solution. Based on the architecture of microservices, multiple design patterns are used, such as simple interest, bridge, factory, template, strategy, etc. The core technologies involved include multithreading, filters, etc. It is committed to solving the problem of MySQL large table migration. Provide multiple migration modes, such as library to library, library to file, and then to library

開發環境

工具 版本 描述
IDEA 2019.2
JDK 1.8
MySQL 5.7 5.7 +

技術框架

技術 描述 用途 備註
SpringBoot 基礎框架 構建系統架構
MyBatis ORM框架 構建數據層
JDBC 構建外部數據源 用於配置遷移
Maven 項目管理 項目管理
ExecutorService 線程池 分批執行任務

需求分析和結構整理

歷史數據遷移,顧名思義就是將某個地方的歷史數據轉移到另外一個地方。需求明瞭,就一句話能夠概括完整。其中要涉及的技術,其實也不是很難。無非是設計一個複製數據的流程,設計一個插入數據的流程和一個刪除數據的流程。有三個流程去完成這件事情,節本就能搞定。當然,我們不能僅僅考慮把流程設計的能執行就好,還需要保障流程的健全性。雖然三個流程,就有了可行性,並且在實際應用中基本這三個流程是不可缺少的。但是我們必須保證數據安全,流程安全,並且可以直接,拓展,這樣更有利於我們的項目的接入,使用。關鍵是解決了數據丟失,數據複製不完整的問題。
支付行業,支付行業中對遷移的應用,都是必須保障數據安全的,寧可數據遷移不成功,也必須保障數據不因爲遷移二缺少,所以以上這三個流程並不完善。
檢查流程的加入,處理以上說的三個流程,本次需求設計了另外兩個檢查流程,用來檢查查詢出來的數據和插入的數據是否完整,這樣保障了源庫數據和遷移到目標庫的數據一致
事務設計,遷移過程中可能出現的各種系統異常會導致數據複製問題,如果不加入事務的考慮,會導致數據丟失,這也是需要解決的問題。
內存管理,在遷移過成功,如果一次性加載所以需要遷移的數據到項目或者內存中,基本都會導致OOM之類的問題,這也是我們要仔細考慮到的問題。

代碼流程設計如下

數據遷移的辦法有很多種,但是其目的都是一致的,都是將數據從源位置,轉移到目標位置。項目中設計了一個通用流程來專門管理多種遷移辦法,並且用到了多種設計模式來讓代碼更加簡潔和更便於管理遷移。讓代碼靈活,並且方便二開或者多開。通用流程執行示意圖如下,也是該項目主要流程
代碼流程設計

簡潔的代碼和流程,其實得益於原本的流程設計和設計模式的運用。

遷移物理架構圖解

遷移物理架構圖解

數據庫模型設計

數據庫模型的設計,是我們整個流程的核心,如果沒有這個數據庫的模型,我們的代碼將會亂成一團。也正是數據庫先一步做出了規定,讓我們的代碼能夠更加的靈活。

表名詳解

  • transfer_data_task: 遷移任務。在該表中配置對應的遷移任務。該表僅做任務管理,並且指定遷移的執行模式
  • transfer_database_config: 數據源配置。一個遷移任務對應兩個數據源配置,一個源數據庫配置,一個目標數據庫配置,由database_direction字段控制是需要源庫還是目標,字段有兩個值source,target
  • transfer_select_config: 源庫表配置。遷移任務通過task_id關聯到源庫查詢配置,其中主要配置信息就是我們需要查詢的表,表字段有哪些,查詢條件是什麼,一次性查詢多少條。最終會在代碼中拼接成爲查詢源庫的SQL語句。
    拼接規則:```select 配置的字段 from 配置的表名 where 配置的條件 limit 配置的限制條數````,目前一個定時任務僅支持配置一個表的遷移。
  • transfer_insert_config: 目標庫表配置。遷移任務通過task_id關聯到目標庫配置,其中主要配置信息就是我們需要插入的表,需要插入的字段有哪些,查詢的條件是什麼,一次性插入多少條。最終會在代碼中拼接成爲插入目標庫的SQL語句。
    拼接規則: insert into 配置的表名 (配置的字段) values (...),(),()...配置的插入條數據控制value後面有多少個值
  • transfer_log: 日誌記錄表。在整個遷移過程中,log是不可或缺的,這裏設計了任務遷移的日誌跟蹤。
    數據庫模型設計

日誌的選型,日誌設計(鏈路追蹤)

  • 日誌框架用的是:Log4j
    這是一個由Java編寫可靠、靈活的日誌框架,是Apache旗下的一個開源項目。最開始沒有做過多的考慮,選用的原則就一個,市面上實際使用多的,並且開源的框架就行。不過日誌的輸出確實精心設計的。
    在我們很多的項目當中,一個接口的日誌,從前到後可能會有很多。在排查的過程中,我們也基本是跟着日誌從代碼最前面往代碼最後面推。這個流程在實際應用當中會略微有一些問題,特別是有新線程,或者併發、大流量的情況基本就很難能一句一句排查了。因爲你看到的上一句和下一句,並不一定是一個線程寫出來的。所以這個時候我們需要去精心設計整個日誌輸出,讓他能夠在各種環境中一目瞭然。
    TLog爲啥要去自己設計,不直接使用TLog這樣的鏈路最終日誌框架呢?這裏有一個問題,TLog鏈路追蹤會在我們的項目中爲每一次執行創建一個唯一鍵,不管傳遞多少個服務,只要都整合了TLog就能按照唯一鍵一路追蹤下去。這裏沒有使用的原因只有一個,那就是爲了方便管理,並且能夠更加快捷的訪問到日誌。我們對日誌的輸出設計不僅僅做了鏈路追蹤的流程,還講唯一鍵直接存入了庫裏面,這樣我們更加的便捷去追蹤我們的遷移任務。
    自己設計的日誌追蹤流程,確實會更加的靈便,而且由於放入了庫中與log表關聯起來,更便於我們查詢。當然他也會有缺點,對於日誌這一塊我們如果忘記這條規則,會導致我們鏈路追蹤在鏈路中斷裂
    按照鏈路追蹤的模式我們設計了日誌,其中最關鍵的環節就兩個
  • 唯一鍵的設計(雪花算法、UUID)
    雪花算法和UUID的選型,剛開始自己實現了一個雪花算法
package com.echo.one.utils.uuid;

/**
 * 雪花算法
 *
 * @author echo
 * @date 2022/11/16 11:09
 */
public class SnowflakeIdWorker {
    /**
     * 開始時間截 (2015-01-01)
     */
    private final long twepoch = 1420041600000L;
    /**
     * 機器id所佔的位數
     */
    private final long workerIdBits = 5L;
    /**
     * 數據標識id所佔的位數
     */
    private final long datacenterIdBits = 5L;
    /**
     * 支持的最大機器id,結果是31 (這個移位算法可以很快的計算出幾位二進制數所能表示的最大十進制數)
     */
    private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
    /**
     * 支持的最大數據標識id,結果是31
     */
    private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
    /**
     * 序列在id中佔的位數
     */
    private final long sequenceBits = 12L;
    /**
     * 機器ID向左移12位
     */
    private final long workerIdShift = sequenceBits;
    /**
     * 數據標識id向左移17位(12+5)
     */
    private final long datacenterIdShift = sequenceBits + workerIdBits;
    /**
     * 時間截向左移22位(5+5+12)
     */
    private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
    /**
     * 生成序列的掩碼,這裏爲4095 (0b111111111111=0xfff=4095)
     */
    private final long sequenceMask = -1L ^ (-1L << sequenceBits);
    /**
     * 工作機器ID(0~31)
     */
    private long workerId;
    /**
     * 數據中心ID(0~31)
     */
    private long datacenterId;
    /**
     * 毫秒內序列(0~4095)
     */
    private long sequence = 0L;
    /**
     * 上次生成ID的時間截
     */
    private long lastTimestamp = -1L;

    /**
     * 構造函數
     *
     * @param workerId     工作ID (0~31)
     * @param datacenterId 數據中心ID (0~31)
     */
    public SnowflakeIdWorker(long workerId, long datacenterId) {
        if (workerId > maxWorkerId || workerId < 0) {
            throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
        }
        if (datacenterId > maxDatacenterId || datacenterId < 0) {
            throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
        }
        this.workerId = workerId;
        this.datacenterId = datacenterId;
    }

    /**
     * 獲得下一個ID (該方法是線程安全的)
     *
     * @return SnowflakeId
     */
    public synchronized long nextId() {
        long timestamp = timeGen();
        // 如果當前時間小於上一次ID生成的時間戳,說明系統時鐘回退過這個時候應當拋出異常
        if (timestamp < lastTimestamp) {
            throw new RuntimeException(
                    String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
        }
        // 如果是同一時間生成的,則進行毫秒內序列
        if (lastTimestamp == timestamp) {
            sequence = (sequence + 1) & sequenceMask;
            // 毫秒內序列溢出
            if (sequence == 0) {
                //阻塞到下一個毫秒,獲得新的時間戳
                timestamp = tilNextMillis(lastTimestamp);
            }
        }
        // 時間戳改變,毫秒內序列重置
        else {
            sequence = 0L;
        }
        // 上次生成ID的時間截
        lastTimestamp = timestamp;
        // 移位並通過或運算拼到一起組成64位的ID
        return ((timestamp - twepoch) << timestampLeftShift) | (datacenterId << datacenterIdShift) | (workerId << workerIdShift) | sequence;
    }

    /**
     * 阻塞到下一個毫秒,直到獲得新的時間戳
     *
     * @param lastTimestamp 上次生成ID的時間截
     * @return 當前時間戳
     */
    protected long tilNextMillis(long lastTimestamp) {
        long timestamp = timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = timeGen();
        }
        return timestamp;
    }

    /**
     * 返回以毫秒爲單位的當前時間
     *
     * @return 當前時間(毫秒)
     */
    protected long timeGen() {
        return System.currentTimeMillis();
    }
}

這個算法其實還是很優秀的,其中的分佈式id保障唯一設計也確實能夠很有效的幫組我們獲取鏈路追蹤的唯一鍵。不過在項目中也暴露出來了兩個問題:
1、ID重複(雪花算法就是獲取唯一id的,爲啥會出現重複的呢?後面解釋)
2、性能瓶頸

在使用過程中雪花算法出現重複,導致唯一鍵報錯。
雪花算法出現重複的可能就幾種:
1)、因爲雪花算法強依賴機器時鐘,所以難以避免受到時鐘回撥的影響,有可能產生ID重複。
2)、同一臺機器同一毫秒需要生成多個Id,並且對long workerId, long datacenterId控制不完善導致
第一種情況這基本沒法避免,不過時鐘回撥很久才發生一次。所以問題還是出現在第二種看情況,主要還是對代碼的不完善導致。後面也正是因爲代碼的不完善直接改用UUID。UUID就一行代碼就能夠解決的事情,這裏還要不斷的去完善設計的代碼。

UUID確實簡潔,選型UUID看中的也是這一點,同時也還有其他因素。在整個項目中由於代碼的靈活性和整個代碼的開放性,在執行的過程中可能會出現多個線程,並行執行,並且爲了資源能夠更高效的執行,那麼對鎖的應用是要慎重考慮的。雪花算法中,使用了sync同步鎖,當我們使用多線程去執行代碼的時候,同步鎖會成爲重量級鎖,並且成爲我們項目執行的性能瓶頸。這樣是最終選擇UUID的重要因素

  • 鏈路的設計

鏈路的設計目前還是極爲簡單的手動將需要加入到整個鏈路中的日誌加上唯一鍵。
鏈路的設計比較簡單,而且整個流程鏈路中,只需要在相應流程模式中進行鏈路的完善即可,比如:在多線程的環境下,怎麼去保證當前線程是我原本的前一任務的鏈路線;執行模式中循環執行回來的時候,怎麼保證新的流程能夠用原本的唯一件

遷移模式

遷移模式總共三種,分別對應transfer_data_task中transfer_mode的0、1、2,推薦使用模式1

  • 0、庫 -> 文件 -> 庫(不推薦使用)

模式流程圖

庫 -> 文件 -> 庫
該模式,可以有效的安全保障數據在遷移過程中,不會出現數據的錯漏,文件相當於備份操作,後期可以直接追溯遷移過程。唯一的缺點就是大表遷移中,如果使用這種模式需要兩個東西,一個是具備local\temp讀寫權限的賬號,一個是足夠的磁盤。大表數據寫入磁盤很暫用空間,在測試中間,嘗試了100w數據,寫入文件完畢後,發現有10G(大表,150字段左右)

  • 1、庫 -> 庫(推薦使用)
    庫 -> 庫

  • 2、source.dump copy target.dump(有風險)
    source.dump copy target.dump

模塊設計

dataMigration
-----bin
---------startup.sh
-----doc
---------blog.sql
---------blogbak.sql
---------community.sql
-----src
---------main
-------------java
-----------------com.echo.one
---------------------common
-------------------------base
-----------------------------TransferContext.java
-----------------------------TransferDataContext.java
-----------------------------BeanUtilsConfig.java
-------------------------constant
-----------------------------DataMigrationConstant.java
-------------------------enums
-----------------------------DatabaseColumnType.java
-----------------------------DatabaseDirection.java
-----------------------------FormatPattern.java
-----------------------------StatusCode.java
-----------------------------TaskStatus.java
-------------------------exception
-----------------------------DataMigrationException.java
-------------------------factory
-----------------------------DataMigrationBeanFactory.java
-------------------------framework
-----------------------------config
---------------------------------RemoveDruidAdConfig.java
-----------------------------filter
---------------------------------WebMvcConfig.java
-----------------------------handler
---------------------------------GlobalExceptionHandler.java
-----------------------------result
---------------------------------Result.java
---------------------controller
-------------------------TransferDatabaseConfigController.java
-------------------------TransferDataTaskController.java
-------------------------TransferHealthy.java
-------------------------TransferInsertConfigController.java
-------------------------TransferLogController.java
-------------------------TransferSelectConfigController.java
---------------------dao
-------------------------TransferDatabaseConfigMapper.java
-------------------------TransferDataTaskMapper.java
-------------------------TransferInsertConfigMapper.java
-------------------------TransferLogMapper.java
-------------------------TransferSelectConfigMapper.java
---------------------DataMigrationApplication.java
---------------------job
-------------------------DataTaskThread.java
-------------------------TransferDataTaskJob.java
-------------------------TransferLogJob.java
---------------------po
-------------------------TransferDatabaseConfig.java
-------------------------TransferDataTask.java
-------------------------TransferInsertConfig.java
-------------------------TransferLog.java
-------------------------TransferSelectConfig.java
---------------------processer
-------------------------CheckInsertDataProcessModeOne.java
-------------------------CheckInsertDataProcessModeTwo.java
-------------------------CheckInsertDataProcessModeZero.java
-------------------------CheckInsertDataTransactionalProcessModeThree.java
-------------------------CheckSelectDataProcessModeOne.java
-------------------------CheckSelectDataProcessModeTwo.java
-------------------------CheckSelectDataProcessModeZero.java
-------------------------CheckSelectDataTransactionalProcessModeThree.java
-------------------------DeleteDataProcessModeOne.java
-------------------------DeleteDataProcessModeTwo.java
-------------------------DeleteDataProcessModeZero.java
-------------------------DeleteDataTransactionalProcessModeThree.java
-------------------------InsertDataProcessModeOne.java
-------------------------InsertDataProcessModeTwo.java
-------------------------InsertDataProcessModeZero.java
-------------------------InsertDataProcessOneThread.java
-------------------------InsertDataTransactionalProcessModeThree.java
-------------------------ProcessMode.java
-------------------------SelectDataProcessModeOne.java
-------------------------SelectDataProcessModeTwo.java
-------------------------SelectDataProcessModeZero.java
-------------------------SelectDataTransactionalProcessModeThree.java
---------------------service
-------------------------imp
-----------------------------TransferDatabaseConfigServiceImpl.java
-----------------------------TransferDataTaskServiceImpl.java
-----------------------------TransferHealthyServiceImpl.java
-----------------------------TransferInsertConfigServiceImpl.java
-----------------------------TransferLogServiceImpl.java
-----------------------------TransferSelectConfigServiceImpl.java
-------------------------TransferDatabaseConfigService.java
-------------------------TransferDataTaskService.java
-------------------------TransferHealthyService.java
-------------------------TransferInsertConfigService.java
-------------------------TransferLogService.java
-------------------------TransferSelectConfigService.java
---------------------utils
-------------------------DateUtils.java
-------------------------jdbc
-----------------------------JdbcUtils.java
-------------------------SpringContextUtils.java
-------------------------thread
-----------------------------CommunityThreadFactory.java
-------------------------thread
-----------------------------DataMigrationRejectedExecutionHandler.java
-------------------------thread
-----------------------------ExecutorServiceUtil.java
-------------------------uuid
-----------------------------SnowflakeIdWorker.java
---------main
-------------resources
-----------------application.yml
-----------------banner.txt
-----------------logback-spring.xml
-----------------mapper
---------------------TransferDatabaseConfigMapper.xml
---------------------TransferDataTaskMapper.xml
---------------------TransferInsertConfigMapper.xml
---------------------TransferLogMapper.xml
---------------------TransferSelectConfigMapper.xml
-----LICENSE
-----pom.xml
-----README.md
-----dataMigration.iml

設計模式

如果不考慮設計模式,直接硬編碼在我們項目當中會有很多的藕合,也會導致整個代碼的可讀性很差。如下面的代碼:
首先來看項目類的代碼執行的步驟:
1、查詢源庫數據
2、檢查查詢出的源庫數據
3、插入目標數據庫
4、檢查插入
5、刪除源庫數據

    if(mode = 1) ...
        selectData();
        checkSelectData();
        insertData();
        checkInsertData();
        deleteData();
    if(mode = 2) ...
        selectData();
        checkSelectData();
        insertData();
        checkInsertData();
        deleteData();
    if(mode = 3) ...
        selectData();
        checkSelectData();
        insertData();
        checkInsertData();
        deleteData();
    ...

每次新增一個mode,我們就需要手動去修改代碼,增加一個if,極度不易於拓展,而且if的多少影響閱讀和美觀。當我們加入設計模式之後,這樣的if可以直接被取消。

  • 橋接模式的應用
    什麼是橋接模式?橋接(Bridge)是用於把抽象化與實現化解耦,使得二者可以獨立變化。這種類型的設計模式屬於結構型模式,它通過提供抽象化和實現化之間的橋接結構,來實現二者的解耦。這種模式涉及到一個作爲橋接的接口,使得實體類的功能獨立於接口實現類。這兩種類型的類可被結構化改變而互不影響。

可以在上面的代碼中看到,假若我們能消除if else很多的代碼就不會重複,比如其中的固定執行流程。然後每一個流程代碼,我們設計成爲一個固定的模板,每個模板都有一個固定的方法,這個時候,對於具體的實現獨立出來就完美的解決了這樣的問題。

首先解決第一個問題:將步驟代碼抽象出來,不管哪個模式,都要走定義好的步驟方法。但是具體實現每個模式互不干擾

public abstract class ProcessMode {

    public final void invoke(TransferContext transferContext) {
        try {
            handler(transferContext);
        } catch (DataMigrationException e) {
            throw e;
        } catch (Exception e) {
            throw new DataMigrationException("system error, msg: " + e.getMessage());
        }
    }

    protected abstract void handler(TransferContext transferContext);

}

這裏直接抽象出來一個類,保障每個流程步驟都屬於他的實現類,這樣解決了流程步驟的多實現互不干擾。

然後解決if else:有了抽象類了,我們這裏直接使用mode去定位對應的具體實現。當然,這裏要配合一個小巧的類的注入名的修改。原本靠if else檢索位置,現在直接採用類型來定位,這樣就不需要使用判斷來做了。類加載到容器中都有自己固定的名字,這裏我們將每一個mode對應的實現類設計成爲帶有mode的類,設計規則:process + 功能 + mode號, 然後我們按照類的實現自動加載使用對應的實現類

ProcessMode sourceBean = SpringContextUtils.getBean("process.selectData." + transferContext.getTransferDataTask().getTransferMode(), ProcessMode.class);

在橋接模式中,我們可以看到需要一個橋(類)來連接對應的實現
橋接模式
在我們的代碼中其實也正是使用了這種思想,來讓我們的代碼每個mode方式之間互不干擾,但又能和代碼緊密相連接。
橋接模式代碼實現

ProcessMode完美的銜接了我們代碼,充當了我們的橋

  • 單例模式的應用
    單利模式很好被理解,單例(Singleton)模式的定義:指一個類只有一個實例,且該類能自行創建這個實例的一種模式。單例模式有 三個特點:1、類只有一個實例對象; 2、該單例對象必須由單例類自行創建; 3、類對外提供一個訪問該單例的全局訪問點;

爲什麼用單利?
單利最常見的應用場景有
1、Windows的Task Manager(任務管理器)就是很典型的單例模式 2、項目中,讀取配置文件的類,一般也只有一個對象。沒有必要每次使用配置文件數據,每次new一個對象去讀取。 3、數據庫連接池的設計一般也是採用單例模式,因爲數據庫連接是一種數據庫資源。 4、在Spring中,每個Bean默認就是單例的,這樣做的優點是Spring容器可以管理 。 5、在servlet編程中/spring MVC框架,每個Servlet也是單例 /控制器對象也是單例.
在我們項目中,到哪裏的場景很顯然就是第一個,他就是一個任務管理器。作爲一個開源的遷移數據項目,我們需要考慮不單單是每次僅執行一個任務,而是每次執行多個,並且每次可能會併發執行很多的任務,當然隨之而來的就是數據安全爲題,和任務重複的問題。這個我們後面在做解釋

單利模式又分爲:懶漢式、餓漢式。爲了保證數據的安全,我們這裏直接選用了餓漢式

具體實現如下:

public class ExecutorServiceUtil {

    private static final Logger logger = LoggerFactory.getLogger(ExecutorServiceUtil.class);

    private static ThreadPoolExecutor executorService;

    private static final int INT_CORE_POOL_SIZE = 50;
    private static final int MAXIMUM_POOL_SIZE = 100;
    private static final int KEEP_ALIVE_TIME = 60;
    private static final int WORK_QUEUE_SIZE = 2000;

    static {
        executorService = new ThreadPoolExecutor(
                INT_CORE_POOL_SIZE,
                MAXIMUM_POOL_SIZE,
                KEEP_ALIVE_TIME,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(WORK_QUEUE_SIZE),
                new CommunityThreadFactory(),
                new DataMigrationRejectedExecutionHandler());
    }

    private ExecutorServiceUtil() {
        if(executorService != null){
            throw new DataMigrationException("Reflection call constructor terminates execution!!!");
        }
    }

    public static ExecutorService getInstance() {
        logger.info("queue size: {}", executorService.getQueue().size());
        logger.info("Number of active threads: {}", executorService.getActiveCount());
        logger.info("Number of execution completion threads: {}", executorService.getCompletedTaskCount());
        return executorService;
    }
}

將我們的線程池進行單利化,爲的就是解決任務多發或者高發的問題,有序的控制任務內存和執行。既然提到了多發或者高發,肯定需要解決的就是線程安全問題。這裏我們不僅使用了單例,還對部分數據操作的類做了數據安全處理

類關係圖

在這裏插入圖片描述
對關鍵類都去定義了一個:@Scope(value = "prototype"),使用原型作用域,每個任務過來都會生成一個獨有的類,這樣有效的防止數據共享。

事務的管理

在數據遷移過程中,事務的執行是很重要的,如不能保證數據一致,可能在遷移過程中直接導致數據丟失。

項目中也有簡單的對事務的應用。由於設計模式的問題,拓展能力很強,代碼靈活度也很高。不過接踵而來的就是部分安全代碼的問題。比如:事務。當我們獨立了插入和刪除的時候,我們的事務就被拆分了。那我們需要使用分佈式事務,或者事務傳播嗎?

項目中並沒有!

事務
原本設計模式之前,直接包含在一個事務內即可,這裏我們使用了設計模式之後,行不通了。

現有的事務邏輯
事務

不難發現,如果insertData出現問題,insertData的事務會回滾對應的插入數據,當時deleteData繼續執行,這個時候肯定會來帶數據一致性問題。不過這裏採用了流程上的控制來解決這個問題。

假若:一個遷移任務只執行一次insertData,deleteData,爲了保證數據一致性,我們將這兩個流程設計了固定的執行先後順序,並且,前面一個報錯,後面不在繼續執行。
在這裏插入圖片描述

報錯如果出現在刪除,那有什麼影響,就一個:那就是源庫的數據有一部分並未被刪除掉,但是肯定是已經遷移到了目標庫,這個時候嚴格意義上來講,數據的遷移並不影響,僅僅只是多出來一份數據,沒有數據安全問題。

總結

每個設計方案都不是完美的,都是巧妙的去設計,靈活的去適應。
假若不斷迭代,問題肯定會不斷出現,新的方案肯定也會不斷的替代老方案。

拓展

當前項目真正應用應該怎麼入手?怎麼商用?

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章