REGULAR PURCHASE MIGRATION

Migration Process 架構設計

一間商店的定期購資料包含商品、會員、訂閱等不同類型。搬移時,要把舊 MySQL 的資料轉到新 PostgreSQL;每一類資料搬完後還要驗證,才能接著處理下一類。

整輪共有 14 個階段。架構設計要同時處理階段順序、每批資料的搬移與驗證,以及中斷後的接續。
01
為什麼設計 Stage

整輪搬移涵蓋商品、會員、訂閱等不同資料。它們各有搬移與驗證規則,卻都要經過「搬移一批、保存進度、核對一批」的節奏。把每一類資料切成一個 Stage,才能讓共通流程與各類資料的規則有清楚的界線。

規則按資料類型分開
每個 Stage 由對應的 Stage Handler 決定資料怎麼搬、怎麼驗證。
完成條件可以辨認
Copied 只代表搬完;達到 Validated,才代表這個階段完成。
進度能定位到階段
migration_process 以 MigrationId、ShopId、Stage 記錄各階段的進度與狀態。

因此,Product 搬完時還不能直接進入 Member。兩個 Stage 之間,必須先確認 Product 已驗證通過。

02
為什麼設計 Manage Job

Transfer Job 知道怎麼搬目前這一批,成功後接續下一批;Validate Job 知道怎麼核對目前這一批,通過後接續下一批。兩者處理的是 Stage 內的工作,還需要有人判斷何時驗證、何時進入下一個 Stage。

以 Product 接到 Member 為例,Product 搬完後仍須驗證。只有狀態達到 Validated,才能安排 Member 搬移。

Product 搬移完成狀態為 Copied
Manage 安排驗證派送 Validate Job
Product 驗證通過狀態為 Validated
Manage 安排搬移進入 Member 階段

這個調度責任即使沒有 Manage Job,仍然需要有人承擔。若由 Transfer Job、Validate Job 各自判斷下一階段,再由 RD 拼出恢復流程,階段越多,越容易漏掉驗證、用錯游標,或提前進入下一階段。

順序只在一處決定
Product 必須到 Validated,才能開始 Member。新增 Stage 時,也有明確的地方調整順序。
恢復從同一入口開始
RD 重新建立 Manage 任務,由保存的進度找出階段與游標,不必自行推算該叫哪個 Job。
驗證失敗先停下判斷
資料差異不能直接視為搬移失敗後的續跑。Manage Job 會區分一般恢復、重新驗證與支援的修復操作。

Manage Job 因此負責跨階段調度、續跑與人工處理的入口。這些規則不必散落在每個批次 Job 裡。

03
架構成員

Manage Job 在階段交接或恢復時讀進度、決定工作;一個階段內的批次接續,仍由 Transfer Job 與 Validate Job 自己完成。資料怎麼搬、怎麼驗證,則交給對應的 Stage Handler。

元件角色負責的事
Manage Job調度讀取整間商店的進度,決定要搬移、驗證,或等待人工處理
Transfer Job搬移讀取一批舊資料,轉換後寫入新資料庫,並接續下一批
Validate Job驗證重新讀取一批來源資料,核對新資料庫,並接續下一批
Stage Handler資料類型規則決定商品、會員、訂閱等資料各自的搬移與驗證方式
Repository資料存取執行來源查詢、目標寫入與比對,以及進度保存
migration_process進度紀錄保存每個階段的搬移與驗證進度、狀態及失敗資訊
NMQ任務派送將 Manage、Transfer、Validate 任務交給 Worker 執行
Manage Job讀取進度,決定下一步
NMQ派送對應任務
Transfer Job搬移一批
Validate Job核對一批
對應的 Stage Handler依資料類型執行搬移或驗證規則
舊 MySQL:來源資料
新 PostgreSQL:目標資料
migration_process 保存狀態與進度,供三種 Job 在處理時查詢或更新。
此圖表示元件的責任與資料關係;Transfer Job 與 Validate Job 是依目前狀態選擇的工作。
RECOVERY & DISPATCH

