上一篇,我們認定了「非同步」是目前的正解。
但接下來的決定,是許多資深工程師都曾面臨的難題:你該選擇哪個消息佇列 (Message Queue)?
網路上充滿了對 Redis Streams、RabbitMQ 和 Kafka 的 QPS、延遲等性能比較,但在搶票這個場景,它們沒有完全命中要害。
真正的問題是:如何在你選擇的架構下,維護「扣減庫存」和「創建訂單消息」這兩個步驟的原子性?
選擇了不同的工具,就要承受不同的後果。今天,我們不比較工具,我們比較詛咒。
回憶一下,我們的生命線是一個 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 的訊息可靠性,同時承擔「庫存已扣、消息未發」這種業務邏輯層面的數據不一致風險。
這才是我們今天要做的真正權衡。
選擇 Redis Streams,就是選擇了極致的性能和完美的原子性。使用我們在 Day 14 和 Day 15 設計的 Lua 腳本,DECR
和 XADD
在 Redis 服務端被當作一個單一、不可分割的操作執行。這意味著絕不會出現庫存扣了、消息卻沒發出去的情況。這非常誘人。
但代價是什麼?
代價是 Redis 的持久化策略。
預設情況下,Redis 的主從複製是非同步的。如果主節點在寫入資料後、但還沒來得及將資料同步到從節點時就徹底掛掉,此時若發生故障切換,那麼這幾秒內的所有寫入(包括我們的訂單消息)都會永久丟失。
這不是猜測,這是必然。但這個風險並非無法管理:
加強監控與維運:建立穩健的 Redis Sentinel 或 Cluster,縮短故障發現和切換的時間。
同步複製選項:使用 Redis 的 WAIT
命令,可以強制要求寫入操作必須同步到指定數量的從節點後才返回。這會犧牲一點延遲,但能換來更強的數據一致性保證。
業務補償機制:建立後續的對帳系統。既然風險只在極端情況下發生,我們可以設計一個補償流程來找回丟失的訂單,而不是在主路徑上增加複雜性。
選擇 RabbitMQ 或 Kafka,意味著你把賭注押在了訊息佇列本身的可靠性上。
它們都是為可靠傳遞而生的專業系統,透過持久化、多副本、發送方確認等機制,能做到訊息「At-Least-Once Delivery」(至少一次成功傳遞)。
但代價是什麼?
如前所述,代價是原子性的徹底喪失。
把一個簡單的資料庫原子性問題,升級成了一個棘手的分散式交易問題。
為了彌補那個「不一致的窗口」,我們必須在應用程式中引入更複雜的設計模式,例如:
這個模式雖然解決了訊息不丟失的問題,卻引入了新的組件和延遲,把複雜性從基礎設施轉移到了應用程式本身,對開發和維護是個不小的挑戰。
權衡維度 | 模式 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 - 網路心之所在