iT邦幫忙

2025 iThome 鐵人賽

DAY 13
0
Software Development

Codetopia 新手日記:設計模式與原則的 30 天學習之旅系列 第 29

Day 29:蒼螺夜市首夜崩壞 & 事件驅動對策會議

  • 分享至 

  • xImage
  •  

Codetopia 創城記 (29)|蒼螺夜市首夜崩壞 & 事件驅動對策會議

1) 今日熱點 (故事開場 & 痛點) ⚡️

故事,要從一個燈火通明,卻混亂不堪的夜晚說起。

今天是 Codetopia 蒼螺夜市的開幕夜。河灣停車場被改造成一片美食的海洋,空氣中瀰漫著烤香腸與炙燒牛肉的香氣。然而,市民們臉上洋溢的,卻不是幸福,而是「怎麼又是你」的厭世表情。

晚上 7 點半,入口處,志工的掃描器對著市民手機上的 QR Code,發出了一連串無情的「嗶嗶嗶」失敗音。供應商 A 的票務系統還活在 auth_v1 的舊時代,而我們熱心的志工們,手上的設備早已是 auth_v2 的新銳。兩邊雞同鴨講,直接把入口變成了大型停車場。

鏡頭轉到攤位區,4G 訊號在這裡彷彿進入了異次元,時有時無。市民們的行動支付請求在空中飄蕩,攤商們急得滿頭大汗,只能瘋狂點擊「重試」。好消息是,錢最終都付了;壞消息是,有時候付了兩三次。(市民的錢包在哭泣啊!)

砰! 一聲悶響,高耗電的章魚燒攤位電力跳脫,結果整排攤商跟著陷入黑暗,上演了一場「我們與餓的距離」。

更糟的是,消防局的例行巡檢、周邊住戶的噪音投訴電話,像潮水般同時湧入市民服務專線。專線瞬間被塞爆,變成一條名副其實的「斷線」。

晚上 10 點 45 分,夜市營運指揮 Helena 站在指揮帳篷裡,看著眼前這場華麗的災難,臉色比停電的攤位還黑。這時,事件平台架構師 Theo 默默地拿出了一塊白板,寫下幾個大字:

把排隊換成事件,把緊耦合拆成自治。

一場關於 Codetopia 未來的緊急對策會議,就此展開。

2) 術語卡 🧭

  • EDA(Event-Driven Architecture):一種以「事件」作為核心驅動力的架構,服務之間不再直接呼叫,而是透過發布與訂閱事件來溝通,達成鬆耦合。

  • Choreography vs Orchestration編舞 (Choreography) 就像舞會,大家聽到音樂(事件)各自起舞;編排 (Orchestration) 則像交響樂,需要一位指揮家(Orchestrator)來協調複雜的跨服務流程。

  • Outbox Pattern:一種確保「資料更新」與「事件發送」這兩件事要嘛都成功、要嘛都失敗的模式,避免系統精神分裂。

  • Idempotency Key(冪等鍵):一個獨特的識別碼,用來告訴系統:「嘿,這個操作我做過了,別再重複執行了!」是處理網路抖動的救星。

  • Saga(長交易):當一個跨多個服務的交易無法用傳統資料庫 ACID 搞定時,就用一系列「本地交易+補償動作」來維持最終的資料一致性。

  • DLQ(Dead-Letter Queue,死信佇列):處理失敗的事件一個有尊嚴的「安息之地」,而不是讓它們在系統裡像孤魂野鬼一樣到處亂竄。

3) 笑中帶淚 (反例/壞味道) 😭