停下來後,怎麼從正確的位置接續?

Manage Job 以保存的階段、狀態與游標作為恢復依據;遇到驗證失敗時,還要區分重新核對與重寫資料。

01
失敗後,從保存的進度續跑

假設 Order 搬到一半失敗,RD 以相同的 MigrationId 與 ShopId 重新建立 Manage 任務。Manage Job 會依已保存的狀態處理:

階段已保存狀態下一步
Shop、Product、MemberValidated略過
OrderFailed從保存的搬移游標續跑
後續階段Pending等待 Order 完成

失敗批次不推進搬移進度。RD 不必自行推算階段、游標或要派送的 Job;恢復工作的依據是 migration_process 保存的進度。

02
搬移失敗與驗證失敗,處理方式不同

搬移失敗可以從保存位置續跑;驗證失敗可能代表資料有差異,不能直接自動重寫。Manage Job 依狀態與 RD 指定的操作做出不同決定:

目前狀態與操作Manage Job 的決定
Pending 或 Failed派送 Transfer Job
Copied派送 Validate Job
Running 或 Validating等待,不另派工作
ValidationFailed,一般 Resume停住,等待人工判斷
ValidationFailed,RetryValidation派送 Validate Job,從驗證游標重新核對
ValidationFailed,RepairValidation派送 Transfer Job 執行該階段支援的修復模式
驗證失敗不會自動重寫資料。一般 Resume 會停住,待 RD 確認原因後再選擇重驗或支援的修復操作。
ONE MIGRATION RUN

一次 Migration 怎麼跑?

先指定這一輪要搬的商店與每批筆數。Manage Job 建立 14 個 Stage 的進度後,從第一個尚未驗證完成的階段開始,依序安排搬移與驗證。

01
啟動時需要哪些資訊?
MigrationId
辨識這一輪搬移。
ShopId
指定要搬移的商店。
BatchSize
決定每批處理多少筆,預設為 500。
02
Manage 如何選下一個 Stage?

Manage Job 初始化 14 個 Stage 的進度,然後找出第一個尚未達到 Validated 的階段。即使某個階段已搬完,只要驗證未通過,就不會進入後面的階段。

順序資料類型
1~4Shop → Product → Member → Order
5~10UpdateHistory → CreateRegularLog → MemberLoginLog → ShopManagementLog → ApiAccessLog → ApiRequestLog
11~14NotifyQueueArchive → OrderCreateQueueArchive → NotifyQueue → OrderCreateQueue
03
一個 Stage 何時從搬移轉成驗證?

每個 Stage 都先由 Transfer Job 一批批搬移。查到來源尾端後,狀態改為 Copied,再由 Manage Job 安排 Validate Job 一批批核對。

搬移階段:Transfer Job
讀取來源的一批資料;有資料時寫入目標並保存搬移進度。
有資料且寫入成功保存搬移進度,接著讀取下一批。
來源查到 0 筆標記 Copied,通知 Manage Job 安排驗證。
搬移失敗標記 Failed;失敗批次不推進進度。
驗證階段:Validate Job
重新讀取來源的一批資料,核對新資料庫裡的結果。
有資料且一致保存驗證進度,接著核對下一批。
來源查到 0 筆標記 Validated,通知 Manage Job 進入下一個 Stage。
內容有差異或驗證出錯標記 ValidationFailed,等待人工判斷。
Copied 只代表搬完;Validated 才代表階段完成。即使最後一批只有 22 筆,系統仍會再查一次;真正查到 0 筆,才確認已到資料尾端。
MIGRATION PROGRESS

怎麼記住搬到哪裡?

同一個 Stage 可能已搬完多批資料,卻只驗證到較早的一批。migration_process 因此分開保存搬移與驗證進度,讓兩種工作都能從各自成功的位置接續。

