🎯 什麼情境該想到我
當你「想透過訊息叫另一個系統去執行某個特定動作,等於把一次方法呼叫非同步化」的時候。
⚙️ 怎麼用(步驟 / 公式)
意圖:用命令訊息可靠地請另一個應用執行某個程序或動作。
做法要點:
- 把「要呼叫的操作」與「所需參數」一起包進訊息本體,形同把一個 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。
★ 兄弟辨析
🧪 我實際套用的紀錄
- (待填)
⚠️ 注意 / 什麼時候不適用
-
接收端必須知道如何解讀並執行該命令,兩端存在語意耦合。
-
一般用點對點通道,避免同一命令被多方各執行一次。
-
非同步下無法立即取得結果,若需回應要搭配請求-回覆 / 回覆位址取回。
-
★★ 命令一律走點對點通道。發到 pub/sub → 兩個服務都執行。
-
★★ 命令必須帶冪等鍵(
CommandID)。至少一次投遞 ⇒ 一定會重複。 -
★ 冪等要用唯一索引 + 捕捉衝突,不要「先查再寫」——併發下會失敗。
-
★ 不要用事件來「觸發」動作。若「必須有人做」就發命令;沒訂閱者的事件是靜默不發生,沒有錯誤。
-
★ 命令的命名用祈使句。用過去式命名等於宣稱它是事實。
-
加一個新下游要改發送者,是命令的固有特性,不是設計缺陷——但要知道你付了這個代價。
-
不要在命令裡放「怎麼做」。
ReserveStock而不是UpdateStockTableSetQtyMinus3——後者把接收端的實作變成契約。
🔗 相關工具
- 文件訊息 Document Message(傳「資料本體」而非「要做的動作」)
- 事件訊息 Event Message(通知「發生了什麼」而非「請去做什麼」)
- 請求-回覆 Request-Reply(命令若需回應結果時搭配)
- 企業整合模式
- 事件訊息、文件訊息 —— 另外兩種語義
- 點對點通道 —— 命令的正確通道
- 請求-回覆、回覆位址 —— 命令的回覆路徑
- 冪等接收者 —— ★ 命令的必備配套
- 遠端程序呼叫 —— 對照:需要立即結果時
- 命令模式 —— 行程內的同一想法
- 工具-交易與隔離等級 —— 唯一索引做冪等