Project

General

Profile

CR #201 » SAD-003_資料架構與CQRS.md

內容更新:內部稱謂去識別化(2026-10-18) - Redmine Admin, 10/18/2026 05:32 AM

 

SAD-003 資料架構與 CQRS

項目 內容
文件編號 SAD-003
版本 1.2
狀態 已核定
撰寫者 SA / DA
審查者 TL / SEC
對應需求 SYS-2.1、SYS-2.4、SYS-2.5、SYS-2.12;POC-14

1. 資料架構原則

編號 原則 說明
DP-01 一服務一 schema 每個服務擁有自己的 schema,僅該服務可寫入。權威 OLTP schema 位於 Oracle(CC_<SERVICE>);CQRS 讀模型、事件流存檔與報表 schema 位於 PostgreSQL(proj/evt/rpt)。落點分工見 CSP-006 §2.4
DP-02 權威資料單一來源 每張表只有一個服務為權威寫入者
DP-03 金額為整數最小單位 一律以「分」儲存,禁止浮點數(BRD-001 C-01)。Oracle 用 NUMBER(19,0),PostgreSQL 用 bigint
DP-04 原始紀錄不可變 交易紀錄不得 UPDATE/DELETE,僅能以沖正處理(C-02)
DP-05 前後值留存 餘額異動須記錄 Before/After Image
DP-06 個資加密與遮罩 卡號、身分證字號加密儲存、遮罩顯示(SAD-005 §5)
DP-07 時間為 UTC 資料庫以含時區型別儲存(Oracle TIMESTAMP WITH TIME ZONE/PostgreSQL timestamptz),顯示時轉換為本地時區
DP-08 事件為跨服務唯一傳播方式 服務間資料同步以事件為之,非共用資料庫

2. 資料分類與儲存策略

資料類別 特性 儲存 一致性要求
主檔(持卡人、卡片、產品、商戶) 讀多寫少 寫庫 + 讀庫 強一致(單庫交易)
交易(授權、帳務、繳款) 寫多、需稽核 寫庫(append-only) 強一致(單庫交易)
餘額(額度、帳務) 高頻讀寫 寫庫 + 快取 強一致 + 快取失效
查詢視圖(明細、報表) 讀多寫少 讀庫(CQRS) 最終一致(≤ 1 秒)
參數(產品、費率、規則) 讀極多寫少 寫庫 + 快取 + 版本 強一致 + 版本控管
事件(領域事件) 只增不改 事件流 + Outbox 至少一次 + 冪等消費
稽核軌跡 只增不改 稽核表 + 不可竄改 強一致
日誌 只增 日誌儲存 最終一致

3. 資料庫部署架構

v1.1 變更:權威 OLTP 由 PostgreSQL 改為 Oracle(客戶指定),
PostgreSQL 轉為承擔 CQRS 讀模型、事件流存檔與報表分析。落點分工見 CSP-006 §2.4。

flowchart TD
  subgraph APP["應用層"]
    W1["寫入路徑<br/>Command Handler"]
    R1["查詢路徑<br/>Query Handler"]
  end
  subgraph ORA["Oracle — 權威 OLTP"]
    OP["Primary<br/>CC_SERVICE 各服務 schema<br/>(含該服務之 Outbox)"]
    OPLT["CC_PLT<br/>Saga 狀態"]
    OS["Standby<br/>Data Guard 同步複寫"]
  end
  subgraph PG["PostgreSQL — 讀側"]
    RA1["proj<br/>CQRS 讀模型"]
    RA2["evt<br/>事件流存檔"]
    RA3["rpt<br/>報表分析"]
  end
  subgraph EVT["事件層"]
    KF["Kafka"]
    PROJ["投影器 Projector"]
  end
  CA["Redis 快取"]
  W1 --> OP
  W1 --> OPLT
  OP --> OS
  OPLT --> KF
  KF --> PROJ
  KF --> RA2
  PROJ --> RA1
  R1 --> RA1
  R1 --> RA3
  R1 --> CA
  W1 --> CA