讓我們倒帶回到災難現場,看看那些「經典」的壞味道設計是如何把開幕夜變成災難片的:

  1. 排隊直連地獄:入口的 GateApp 天真地以為,只要不斷直接呼叫 TicketingHTTP.verify(),總有一次會成功。結果 API 一逾時,它就更賣力地同步重試,像個執著的恐怖情人,最終把整個入口活活卡死。

  2. 支付重扣悲劇POST /pay 的請求裡,根本沒有冪等鍵這個概念。於是,在 4G 訊號的抖動下,市民每重送一次,系統就開心地再扣一次款。攤商老闆的笑容,逐漸母湯。

  3. 把資料庫當匯流排:有人覺得在 Orders 資料表上掛一個 AFTER INSERT 觸發器來「順便」發通知很聰明。結果呢?資料庫交易一回滾 (Rollback),觸發器也跟著惦惦,全城靜悄悄,彷彿什麼事都沒發生過。

  4. 一神控全域的恐懼:一個萬能的 NightMarketController 試圖掌控一切。無論收到什麼錯誤,它的 try/finally 區塊都只會一招:notify_everyone()。結果就是訊息亂序重複雪崩三合一大放送,天下大亂。

  5. 事件的濫用:把查詢訂單狀態這種需要即時回饋的操作,也包成事件丟進去排隊。「先生,您的訂單狀態我們正在處理中,請稍候五秒...」客人聽完,轉身就去隔壁攤了。

4) 王牌出手 (核心觀念/何時用/不適用) 👑

在 Helena 幾乎要拍桌的眼神壓力下,Theo 在白板上迅速勾勒出了解決方案的藍圖:

  • 打造城市事件總線 (City Event Bus):我們不再私下打電話!所有溝通都透過標準化的主題 (Topic) 進行,版本號直接包含在 Topic 中,例如 pay.txn.v1每一則事件都必須攜帶冪等鍵、關聯 ID (Correlation ID),以及明確的事件類型 (Event Type)

  • 協作優先,編排兜底:預設情況下,各個服務都是獨立自主的專家(「編舞」)。只有在處理像「實際發生重複扣款」這種需要跨服務補償的長交易時,才交給 Ivy|事件編排師 的 Orchestrator 服務擔任總指揮。

  • Outbox + Inbox 雙保險:發布端(Producer)採用 Outbox Pattern 確保交易與事件的原子性。消費端(Consumer)則採用 Inbox Pattern 進行去重,避免因為上游重播事件而導致副作用重複執行。

  • 優雅的失敗治理:面對失敗,我們不再手忙腳亂。統一的指數退避 (Exponential Backoff) + 抖動 (Jitter) 策略避免重試風暴。無法處理的事件會進入 DLQ,並觸發告警,等待人工介入。

  • 嚴謹的版本治理:Topic 包含版本號。事件 Schema 的演進規則是:「只許新增欄位、不許刪改舊欄位、新欄位必須有預設值或為可選」。舊的消費者可以繼續運行,不受影響。

  • 安全與隱私:冪等鍵與關聯 ID 嚴禁包含個資 (PII)。事件的 payload 應奉行最小化原則,並對敏感資料進行脫敏。

城市邊界與事件流向 (Context Map)

https://ithelp.ithome.com.tw/upload/images/20251013/20178500qV2PELeG8Q.png

何時用 (When to Use)

  • 當你需要解耦:當系統由多個獨立的服務組成,你希望它們能各自演化、部署,而不會因為一個服務的變動就導致其他服務跟著修改。

  • 追求高彈性與擴展性:當業務流量有明顯的波峰波谷,或未來可能新增許多未知的事件消費者時,事件驅動能讓系統像樂高一樣輕鬆擴展。

  • 處理非同步與長時間運行的任務:例如下單後的庫存、物流、通知流程,這些不需要立即同步完成的工作,最適合用事件來驅動。

何時不要用 (When NOT to Use)

  • 簡單的 CRUD 或請求-回應場景:如果你的業務就是一個簡單的「讀取資料 -> 回傳」,硬要套用 EDA 反而會把事情搞複雜。(殺雞焉用牛刀!)

  • 需要強一致性的原子交易:如果一個操作橫跨多個服務,而且必須「瞬間」完成,不可分割,傳統的分散式交易所可能更適合。EDA 追求的是「最終一致性」。

  • 團隊對非async與最終一致性還不熟悉時:EDA 的除錯與監控相對複雜,貿然導入可能會帶來比解決的問題更多的麻煩。

5) 導播切景 (三層並置圖) 🎥