01
一列進度對應哪個工作範圍?

migration_process 以一輪搬移、一間商店、一個 Stage 的組合識別每列進度:

MigrationId
ShopId
Stage
02
搬移與驗證各記住什麼?

每套進度都記住「上次成功處理到的來源主鍵」與成功累計。下一批在同一商店的來源資料中,從 PK 大於各自游標的位置繼續讀取。

搬移進度
最後成功處理的來源主鍵last_pk
搬移成功累計processed_count
下一批讀取:PK > last_pk
驗證進度
最後核對通過的來源主鍵validation_last_pk
驗證成功累計validated_count
下一批讀取:PK > validation_last_pk
03
為什麼不能共用一個游標?

「搬移成功累計 1,200 筆」和「驗證通過 500 筆」描述的是兩件不同的事。若驗證停在某一批,搬移已完成的位置仍要保留,重驗才能從驗證游標接續,不會把搬移進度蓋掉。

游標只在成功處理後向前。搬移與驗證各自保存成功位置,後續批次使用對應的游標讀取來源。
FAILURE & REPAIR

失敗後,資料與進度怎麼接回來?

重新派送工作只是恢復入口。真正需要確認的是:同一批資料重跑會不會重複寫入、進度會不會重複累計,以及驗證失敗是否需要改動目標資料。

01
資料已寫入,進度卻沒保存,重跑會怎樣?

目標寫入採用 Upsert:資料不存在就新增,存在則依規則更新。因此同一批再次執行時,可以重新處理已寫入的資料;進度更新另會檢查原游標,避免重複累計。

目標資料已寫入本批 Upsert 完成
進度保存失敗游標仍在舊位置
再次處理同一批從保存的游標重跑
寫入與計數各自保護Upsert;檢查原游標
02
停在 Running 或 Validating,能直接續跑嗎?

目前尚未有鎖、租約或心跳判斷任務是否仍存活。若程序突然中止,狀態仍留在 Running 或 Validating,Manage Job 會等待,不另派工作,需要人工確認後恢復。

這是目前的實作限制。進度表顯示工作進行中,不代表程序此刻仍在執行。
03
ValidationFailed 要先查什麼?

ValidationFailed 可能是資料真的不同,也可能是驗證過程發生錯誤。先看 migration_process.last_error 和差異 log,確認原因;若來源或轉換程式有問題,應先修正原因,再決定後續操作。

操作實際做的事是否修改目標業務資料
RetryValidation再檢查一次,只讀取與比對不會
RepairValidation依目前來源重寫失敗批次,再核對會,執行 Upsert
修復操作有適用範圍。目前只有 Product Stage 支援 RepairValidation;其他 Stage 驗證失敗時不能直接套用。
查到不同原因,下一步也不同
查到的原因RD 要確認什麼適合的處理
SCM timeout、DB timeout、網路錯誤服務是否恢復?有沒有證據顯示目標資料錯誤?恢復後執行 RetryValidation
目標缺資料或欄位不符目前來源與轉換結果是否正確?目標是否應該跟著更新?Product 可選 RepairValidation
資料已由 RD 或其他程序修正修正結果是否符合預期?以 RetryValidation 驗收
來源異常或轉換程式有 bug哪一份內容才是正確的?先修原因,再決定重驗或重寫
目標已有新版正常操作產生的更新重寫是否會蓋掉應保留的資料?先確認資料交接規則,暫不修復
SUPPLEMENTAL SYNC

補同步為什麼要開新的一輪?

第一輪完成後,舊系統的資料仍可能改變。建立新的 MigrationId,沿用相同的 ShopId,就能從頭再搬移並驗證一次,補到原本游標之前的修改與新增關聯資料。

01
為什麼不能只沿用上一輪的游標?