Outbox 位置之關鍵約束:Outbox 表建於每個服務自己的 schema 內(CC_<SERVICE>.outbox),
依 SDD-001 §9「通用平台表(每服務各一份)」。如此「業務資料寫入」與「事件寫入」
可於同一 schema、同一資料庫交易內完成(SAD-001 AP-05、SDD-004 §4.3),
且不違反 DP-01「僅該服務可寫入自己 schema」。

不得將 Outbox 集中至共用 schema(違反 DP-01),亦不得移至 PostgreSQL
(跨庫即無法保證原子性)。CC_PLT 僅承載跨服務之 Saga 協調狀態(SDD-004 §5.3)。

3.1 讀寫分離策略

面向 寫入路徑 讀取路徑
資料庫 Oracle(Primary) PostgreSQL(proj/rpt)
資料來源 服務自身 schema + CC_PLT CQRS 投影視圖、報表視圖
一致性 強一致(Oracle 單庫交易) 最終一致(≤ 1 秒)
用途 授權佔用、入帳、狀態變更 餘額查詢、明細查詢、報表
併發控制 樂觀鎖(version)+ 資料庫鎖 無鎖
失敗處理 交易回滾 重試或回退至 Oracle 即時查詢
高可用 Data Guard Standby 串流複寫 Replica

設計意涵:讀路徑完全不碰 Oracle,故分析型與高頻查詢不佔用 Oracle 授權;
寫路徑維持 Oracle 之強一致與交易保證。此為本案「Oracle 主 + PostgreSQL 讀側」之核心價值。

3.2 同步機制(對應 POC-14)

sequenceDiagram
  participant CMD as 命令處理器
  participant DB as Oracle 寫入庫<br/>服務 schema
  participant OBX as Outbox<br/>同服務 schema
  participant KFK as Kafka
  participant PRJ as 投影器
  participant RDB as PostgreSQL 讀取庫<br/>proj
  participant API as 查詢 API

  CMD->>DB: 交易開始
  CMD->>DB: 更新餘額(樂觀鎖)
  CMD->>OBX: 寫入領域事件(同一交易)
  CMD->>DB: 交易提交
  Note over DB,OBX: 同一 Oracle 交易,資料與事件同時成立(不可分離)
  OBX->>KFK: 發送事件(至少一次)
  KFK->>PRJ: 消費事件(分區內有序)
  PRJ->>RDB: 更新查詢視圖(冪等)
  API->>RDB: 查詢餘額與明細
  Note over CMD,API: 端到端延遲目標 ≤ 1 秒

保證:

保證 機制
事件不遺失 Outbox 與業務資料同一交易寫入
事件不重複生效 消費者以 event_id 冪等
順序正確 以 aggregate_id 為 Kafka 分區鍵
斷線可補 消費者 offset 由 Kafka 管理,恢復後續傳
可追蹤 事件帶 trace_id、causation_id

4. CQRS 投影設計

