海外雲在線 海外雲在線 立即諮詢

騰訊雲國際企業帳號 騰訊雲消息對列 CMQ 訊息重複消費與冪等性處理技巧

騰訊雲國際 / 2026-08-03 20:11:13

先搞清楚:CMQ 為什麼會重複消費

使用騰訊雲消息隊列 CMQ 時,很多人第一次遇到重複消費,直覺反應是系統有問題。其實大多數情況下,這不是故障,而是訊息系統的正常行為。CMQ 走的是高可用、可重試的設計路線,通常保證的是「至少投遞一次」,而不是「剛好投遞一次」。只要是至少一次,就代表同一條消息有機會被多次送到消費端。

重複的來源很多。最常見的是消費者處理成功了,但還沒來得及回應確認,程序就掛掉了;也可能是網路抖動、超時重試、容器重啟、服務擴縮容,甚至是消費端自己主動拉取重試。從訊息系統的角度看,只要它無法百分之百確定你已經成功處理,就會傾向於再次投遞。這種設計的目的很簡單:寧可多送,也不要丟。

所以,處理 CMQ 時最重要的一個觀念不是「如何避免重複」,而是「如何讓重複無害」。只要業務層能保證冪等,同一條消息來一百次,結果也只能落一次,系統就穩了。

冪等性不是不重複,而是重複也只生效一次

很多人把冪等性理解成「不要重複執行」,其實更準確的說法是:相同的請求或消息,無論執行多少次,最終對外呈現的結果都一致。這個一致,不只是資料不重複,還包括狀態不亂、金額不錯、流程不跳步。

舉個例子。假設一條訂單支付成功的消息被消費兩次,如果你的邏輯只是簡單地把訂單狀態從待支付改成已支付,第二次其實可能不會出錯,因為狀態本來就已經是已支付。但如果這條消息還會觸發庫存扣減、積分發放、發券、發短信,那第二次就可能把庫存扣兩次、積分加兩次,麻煩會很大。這就是為什麼冪等不是一句口號,而是整個業務鏈路都要配合的設計。

真正可靠的冪等,通常建立在業務唯一鍵上,而不是建立在「當前這次消息看起來像不像重複」。你要先回答一個問題:這條消息對業務來說,唯一標識是什麼?是訂單號、支付單號、發貨單號,還是某個事件編號?只要唯一鍵定對了,後面的去重才有意義。

先定唯一鍵,再決定去重位置

做冪等設計,第一步不是寫代碼,而是把消息對應的業務邏輯拆清楚。不同場景,唯一鍵不一樣,去重位置也不一樣。很多系統失敗,不是因為沒做去重,而是去重做在了錯的地方。

常見的唯一鍵選擇

對下單類消息,通常用訂單號作為唯一鍵;對支付回調類消息,用支付單號或交易流水號;對通知類消息,則可以用事件類型加業務編號組合成唯一鍵。原則很簡單:只要能代表這次業務動作唯一發生過一次,就拿來做冪等依據。

騰訊雲國際企業帳號 很多團隊會誤用 CMQ 的消息 ID 作為去重依據,這其實不夠穩。消息 ID 只能標識一條具體消息,不一定能代表一筆業務。真正麻煩的是,同一筆業務可能因為上游重試、程序補發、人工補單而產生多條內容相同但消息 ID 不同的訊息。這種情況下,只靠消息 ID 去重,等於沒有去重。

去重可以放在哪裡

去重通常有四個位置:入口處、處理前、處理中、處理後。入口處做得越早,越省資源,但判斷條件也要越準;處理後去重雖然簡單,卻可能讓前面的業務動作已經執行了一半。最穩妥的做法通常是把冪等校驗和核心業務更新放在同一個事務裡,讓兩件事要嘛一起成功,要嘛一起失敗。

幾種最常用、也最實用的冪等方案

方案一:資料庫唯一索引去重

這是最直接、也最容易落地的方法。你先為某個業務唯一鍵建立唯一索引,例如訂單號、事件號、支付流水號等,當消息到達後,先嘗試插入一條處理記錄。如果插入成功,表示這次消息第一次到達,可以繼續做後續業務;如果因為唯一約束失敗,說明這條消息之前已經處理過,直接忽略即可。