導播,鏡頭拉一下!讓我們從三個不同的尺度,來看看這個新架構的樣貌。

閱讀順序:先看 ① 微觀的物件協作,再到 ② 中觀的事件流程,最後是 ③ 宏觀的角色協定。

視角 觀念/模式 在城市的說法
微觀 (GoF) Observer / State / Mediator 城市廣播台 / 交通號誌 / 調度中心
中觀 (EIP/EDA) Topic / Queue / Router / Saga 事件總線 / 訊息佇列 / 內容路由 / 長交易
宏觀 (MAS) DF / ACL 黃頁服務 / 代理通訊語言

5.1 微觀 (GoF)|事件觀察與調度 (Class Diagram)

https://ithelp.ithome.com.tw/upload/images/20251013/20178500SIUKImH4kb.png

5.2 中觀 (EDA 流程)|「重複扣款補償」編排兜底 (Sequence Diagram)

https://ithelp.ithome.com.tw/upload/images/20251013/20178500S3rAmoa4SB.png

Saga 退款活動圖 (Activity Diagram)

https://ithelp.ithome.com.tw/upload/images/20251013/201785001N2Lc9fB9U.png

事件族譜與版本演進 (Schema Evolution)

https://ithelp.ithome.com.tw/upload/images/20251013/20178500hgzbyQtfZR.png

5.3 宏觀 (MAS)|「黃頁 + 協作協定」

在宏觀的城市治理層面,市府的「黃頁服務 (DF)」會登錄所有具備處理 pay.*gate.* 等事件能力的代理(微服務)。各局處之間,則嚴格遵守「代理通訊語言 (ACL)」的協定來進行協作。

6) 最小實作 (準生產級 Python 風格) 💻

Talk is cheap, show me the code. 讓我們看看這套精修後的機制,如何區分「重送請求」與「重複扣款」。

from dataclasses import dataclass, field
from typing import Callable, Dict, Any
import uuid, time, random

# --- Helper functions for readability ---
def new_id() -> str: return str(uuid.uuid4())
def now() -> float: return time.time()

# 神經串連:版本號併入 Topic,Event 結構更精簡
@dataclass(frozen=True)
class Event:
    id: str
    topic: str # e.g. "pay.txn.v1"
    ts: float
    key: str
    type: str  # 'requested', 'captured', 'duplicate_captured', etc.
    payload: Dict[str, Any]
    correlationId: str | None = None
    causationId: str | None = None

# ... Bus 和 IdempotencyStore 的實作與前一版相同 ...
class Bus:
    def __init__(self): self.subs: Dict[str, list] = {}
    def publish(self, e: Event): print(f"🚌 BUS ('{e.topic}'): {e.type} | key={e.key}"); [h(e) for h in self.subs.get(e.topic, [])]
    def subscribe(self, topic: str, handler): self.subs.setdefault(topic, []).append(handler)
class IdempotencyStore:
    def __init__(self): self._seen = {}
    def seen(self, key: str) -> bool: return key in self._seen
    def mark(self, key: str, result: Any): print(f"멱 Idempotency: Marked '{key}'."); self._seen[key] = result
    def get_result(self, key: str): return self._seen.get(key)

bus, idem = Bus(), IdempotencyStore()

