🎯 什麼情境該想到我

當你「想透過訊息叫另一個系統去執行某個特定動作,等於把一次方法呼叫非同步化」的時候。

⚙️ 怎麼用(步驟 / 公式)

意圖:用命令訊息可靠地請另一個應用執行某個程序或動作。

做法要點:

  • 把「要呼叫的操作」與「所需參數」一起包進訊息本體,形同把一個 Command 物件序列化後非同步送出。
  • 通常送往 Point-to-Point Channel(點對點通道),確保只有一個接收者拿到並執行一次。
  • 接收端解讀訊息內容後,執行對應的動作。

★★ 命令的三條紀律(違反任一條就會出事)

① ★ 恰好一個接收者
   命令發到 [[發布訂閱通道]] → 兩個服務都執行 → ★ 寄兩封信、扣兩次款
   → 命令一律走 [[點對點通道]]

② ★ 命名用祈使句
   CreateOrder / SendEmail / ReserveStock   ← 命令
   OrderCreated / EmailSent / StockReserved ← ★ 那是事件,不要混

③ ★★ 接收者可以拒絕
   命令是「請求」,不是「事實」。驗證失敗、權限不足、狀態不對 → 都該能拒絕
   → ★ 所以命令通常需要一條回覆路徑(見 [[請求-回覆]]、[[回覆位址]])

★ Go 的命令訊息設計

type ReserveStock struct {
    CommandID string `json:"command_id"`   // ★★ 冪等鍵:同一個命令重送只生效一次
    OrderID   string `json:"order_id"`
    SKU       string `json:"sku"`
    Qty       int    `json:"qty"`
    IssuedBy  string `json:"issued_by"`    // ★ 誰下的命令(稽核)
    IssuedAt  time.Time `json:"issued_at"`
}

★ CommandID 不是可選的。命令有副作用,而投遞保證是「至少一次」
⇒ 同一個命令一定會重複到達(見 冪等接收者)。

// ★ 接收端:用唯一索引把冪等做在資料庫層,不要用「先查再寫」
_, err := tx.Exec(ctx,
    `INSERT INTO processed_commands (command_id) VALUES ($1)`, cmd.CommandID)
if isUniqueViolation(err) {
    return nil                      // ★ 處理過了,直接成功
}
// ★ 然後在同一個交易裡做真正的業務動作 —— 兩者必須原子

⚠️ 「先 SELECT 看有沒有處理過、再 INSERT」在併發下會失敗——
兩個 goroutine 同時查都說沒有。唯一索引 + 捕捉衝突才是正確做法
(見 工具-交易與隔離等級)。

★★ 命令 vs 事件:什麼時候該用哪一個

用命令用事件
發送者關心結果✅❌
接收者數量恰好一個零到多個
加一個新的下游★ 要改發送者★ 不用改任何東西
耦合方向★ 發送者知道「要誰做什麼」發送者只宣告「發生了什麼」

★★ 判準:發送者在不在乎「有沒有人處理、處理成功沒」?
在乎 → 命令。不在乎 → 事件。

★ 常見的設計陷阱:用事件來「觸發」動作。
發 OrderPlaced,期望某個服務會寄確認信 →
這是把命令偽裝成事件。表面上解耦了,實際上你仍然依賴「有人在聽」,
但沒有任何機制保證這件事(沒訂閱者 = 靜默不發生,沒有錯誤)。
→ ★ 若「必須有人做」,就發命令;若「誰想反應誰反應」,才發事件。

★ 用訊息送命令 vs 直接 RPC

選訊息選 遠端程序呼叫
★ 可以稍後執行(寄信、產報表、批次)★ 呼叫端需要結果才能繼續
★ 接收端可能離線/忙碌,需要削峰延遲預算很緊
★ 需要重試與 DLQ 的完整機制錯誤要立即回饋給使用者
有排隊語義(任務佇列)—

★ 判準:呼叫端能不能先回應使用者「已收到,稍後處理」?
能 → 訊息;不能 → RPC。

★ 兄弟辨析

差別
事件訊息★ 「已發生」的事實,零到多個接收者,不可拒絕
文件訊息「這是資料」,不含意圖
命令模式★ 行程內的同一想法(把請求物件化、可排隊、可重放、可 undo)

🧪 我實際套用的紀錄

  • (待填)

⚠️ 注意 / 什麼時候不適用

  • 接收端必須知道如何解讀並執行該命令,兩端存在語意耦合。

  • 一般用點對點通道,避免同一命令被多方各執行一次。

  • 非同步下無法立即取得結果,若需回應要搭配請求-回覆 / 回覆位址取回。

  • ★★ 命令一律走點對點通道。發到 pub/sub → 兩個服務都執行。

  • ★★ 命令必須帶冪等鍵(CommandID)。至少一次投遞 ⇒ 一定會重複。

  • ★ 冪等要用唯一索引 + 捕捉衝突,不要「先查再寫」——併發下會失敗。

  • ★ 不要用事件來「觸發」動作。若「必須有人做」就發命令;沒訂閱者的事件是靜默不發生,沒有錯誤。

  • ★ 命令的命名用祈使句。用過去式命名等於宣稱它是事實。

  • ★ 命令可以被拒絕,所以通常需要回覆路徑(請求-回覆/回覆位址)。

  • 加一個新下游要改發送者,是命令的固有特性,不是設計缺陷——但要知道你付了這個代價。

  • 不要在命令裡放「怎麼做」。ReserveStock 而不是 UpdateStockTableSetQtyMinus3——後者把接收端的實作變成契約。

🔗 相關工具