這種方式的優點是清楚、可靠、可追蹤,數據也容易查。缺點是高併發下會有寫衝突,另外還要考慮業務表和去重表如何保持一致。最好的方式,是把去重記錄與業務更新放進同一個資料庫事務中,避免出現「去重記錄已寫入,但業務只做了一半」的尷尬情況。

例如,訂單支付成功後,你可以先寫入一條支付事件處理記錄,再把訂單狀態改成已支付,兩者同時提交。如果中途任何一步失敗,整個事務回滾,下次同一消息重試時還能重新進來處理。這樣,重複消息就不會把系統推到半成功的狀態。

方案二:狀態機加條件更新

對於狀態型業務,狀態機是非常好用的冪等工具。比如訂單從待支付到已支付、已支付到已發貨,每一步都應該有明確的前置狀態。消費消息時,不是簡單地覆蓋狀態,而是帶條件更新:只有當當前狀態等於某個值時,才允許轉換。

這種做法的好處是自然冪等。假設某條支付成功消息已經把訂單改成已支付,第二次重放時,條件更新會因為當前狀態不符合而失敗,從而避免重複扣減庫存、重複發券或重複記賬。對業務來說,這種失敗不需要當成異常,而應該當成「這條消息已經處理過」的正常結果。

如果你的業務流程比較長,例如先鎖庫存、再扣庫存、再更新訂單、再寫流水,狀態機尤其重要。它能把每個步驟都固定在有限狀態中,避免重複消息把流程重新跑一遍。

方案三:處理表加處理狀態

有些任務不是一兩行 SQL 就能完成,可能還要調外部服務、發起多個內部操作,執行時間較長。這時候可以設計一張處理表,裡面記錄消息唯一鍵、處理狀態、開始時間、完成時間、失敗原因等資訊。消息到達後先嘗試把狀態從未處理改成處理中,成功後再進行業務操作,完成後改成已處理。

這種方式的重點是要有狀態流轉,不能只有一個「是否處理過」的布林值。因為如果程式在處理中途崩潰,布林值只能告訴你它不是成功就是失敗,但無法判斷它是不是卡在半路。加上處理中狀態後,你就可以做超時補償:如果某條消息長時間停在處理中,說明前一次執行可能已失敗,這時候再觸發重試更安全。

方案四:Redis 輔助去重

騰訊雲國際企業帳號 在高吞吐場景下,Redis 常被用作第一層去重。做法通常是把業務唯一鍵寫進一個 setnx 或相似機制裡,若寫入成功則代表第一次出現,若失敗就直接跳過。這種方式效率高,適合短時間內的重複抑制。

但要注意,Redis 更適合做輔助,不適合單獨承擔最終冪等責任。原因很簡單:Redis 的 key 可能過期,節點故障後也可能丟數據。如果你的業務是扣款、發貨、改帳戶餘額,最後還是要落到資料庫或核心業務表做二次確認。更實際的做法是,Redis 擋住大量短時間重複流量,資料庫負責最終正確性,兩層一起用,既快又穩。

一套標準的 CMQ 消費流程,應該長什麼樣

如果把冪等處理放進完整消費流程,可以把思路簡化成幾步。第一步,消費端收到消息後,先解析出業務唯一鍵,不要急著做任何破壞性操作。第二步,做冪等校驗,可以是插入處理表、更新狀態機,或先查後寫的條件控制。第三步,執行核心業務,例如更新訂單、扣庫存、發積分。第四步,在同一事務或同一原子邏輯中完成提交。第五步,確認消息處理成功,再向 CMQ 返回成功信號。

這裡最容易犯的錯,是先確認消息成功,再去處理業務。這樣一旦服務在業務執行中途掛掉,CMQ 會以為這條消息已經處理完畢,不再投遞,結果真正的業務反而丟了。相比之下,先把業務做完,再確認消息,雖然可能帶來重複投遞,但至少不會丟單。對消息系統而言,重複是可控的,丟失才是致命的。

最容易踩的幾個坑

只用消息 ID 做去重

前面已經提過,消息 ID 只能代表一條消息,不一定代表一次業務。真正該去重的是業務行為,而不是隊列裡那個臨時標識。尤其是上游補發、人工補單、跨系統重放時,同一業務可能有多個不同消息 ID,這時候只看消息 ID 等於看錯了對象。