# --- 核心邏輯:區分「請求重送」與「重複扣款」 ---
def on_pay_requested(e: Event):
    """處理支付請求的 Handler,現在更穩健了"""
    # 補丁B:Handler 型別守門,避免處理非預期事件
    if e.type != "requested":
        return

    order_key = e.key  # 以 orderId 作為冪等鍵

    # 情況一:重送請求 (Idempotency Key 已存在)
    if idem.seen(order_key):
        print(f"🔄 PAYMENT: Replayed request for key '{order_key}'.")
        last_result = idem.get_result(order_key)
        bus.publish(Event(new_id(), "pay.txn.v1", now(), order_key, "result_replayed",
                          {"result": last_result}, e.correlationId, e.id))
        return

    # 情況二:全新請求
    is_real_duplicate, txn_id, orig_txn_id = check_and_capture_payment(order_key)

    if is_real_duplicate:
        # 情況三:真的發生了「重複扣款」
        print(f"‼️ PAYMENT: Duplicate CAPTURE detected for key '{order_key}'.")
        idem.mark(order_key, {"status": "duplicate_captured", "dupTxnId": txn_id})
        bus.publish(Event(new_id(), "pay.txn.v1", now(), order_key, "duplicate_captured",
                          {"dupTxnId": txn_id, "origTxnId": orig_txn_id},
                          e.correlationId, e.id))
    else:
        # 情況四:首次成功扣款
        print(f"✅ PAYMENT: Payment captured for key '{order_key}'.")
        idem.mark(order_key, {"status": "captured", "txnId": txn_id})
        bus.publish(Event(new_id(), "pay.txn.v1", now(), order_key, "captured",
                          {"txnId": txn_id}, e.correlationId, e.id))

# --- 模擬外部支付閘道與資料庫檢查 ---
captured_txns = {}
def check_and_capture_payment(order_key):
    if order_key in captured_txns:
        new_txn_id = f"TXN_{random.randint(9000,9999)}"
        return True, new_txn_id, captured_txns[order_key]
    else:
        new_txn_id = f"TXN_{random.randint(1000,8999)}"
        captured_txns[order_key] = new_txn_id
        return False, new_txn_id, None

# 補丁A:訂閱時 Topic 已包含版本號
bus.subscribe("pay.txn.v1", on_pay_requested)
# ... Orchestrator 訂閱 duplicate_captured 的邏輯在此省略 ...

6.1) Outbox 與 Inbox 模式示意 (Pseudo Code)

為了達到工業級的可靠性,光靠上面的程式碼還不夠。我們還需要 Outbox 和 Inbox 模式來保證事件的傳遞。

# --- 在 Producer (如 PaymentService) 的交易中 ---
def capture_payment_with_outbox(order_id, amount):
    with db.transaction(): # 在同一個資料庫交易內
        # 1. 執行核心業務邏輯
        orders.update_status(order_id, "CAPTURED")

        # 2. 將要發送的事件寫入 outbox 資料表
        outbox.insert({
            "id": new_id(), # 補丁C:明確事件主鍵,可以是 UUID 或 DB 自增
            "topic": "pay.txn.v1",
            "key": order_id,
            "type": "captured", "payload": {"amount": amount},
            "correlationId": get_current_correlation_id()
        })
    # 交易提交後,DB 和 outbox 的內容才會被確認

# --- 獨立的 Relay 服務:負責把 Outbox 的事件發出去 ---
def relay_process():
    while True:
        events_to_send = outbox.poll(batch=100)
        for event in events_to_send:
            try:
                bus.publish(to_event(event))
                outbox.mark_sent(event.id) # 發送成功後標記
            except TransientError:
                backoff_with_jitter(event.id) # 網路問題就稍後重試
        time.sleep(1)

# --- 在 Consumer (消費者) 端的 Handler ---
def payment_projection_handler(e: Event):
    # 1. 先檢查 Inbox,避免重複處理同一個事件
    if inbox.already_handled(e.id):
        return

    with db.transaction(): # 在消費者這邊也用交易包裹
        # 2. 標記事件已處理
        inbox.mark_handled(e.id)

        # 3. 才執行真正的副作用 (更新報表、通知...等)
        reporting_dashboard.update(e)

6.2) Outbox/Inbox 可靠訊息傳遞元件圖

https://ithelp.ithome.com.tw/upload/images/20251013/20178500y5bj6eoSJH.png

6.3) 消費端重試/退避狀態圖

https://ithelp.ithome.com.tw/upload/images/20251013/201785001LzWynIdeQ.png

6.4) 分區與有序性保證

https://ithelp.ithome.com.tw/upload/images/20251013/20178500lfpca2iGuP.png

7) 反模式紅旗 🚩