上一輪的游標已走到來源尾端。若舊系統後來修改了較早的資料,從原游標繼續讀取就不會再讀到它。新的一輪有自己的進度,會從來源重新讀取,再以 Upsert 寫入目標。

第一輪完成游標已到來源尾端
來源再次變動可能落在舊游標之前
建立新 MigrationId同一 ShopId,從頭讀取
Upsert 並驗證補上新增與更新
02
第二輪碰到不同變動,結果是什麼?

相同 PK 的資料走 Upsert,不會再新增一筆相同主鍵的資料。以下是第一輪完成後,來源再次變動的例子:

來源變動第二輪從頭跑的結果
訂閱 PK 300 修改收件地址更新目標既有的 PK 300
訂閱 PK 500 新增一期明細重讀主檔及全部關聯資料,新增缺少的明細
新增訂閱 PK 1001新增目標 PK 1001
商品 PK 100 完全沒變不會新增另一筆 PK 100
03
完整重跑,仍有哪些資料接不起來?
來源已刪除的資料

第二輪只讀得到來源仍存在的資料。來源已刪除的資料不會因 Upsert 自動從目標刪掉。

已開始執行的 Queue

若新版已開始處理 Queue,重跑可能把目標的執行狀態更新成舊來源狀態。沒有重複資料列,不代表工作不會再次執行。

切換前的最後補同步需要控制交接時機。先控制來源寫入與 Queue 執行,再完成核對及交接,才能把新的一輪當作最後補同步。
DELETION GAP

舊資料刪除了,目標為什麼還在?

目前的補搬只會讀取來源仍存在的資料,再對目標執行 Upsert。來源刪掉的資料不會被讀到,因此重新搬移也不會自動清掉目標那一筆。

01
PK 200 怎麼變成目標獨有?

第一輪完成時,舊 MySQL 與新 PostgreSQL 都有 PK 100、PK 200、PK 300。之後舊系統刪除 PK 200;第二輪只能讀到 PK 100 和 PK 300,目標的 PK 200 仍會留下。

舊 MySQL:目前來源
第二輪能讀到的主鍵
PK 100PK 300
新 PostgreSQL:目前目標
第二輪結束後仍存在的主鍵
PK 100PK 200,目標獨有PK 300
02
要找出刪除差異,還需補上什麼?

目前流程沒有找出「目標有、來源沒有」的資料。若要把刪除納入完整補同步,還需要增加以下比對與處置步驟:

確認來源停止變動先固定比對範圍
取得完整 PK 清單同一商店的來源與目標
找出目標獨有 PK目標有,來源沒有
依類型處置並驗證刪除、停用或保留
03
目標多出一筆,就能直接刪嗎?

同樣是目標獨有的資料,原因與用途可能不同。找出差異後,還需要按資料類型判斷:

資料類型需要判斷的事
商品、會員、訂閱是否確實被來源刪除?應刪除還是停用?
訂閱明細、mapping主檔還在,但部分關聯資料是否被刪除?必須各自比對。
Queue是否已執行並移到 Archive?不能只因來源 Queue 沒有就判定刪除。
Log、Archive是否因保存期限被清除?目標是否仍應保留?
比對範圍必須先界定。只比較同一商店、同一搬移範圍;目標由新版系統自行建立的資料,也不能直接當成來源刪除。

目前支援的是新增與更新的補搬。完整補同步還缺刪除差異偵測與處置規則;可先產出「目標多出哪些資料」的清單,確認每類資料的意義後,再決定如何處理。

COMPATIBILITY, NOT COPYING

搬移完成,資料就能在新架構運作嗎?

搬移目標是讓舊資料能被新系統正確使用。主鍵存在、欄位原值相同,只能證明資料寫進去了;資料格式、時間區間與狀態仍可能需要轉換。

01
NotifyQueue:字串相同,Worker 卻讀不懂

文件中的待發通知是 StockNotify。這筆資料仍可能被新版 Worker 消費,關鍵不只在於搬移後字串是否相同,還在於 JSON 是否符合新版格式。