先確認再處理

這是高風險做法。消息一旦被確認,系統通常不會再幫你補發。只要業務還沒真正完成,就不應該讓消息退出可重試範圍。正確順序永遠是先處理、再確認。

去重表永遠只增不刪

很多人把去重表做出來後,忘了設計清理策略。時間一長,表會越來越大,查詢越來越慢,備份和維護成本也跟著上升。比較成熟的做法,是根據業務需要做分區、按天或按月歸檔,或者只保留一段合理的冪等窗口。對於永久性唯一約束的關鍵業務,可以保留核心鍵;對於通知、統計這類可接受時間窗口去重的場景,則不必無限增長。

把所有重試都當成異常

在消息系統裡,重試本身不是錯誤,而是保險機制。真正要關注的是重試次數是否異常、處理耗時是否異常、重複率是否異常升高。與其在程式裡看到重複就報警,不如把重複當成正常流程的一部分,重點監控那些超出預期的情況。

沒有補償機制

冪等只能保證重複不造成更多傷害,但不能自動修復所有失敗。例如一條消息在更新訂單後,還沒來得及發通知就崩潰了。下次重放時,如果你的冪等邏輯直接判定整條消息已處理,就可能把通知也一起跳過。這種情況就需要把業務拆成更細的步驟,或者加入補償機制,讓每一步都有獨立狀態可追蹤。

不同業務場景,冪等做法也要跟著變

通知類業務

短信、站內信、郵件這類業務,通常允許一定程度的重複,但不能無限重複。比較常見的做法,是按事件編號加通知類型做唯一鍵,同一事件在一段時間內只發一次。若需要人工重發,也應該有顯式的重發標識,避免跟正常流量混在一起。

扣款與下單類業務

這類場景最敏感,要求最嚴。冪等鍵一定要落在業務單號上,並且所有涉及金額變更、庫存變更、訂單狀態變更的操作都要同時受控。只要有一項沒做冪等,整個鏈路都可能出問題。這時候不要嫌麻煩,寧可多寫幾個狀態和校驗,也不要把核心交易邏輯寫成一次性的腳本式處理。

統計與埋點類業務

統計類業務對一致性的要求相對低一些,但對數據量和吞吐要求更高。這類消息可以接受一定時間窗口內的重複去重,甚至可以用批次聚合的方式來做。重點不是每條都完美,而是整體統計足夠接近真實值,並且不會因為重複消息導致數據長期偏移。

一個比較穩的落地方案

騰訊雲國際企業帳號 如果你現在就要在 CMQ 上做一套可上線的消費方案,我建議你按下面的思路來。先在生產端為每個業務事件生成穩定的唯一鍵,這個鍵要能跨重試、跨補發、跨人工操作保持不變。消費端收到消息後,先解析唯一鍵,再進行冪等校驗。對核心交易類業務,優先採用資料庫唯一索引加事務的方式;對長流程任務,增加處理中狀態和超時補償;對高吞吐但非核心場景,Redis 可以做前置擋板,但最終還是要有資料庫落地。

另外,最好把重複消息的監控也一起做起來。不是說有重複就代表有問題,而是當重複率突然升高、某個業務鍵重放次數異常、某個處理狀態長時間卡住時,你能第一時間看到。很多線上事故不是因為沒有冪等,而是因為冪等做了,卻沒人知道哪裡卡住了。

結語:把重複當成常態,系統才真的穩

訊息隊列的價值,不只是幫你解耦和削峰,更重要的是讓系統面對不穩定時仍然能向前走。CMQ 的重複消費不是缺陷,而是高可用設計的副作用。真正成熟的做法,不是幻想消息永遠只來一次,而是承認它可能來很多次,然後把每一次都變成安全的重試。

當你把業務唯一鍵、資料庫唯一約束、狀態機、事務邊界和補償機制一起設計好之後,重複消息就不再是麻煩,而只是一次普通的重放。到了那時候,你對消息系統的掌控感,才算真正建立起來。

Telegram售前客服
客服ID
@cloudcup
联系
Telegram售后客服
客服ID
@yanhuacloud
联系