在推動事件驅動架構時,請務必留意這些飄揚的紅色警示旗:

  • 把同步 RPC 塗成非同步的樣子:只是把一個 HTTP 呼叫包在事件裡,然後焦急地等待回應事件,這不是 EDA,這是自欺欺人。

  • 沒有冪等鍵就敢談重放:在沒有冪等設計的情況下重播死信佇列裡的事件,不是在解決問題,是在重演災難

  • 一隻「中央編排神獸」:如果所有事件都要經過一個中央 Orchestrator,恭喜你,你只是把緊耦合換了個地方塞回去而已。

  • 以資料庫觸發器代替真正的事件匯流排:交易一回滾,事件就人間蒸發,這會讓你的系統失去最重要的「事實根據」。

  • 對事件版本放任不管:今天改個欄位,明天換個格式,卻不升級版本號。很快,你的舊服務就會像看不懂新公文一樣,集體失語。

  • 指望 exactly-once:補丁D:在分散式系統中是神話;請用 at-least-once + 冪等鍵 + 補償,並以 DLQ/觀測守住邊界。

8) 城市望遠鏡 (升維) 🔭

從今天的夜市混戰中,我們可以把視野拉得更高更遠:

  • 與六角架構的完美結合:事件的發布與接收,正是發生在六角架構的「邊界」上。事件驅動的 Adapter 讓我們的核心業務邏輯(Domain)保持純淨,這完美地銜接了 Day 28 的「邊界保護」精神。

  • 從微服務到多代理系統:如果我們把每個微服務看作一個具備特定「能力」的「代理 (Agent)」,那麼事件就是它們之間溝通的「語言 (ACL)」。整個 Codetopia 就演變成一個大型的多代理協作網路。

  • 補丁E:事件進出都在 Adapter 層,Domain 維持純淨(六角依賴只指向核心),Day 29 的事件語言才不會滲進 Day 28 的核心領域。

9) ✅ 回到現場 (同一組驗收)

會議結束,天已微亮。在 Theo 的架構指導與團隊的連夜奮戰後,蒼螺夜市第二天,迎來了新生:

  • 入口不相容?解決! gate.auth.v2 事件被順利發布。一個 Adapter 服務負責轉譯,確保新舊掃描器都能訂閱到自己看得懂的格式。

  • 重複扣款?解決! 支付服務現在有了 Idempotency-KeyOutbox/Inbox 護體,能從容應對。一旦偵測到真正的「重複扣款」,就會觸發 Saga 流程完成退款。

  • 用電跳脫?解決! 電力負載調度員 Sable 能監聽到 ops.power.v1 事件,並發出 Power.Shed 指令,實施智慧電力調度。

  • 市服壅塞?解決! 市服專線隊長 Yuan 將所有通報轉換成標準化事件,由系統自動路由給權責單位。

蒼螺夜市,終於迎來了它應有的秩序與繁華。

10) 測試指北 🧪

要確保這套複雜的系統能穩定運行,測試策略至關重要:

  • 合約測試 (Contract Testing):事件的 Schema 就是服務之間的「契約」。任何會破壞這個契約的變更,都應該在 CI/CD 階段就被攔截下來。

  • 冪等性測試:寫個測試案例,用同一個 key 調用你的服務 N 次,然後斷言:最終的狀態只應該被改變一次。

  • 重試/退避策略測試:利用測試時鐘 (Test Clock) 來驗證你的指數退避策略(包含抖動)是否如預期般,一次比一次等待更久。

  • DLQ 驗證:在測試中故意注入一個「有毒」的事件,驗證它在重試幾次後,是否真的被送進了死信佇列,並且觸發了監控告警。

  • 契約測試(Schema/Topic 版本):對 pay.txn.v1 的 Schema 做「只增不改」的相容性檢查;任何破壞性變更(刪/改欄位)的 Pull Request 都會自動失敗。

  • 退避階梯可觀測性:以假時鐘驗證 backoff 序列(含 jitter)符合預期;並驗證有毒訊息在達到重試上限後,必定落入 DLQ 且產生告警。

  • Partition 有序性測試:同一 key 重放 N 次,驗證處理順序與冪等。

11) 鄉民出題 (動手+反模式紅旗) 🗳️