4.1 跨服務投影清單(★ CR-030 §2 #6 裁定後改寫)

原表之 7 個 q_* 已全數廢除(2026-10-01,CR-030 §2 #6)。

廢除理由(P1/P2):原 7 列逐列比對後 7/7 皆為「自投影」——「來源事件」之生產者
與「服務」欄(存放地)為同一服務:

原視圖 來源事件 生產者 存放地 判定
q_account_balance posting.balance.changed.v1 posting posting 自投影
q_available_limit limit.limit.adjusted.v1、limit.hold.* limit limit 自投影
q_transaction_history posting.transaction.posted.v1 posting posting 自投影
q_authorization_history authorization.* authorization authorization 自投影
q_points_balance rewards.points.* rewards rewards 自投影
q_statement_summary statement.statement.issued.v1 statement statement 自投影
q_merchant_settlement merchant.settlement.submitted.v1 merchant merchant 自投影

自投影不是 CQRS 讀模型:它只是把權威表複製一份到讀側,帶來雙寫、投影延遲、一致性三重成本,
而零收益(權威表本即可直接查詢),且與權威表同名同義易誤用。
CQRS 讀模型之存在價值在「跨服務查詢」——避免同步呼叫扇出(fan-out)與跨庫 join。

業界依據:CQRS 之 read model 是「為查詢而生的另一種視圖」;同構複製不構成 read model。

現行跨服務投影清單(由實際需求驅動;SAD-004 §5.2 為事件之權威清單)

投影 存放地 餵養事件(生產者 ≠ 存放地) 用途 依據
card_status_caches authorization card.card.status-changed.v1(card) 授權時免同步查卡況 #A-117/#A-118
account_limit_caches authorization limit.limit.adjusted.v1(limit) 授權時免同步查額度 #A-119
limit_holds(本地投影) authorization limit.hold.created.v1/.released.v1(limit) 佔用狀態本地可見(含 stand-in 對帳) #A-128

⚠️ 本清單之新增須經規格(CSP-001 §5 條件 9:無 SCS 不寫程式),
並以「生產者 ≠ 存放地」為必要條件——否則即為自投影,不應建立。
後續候選(待規格):party 之卡片狀態投影(CR-029 §4 #3)、各服務之參數快取(CR-029 §4 #4)。

4.1.1 原表保留之歷史說明

SAD-003 §3.2 之「同步機制(對應 POC-14)」與 §4.2「投影規則」、§4.3「投影延遲監控」仍然有效,
惟其適用對象為本節所列之跨服務投影(非自投影)。

4.2 投影規則

規則 說明
冪等 以 event_id 去重,重複事件直接略過
有序 同一聚合之事件依 sequence_no 套用;亂序則暫存等待
可重建 視圖可自事件流全量重建(rebuild 命令)
可監控 投影延遲(event time → apply time)為監控指標
不可回寫 投影視圖僅供查詢,不得作為寫入來源

4.3 投影延遲監控

指標 目標 告警
projection_lag_seconds(P95) ≤ 1 秒 > 2 秒
projection_lag_seconds(P99) ≤ 3 秒 > 5 秒
projection_failed_total 0 > 0
consumer_lag(Kafka) ≤ 1,000 > 10,000

5. 分庫分表策略

5.1 分表(Partitioning)

表 分表鍵 策略 理由
posting.transactions posted_at 月分區 量大、查詢多以時間範圍
authorization.authorizations created_at 月分區 量大、生命週期短
posting.balance_history account_id 雜湊分區 查詢以帳戶為主
statement.statements statement_month 月分區 週期性產生
notification.notifications created_at 月分區 量大
audit.audit_trails created_at 月分區 量大、需歸檔

方言對照:Oracle 以 PARTITION BY RANGE (...)/PARTITION BY HASH (...) 實作;
PostgreSQL 以 PARTITION BY RANGE/HASH 實作。Oracle 之「雜湊分區」需注意
分區數須為 2 的次方以達均勻分布(PARTITION BY HASH (account_id) PARTITIONS 16)。

5.2 分庫(Sharding)

階段 策略
第一階段 不分庫,以讀寫分離與分表達成 ≥ 1,000 TPS
擴充階段 若需 > 2,000 TPS,以 account_id 雜湊分庫(4 庫起)
分片鍵選擇 account_id(歸戶),確保同一歸戶之交易同庫

決策:ADR-11 第一階段不分庫。理由:分庫將使跨帳戶查詢(如歸戶合併帳單)複雜化,
且本行 1,000 萬客戶之規模以讀寫分離+分表即可滿足。


6. 一致性與補償

情境 一致性需求 機制
授權佔用額度 強一致(不可超額) 單庫交易 + 樂觀鎖 + 唯一鍵
入帳與餘額更新 強一致 單庫交易(同服務內)
入帳與事件發布 原子 Outbox(同一交易)
跨服務(授權→帳務) 最終一致 事件 + 補償(Saga)
跨服務(繳款→額度) 最終一致(目標 ≤ 1 秒) 事件 + 同步呼叫雙軌
清算比對 強一致(差異須為零) 批次比對 + 差異處理流程
投影視圖 最終一致 冪等投影 + 延遲監控

詳見 SDD-004。


7. 字碼與難字轉換(FR 2.3)

面向 設計
儲存編碼 UTF-8(資料庫、檔案、訊息)
傳輸編碼 對外電文(ISO 8583)依卡組織規範,以約定之替代編碼承載難字
難字對照 product.charset_mappings 表維護「內部 Unicode ↔ 外部替代碼」對照
落地還原 接收時依對照表還原為 Unicode;查無對照則記錄待處理清單
稽核 難字轉換須留存原始與轉換後值
flowchart LR
  IN["外部電文<br/>替代編碼"] --> MAP{"查對照表"}
  MAP -->|"命中"| DEC["還原為 Unicode"]
  MAP -->|"未命中"| Q["寫入待處理清單"]
  DEC --> DB["寫入資料庫<br/>UTF-8"]
  DB --> OUT["產生對外電文"]
  OUT --> ENC["轉為替代編碼"]
  ENC --> SEND["送出"]
  Q --> MANUAL["人工確認並補對照"]
  MANUAL --> MAP

8. 備份、還原與歸檔

項目 Oracle(權威 OLTP) PostgreSQL(讀側)
全量備份 RMAN Level 0,每日一次(離峰) pg_basebackup/pgBackRest,每日一次
增量備份 RMAN Level 1 + ARCHIVELOG 歸檔(每 15 分鐘) WAL 歸檔(每 15 分鐘)
保留 全量 30 天、歸檔日誌 7 天 全量 30 天、WAL 7 天
異地備份 每日同步至異地 每日同步至異地(可由事件流重建,優先度較低)
還原演練 每季一次,量測 RTO/RPO 每季一次
目標 RPO ≤ 5 分鐘、RTO ≤ 30 分鐘 RPO ≤ 15 分鐘、RTO ≤ 2 小時
歸檔 超過線上保留期之交易移至歸檔庫(可查不可改) 事件流依 evt 保留策略處理

可重建性:PostgreSQL 讀側資料可自 Kafka 事件流(evt)與 Oracle 權威資料全量重建,
故其 RPO/RTO 要求低於 Oracle。但 evt(事件流存檔)本身不可重建,須與 Oracle 同級保護。


9. 資料字典與治理

治理項 規則
命名 見 GOV-001 §4.3
型別 金額:Oracle NUMBER(19,0)/PostgreSQL bigint(分);
比率:Oracle NUMBER(9,6)/PostgreSQL numeric(9,6);
時間:Oracle TIMESTAMP WITH TIME ZONE/PostgreSQL timestamptz
必填欄位 每表必有 id、created_at、updated_at、version
軟刪除 主檔可軟刪除(deleted_at);交易不得刪除
稽核欄位 異動類表須有 created_by、updated_by
個資標記 個資欄位須於資料字典標記 PII,並註明遮罩方式
變更 結構變更一律以 migration 檔為之,不得手動改線上結構
自動檢查 tools/validate_sql.py(規則見 CSP-001 §5.1)於 CI 強制執行

完整資料字典見 SDD-001 §4。


10. 修訂記錄

版本 日期 修訂者 說明
1.0 2026-09-17 SA / DA 初版
1.1 2026-09-18 SA / DA 依客戶指定「Oracle 為主、PostgreSQL 於必要時採 Hybrid」調整資料架構:①§1 DP-01 由「一服務一 PostgreSQL schema」改為「Oracle 權威 OLTP schema + PostgreSQL 讀側 schema」,並明訂落點分工;②DP-03/DP-07 補 Oracle 型別對照;③§3 資料庫部署架構圖改為 Oracle(Primary + Data Guard Standby + CC_PLT)與 PostgreSQL(proj/evt/rpt)雙層,並補 Outbox 位置約束;④§3.1 讀寫分離策略改以資料庫分工表述;⑤§3.2 時序圖參與者標註實際資料庫;⑥§5.1 補 Oracle/PostgreSQL 分區方言對照;⑦§8 備份策略由單一 WAL 表述改為 Oracle RMAN/ARCHIVELOG 與 PostgreSQL WAL 雙軌,並補充讀側可重建性之 RPO 差異;⑧§9 型別對照與自動檢查納入
1.2 2026-09-18 SA / DA 更正 1.1 版對 Outbox 位置之錯誤敘述:1.1 版誤載 Outbox 集中於 CC_PLT,與 SDD-001 §9「通用平台表(每服務各一份)」及 DP-01「僅該服務可寫入自己 schema」相牴觸。已更正為 Outbox 建於每個服務自己的 schema 內(CC_<SERVICE>.outbox),CC_PLT 僅承載跨服務之 Saga 協調狀態。§3 部署圖與 §3.2 時序圖之標註同步更正
(6-6/67)