來源欄位案例值
nq_pk12001
nq_typeStockNotify
nq_ref_shop2
nq_send_status0,待發
nq_sended_count0

舊版 GameBall 的 stock_estimate_housekeeper.php 直接儲存商品陣列;新版 RegularOrderStockNotifyJob 要求外層有 SchemaVersion、ProductItems、GiftItems 的 Version 2 envelope:

舊資料:直接存陣列
[
  {
    "Cycle": 1,
    "SalePageId": 456,
    "Qty": 2
  }
]
新版 Worker 需要:Version 2 envelope
{
  "SchemaVersion": 2,
  "ProductItems": [
    {
      "Cycle": 1,
      "SalePageId": 456,
      "Qty": 2
    }
  ],
  "GiftItems": []
}

目前程式把舊 JSON 原樣搬過去,再以來源原字串比對目標。這讓 Validate 可以通過,但新版 Worker 讀到舊陣列時仍會解析失敗;文件指出此問題目前尚未修正。

原樣搬移舊 JSON目標字串與來源相同
Validate 通過只比對原字串
新版 Worker 消費期待 Version 2 格式
解析失敗舊陣列缺少 envelope
這裡的驗證成功只代表原值一致。notify_queue_bak 是封存資料,不由待發通知 Worker 消費;相容性問題針對仍可能執行的 notify_queue。
02
Product:銷售期間與狀態要一起轉換

Product Handler 以 regular_product.rpd_page_id 向 SCM 查商品銷售期間,並以台灣時間(UTC+8)的 Now 比較。查到有效的 SCM 商品時,銷售期間使用 SCM 最新值;來源狀態為啟用時,還要依當下是否位於銷售區間內決定搬移後狀態。

來源 Productrpd_page_id
SCM 銷售期間SellingStart / End
Product Handler比較台灣時間 Now
目標 Productsale、plan 期間與狀態
此圖呈現查到有效 SCM 商品時的轉換;查無商品與無效期間的行為見下方條件表。
SCM 時間要寫入哪些欄位?
SCM 商品銷售時間新架構欄位
SellingStartDateTimerpd_sale_date_begin、rpd_plan_date_begin
SellingEndDateTimerpd_sale_date_end、rpd_plan_date_end

舊架構沒有 rpd_plan_date_begin、rpd_plan_date_end,Migration 必須用 SCM 銷售區間補上。V2 用方案生效時間判斷方案是否適用;若 plan 區間沒有對齊銷售區間,即使原有銷售欄位正確,新架構仍可能在錯誤時間選到或排除方案。

狀態會如何轉換?

程式測試使用固定的 Now。下表保留測試涵蓋的區間邊界、原狀態及 SCM 查無商品的結果:

商品原狀態SCM 銷售期間相對 Now搬移後狀態
1,啟用Now 前 1 小時至 Now 後 1 小時1,仍在銷售
1,啟用Now 至 Now 後 1 小時1,開始時間剛好等於現在
1,啟用Now 前 1 小時至 Now1,結束時間剛好等於現在
1,啟用Now 後 1 小時至 Now 後 2 小時9,尚未開賣,停用
1,啟用Now 前 2 小時至 Now 前 1 小時9,已停售,停用
9 或 8期間在銷售中保留原狀態
1,但 SCM 查無商品沒有目前銷售期間保留來源狀態與期間;HasCurrentSales = false
來源與 SCM 都會影響搬移結果。查無 SCM 商品時保留來源狀態與期間;SCM 回傳無效期間時,搬移會失敗。
驗證可能遇到不同的 SCM 預期值。Product 驗證會重新查 SCM 最新期間,沒有使用跨系統快照。若 SCM 在搬移與驗證之間改變銷售期間,兩次可能得到不同結果;目前沒有固定整輪搬移的時間基準。