身為總設計師的你,來動動腦吧!

  1. 回顧一下「笑中帶淚」章節那個用資料庫觸發器來發事件的「天才」設計。如果你是當時的英雄,你會如何利用 Outbox Pattern 來重構它?請用幾句話描述你的改造步驟。

  2. (二選一) 假設資源有限,你必須在夜市重新開張前,決定優先上線的策略。你會選哪個?為什麼?(請在 30 字內說明)

    • A. 只上 Outbox + 冪等鍵,確保支付不丟失、不重複,暫時不做自動退款的 Orchestrator。

    • B. 先上 Orchestrator,用它來攔截並處理所有重複扣款的爛攤子。

12) 二十字摘要 & 明日預告

  • 摘要:把排隊換成事件,讓自治服務以冪等鍵和補償舞出秩序。

  • 明日預告(Day 30):AI 多代理華麗登場,我們將把今天的事件,升格為更智慧的協作協定能力路線圖


附錄:ASCII 版圖示

為了確保在不支援 Mermaid 渲染的環境中也能正常閱讀,以下提供文中圖表的 ASCII 替代版本:

A.1 城市邊界與事件流向 (Context Map)

┌──────────────┐    ┌──────────────┐    ┌──────────────┐    ┌──────────────┐
│  Ticketing   │    │   Payments   │    │  Power Ops   │    │ Civic Hotline│
│     BC       │    │     BC       │    │     BC       │    │     BC       │
├──────────────┤    ├──────────────┤    ├──────────────┤    ├──────────────┤
│ GateService  │    │PaymentService│    │ PowerService │    │ CivicRouter  │
│              │    │     Ivy      │    │              │    │              │
│              │    │ Orchestrator │    │              │    │              │
└──────┬───────┘    └──────┬───────┘    └──────┬───────┘    └──────┬───────┘
       │                   │                   │                   │
       └───────────────────┼───────────────────┼───────────────────┘
                           │                   │
                    ┌──────▼───────────────────▼──────┐
                    │        City Event Bus           │
                    ├─────────────────────────────────┤
                    │ gate.auth.v2                    │
                    │ pay.txn.v1                      │
                    │ ops.power.v1                    │
                    │ civic.events.v1                 │
                    └─────────────────────────────────┘

A.2 重複扣款補償流程

GateService    Bus    PaymentService    Orchestrator
     │          │           │               │
     ├─ publish ─┤           │               │
     │  gate.   │           │               │
     │  auth.v2 │           │               │
     │          │◄─subscribe─┤               │
     │          │           │               │
     │          │           ├─ publish ─────┤
     │          │           │  pay.txn.v1   │
     │          │           │ "requested"   │
     │          │           │               │
     │          │           ├─ publish ─────┤
     │          │           │  pay.txn.v1   │
     │          │           │ "captured"    │
     │          │           │               │
     │          │           ├─ publish ─────┤
     │          │           │  pay.txn.v1   │
     │          │           │"duplicate_    │
     │          │           │ captured"     │
     │          │           │               │◄─ monitor
     │          │           │               │
     │          │           │◄─ command ────┤
     │          │           │   refund      │
     │          │           │               │
     │          │           ├─ publish ─────┤
     │          │           │  pay.txn.v1   │
     │          │           │ "refunded"    │

A.3 Outbox/Inbox 可靠訊息傳遞

Producer Service              Event Bus               Consumer Service
┌─────────────────┐          ┌──────────┐             ┌─────────────────┐
│   Domain Tx     │          │ Topic:   │             │  Inbox Table    │
│                 │          │pay.txn.v1│             │                 │
│        │        │          │          │             │        │        │
│        ▼        │          │    ┌─────┤             │        ▼        │
│ ┌─────────────┐ │          │    │     │             │   ┌─────────┐   │
│ │ Outbox      │ │   ────── │    │     │  ──────────▶│   │Handler  │   │
│ │   Table     │ │  publish │    │     │   consume   │   │         │   │
│ └─────────────┘ │          │    │     │             │   └─────────┘   │
│        │        │          │    └─────┤             │        │        │
│        ▼        │          │          │             │        ▼        │
│ ┌─────────────┐ │          │   ┌────┐ │             │ ┌─────────────┐ │
│ │Outbox Relay │ │          │   │DLQ │ │             │ │Side Effects │ │
│ └─────────────┘ │          │   └────┘ │             │ └─────────────┘ │
└─────────────────┘          └──────────┘             └─────────────────┘

