iT邦幫忙

2025 iThome 鐵人賽

DAY 17
1
Cloud Native

Go 語言搶票煉金術:解鎖千萬級併發下的原子交易奇蹟系列 第 17

Go 語言搶票煉金術 Day 17 - 選擇你的佇列:Redis Streams vs RabbitMQ Kafka

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20250915/20124462YARMcXUyfa.png

Day 17 - 選擇你的佇列:Redis Streams vs RabbitMQ Kafka

上一篇,我們認定了「非同步」是目前的正解。

但接下來的決定,是許多資深工程師都曾面臨的難題:你該選擇哪個消息佇列 (Message Queue)?

網路上充滿了對 Redis Streams、RabbitMQ 和 Kafka 的 QPS、延遲等性能比較,但在搶票這個場景,它們沒有完全命中要害。

真正的問題是:如何在你選擇的架構下,維護「扣減庫存」和「創建訂單消息」這兩個步驟的原子性?

選擇了不同的工具,就要承受不同的後果。今天,我們不比較工具,我們比較詛咒。

困境:原子性 vs. 分散式一致性

回憶一下,我們的生命線是一個 Lua 腳本,它能把 DECR(扣庫存)和 XADD(發消息到 Redis Stream)綁定在一個絕對原子的操作裡。
這是我們能擁有的、最乾淨、最優雅的解決方案。

一旦想決定換成 RabbitMQ 或 Kafka,就等於放棄了這個天然的原子性保證。程式碼會變成類似這樣:

// 這是災難的開始
func purchaseHandler(...) {
    // 步驟 1: 扣減 Redis 庫存
    err := redisClient.Decr(ctx, "ticket_stock").Err()
    if err != nil { /* ... */ }

    // <-- 如果你的服務在這裡崩潰了呢? -->

    // 步驟 2: 發送消息到 RabbitMQ
    err = rabbitClient.Publish("orders", orderMessage)
    if err != nil {
        // 發送失敗了怎麼辦?把 Redis 的庫存加回去?
        // 如果加回去又失敗了呢?
    }
}

我們會在兩個獨立的系統(Redis 和 RabbitMQ)之間創造了一個不一致的窗口

處理這個問題,比處理 Redis 故障轉移那幾秒的數據丟失,要複雜一萬倍。

所以,我們的選擇題變成了:

  • 選項 A:接受 Redis Streams 的原子性,同時承擔它在極端故障下(主從切換)可能丟失數據的風險。

  • 選項 B:追求 RabbitMQ/Kafka 的訊息可靠性,同時承擔「庫存已扣、消息未發」這種業務邏輯層面的數據不一致風險。

這才是我們今天要做的真正權衡。

選項 A:Redis Streams 的優勢與風險

選擇 Redis Streams,就是選擇了極致的性能和完美的原子性。使用我們在 Day 14 和 Day 15 設計的 Lua 腳本,DECRXADD 在 Redis 服務端被當作一個單一、不可分割的操作執行。這意味著絕不會出現庫存扣了、消息卻沒發出去的情況。這非常誘人。

但代價是什麼?

代價是 Redis 的持久化策略。
預設情況下,Redis 的主從複製是非同步的。如果主節點在寫入資料後、但還沒來得及將資料同步到從節點時就徹底掛掉,此時若發生故障切換,那麼這幾秒內的所有寫入(包括我們的訂單消息)都會永久丟失

這不是猜測,這是必然。但這個風險並非無法管理:

  1. 加強監控與維運:建立穩健的 Redis Sentinel 或 Cluster,縮短故障發現和切換的時間。

  2. 同步複製選項:使用 Redis 的 WAIT 命令,可以強制要求寫入操作必須同步到指定數量的從節點後才返回。這會犧牲一點延遲,但能換來更強的數據一致性保證。

  3. 業務補償機制:建立後續的對帳系統。既然風險只在極端情況下發生,我們可以設計一個補償流程來找回丟失的訂單,而不是在主路徑上增加複雜性。

選項 B:RabbitMQ/Kafka 的代價與其複雜性

選擇 RabbitMQ 或 Kafka,意味著你把賭注押在了訊息佇列本身的可靠性上。

它們都是為可靠傳遞而生的專業系統,透過持久化、多副本、發送方確認等機制,能做到訊息「At-Least-Once Delivery」(至少一次成功傳遞)。

但代價是什麼?

如前所述,代價是原子性的徹底喪失
把一個簡單的資料庫原子性問題,升級成了一個棘手的分散式交易問題
為了彌補那個「不一致的窗口」,我們必須在應用程式中引入更複雜的設計模式,例如:

  • Transactional Outbox 模式:將「扣減庫存」和「寫入本地訂單消息表」放在一個本地資料庫交易中完成。然後再由另一個獨立的進程去掃描這張表,把消息轉發到 RabbitMQ。
    • 這確保了消息不會丟失,但也引入了更多的組件、延遲和維護成本。

這個模式雖然解決了訊息不丟失的問題,卻引入了新的組件和延遲,把複雜性從基礎設施轉移到了應用程式本身,對開發和維護是個不小的挑戰。

決策框架與權衡

權衡維度 模式 A: Redis Streams 模式 B: 外部佇列 (RabbitMQ/Kafka)
原子性 完美:Lua 腳本保證庫存和消息的原子性。 :應用程式需處理跨服務的一致性問題。
主要風險 基礎設施層:極端故障下的數據丟失。 應用程式層:業務邏輯不一致(票已扣、單未建)。
複雜度來源 維運:需要專業地維護和監控 Redis 高可用叢集。 開發:需要實現複雜的分散式交易或補償模式。
性能 極高:基於記憶體,延遲極低。 :網路開銷和獨立 Broker 帶來了更高的延遲。
適用場景 對延遲極度敏感、瞬間流量極大、且允許後續業務對帳的場景。 業務流程複雜、對單筆訊息的可靠性要求高於一切的場景。

總結:我們的選擇

對於搶票這種瞬間流量極高、對延遲極度敏感、但允許後續對帳的場景,Redis Streams 是那個更務實、風險更可控的選擇

這次用 Lua 換來了完美的原子性,避免了在應用程式中處理複雜分散式交易的痛苦。

需要承擔的,是 Redis 的基礎設施風險,而這是一個可以透過標準化的高可用方案(Sentinel/Cluster)、嚴密的監控和專業的維運來管理和控制的問題。

選擇 RabbitMQ/Kafka 解決這個特定問題,有點「殺雞用牛刀」,不只增加了不必要的複雜度,還可能引入新的問題。

我們已經做出了選擇。
下一篇,來打造這個基於 Redis Streams 的非同步訂單處理流程。

參考資源

RabbitMQ vs Kafka vs Redis
Choosing the Right Messaging Tool: Redis Streams, Redis Pub/Sub, Kafka, and More - DEV Community
Reddit - 網路心之所在


上一篇
Go 語言搶票煉金術 Day 16 - 流程解耦:為什麼你需要消息佇列
下一篇
Go 語言搶票煉金術 Day 18 - 生產者:將「成功訂單」送入消息佇列
系列文
Go 語言搶票煉金術:解鎖千萬級併發下的原子交易奇蹟18
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言