A.4 Saga 退款狀態機

    [*]
     │
     ▼
┌─────────────┐
│  Requested  │
└─────┬───────┘
      │
      ├──capture ok──┐
      │              ▼
      │         ┌─────────────┐
      │         │  Captured   │ ──────── [*]
      │         └─────────────┘
      │
      └──detected by gateway──┐
                              ▼
                   ┌─────────────────────┐
                   │ DuplicateCaptured   │
                   └─────────┬───────────┘
                             │
                             ▼
                   ┌─────────────────────┐
                   │ RefundCommanded     │
                   └─────────┬───────────┘
                             │
                             ▼
                   ┌─────────────────────┐
                   │   Refunded          │ ──────── [*]
                   └─────────────────────┘

A.5 事件分區與有序性保證

Event Sources                 Event Bus (Partitioned)         Consumers

orderId=A1 ──hash(A1)──┐    ┌─────────────┐                 ┌──────────────┐
orderId=A2 ──hash(A2)──┼───▶│ Partition 0 │ ───────────────▶│ A1,A2 事件   │
orderId=A3 ──hash(A3)──┘    └─────────────┘                 │ 保證局部順序  │
                                                             └──────────────┘
orderId=B1 ──hash(B1)──┐    ┌─────────────┐                 ┌──────────────┐
orderId=B2 ──hash(B2)──┼───▶│ Partition 1 │ ───────────────▶│ B1,B2 事件   │
orderId=B3 ──hash(B3)──┘    └─────────────┘                 │ 保證局部順序  │
                                                             └──────────────┘
orderId=C1 ──hash(C1)──┐    ┌─────────────┐                 ┌──────────────┐
orderId=C2 ──hash(C2)──┼───▶│ Partition 2 │ ───────────────▶│ C1,C2 事件   │
orderId=C3 ──hash(C3)──┘    └─────────────┘                 │ 保證局部順序  │
                                                             └──────────────┘

A.6 消費端重試/退避狀態圖

                    [*]
                     │
                     ▼
               ┌──────────┐
               │ Handling │
               └────┬─────┘
                    │
          ┌─────────┼─────────┐
          │         │         │
       success   transient  non-retryable
          │      error       or max retry
          ▼         │              │
    ┌──────────┐    ▼              ▼
    │  Acked   │ ┌──────────┐  ┌──────────┐
    └──────────┘ │RetryWait │  │   DLQ    │
          │      └────┬─────┘  └────┬─────┘
          │           │             │
          │      backoff+jitter     │
          │           │             │
          ▼           └─────────────┘▼
         [*]                       [*]

A.7 事件版本演進架構

PayTxnV1 (pay.txn.v1)
┌─────────────────────────────┐
│ + topic: "pay.txn.v1"       │
│ + type: requested|captured| │
│        duplicate_captured|  │
│        refunded             │
│ + key: orderId              │
│ + correlationId: UUID       │
│ + causationId: UUID         │
│ + payload.txnId?: string    │
│ + payload.origTxnId?: str   │
│ + payload.dupTxnId?: str    │
└─────────────────────────────┘
                │
                │ backward compatible
                │ (add-only)
                ▼
PayTxnV2 (pay.txn.v2)
┌─────────────────────────────┐
│ (繼承所有 V1 欄位)           │
│ + payload.amount?: number   │ ◄── 只增欄位
│ + payload.currency?: string │ ◄── 向後相容
└─────────────────────────────┘

上一篇
Day 28:六角架構:當「乾濕分離」的衛浴設計,拯救了瀕臨崩潰的開幕夜!
系列文
Codetopia 新手日記:設計模式與原則的 30 天學習之旅29
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言