本部落格討論一下akka在秒殺場景下的應用,提出自己的見解,只做拋磚引玉,大神勿噴。秒殺活動涉及到前中後台各個階段,為了說明問題,我們簡化場景,只研究akka在後台如何處理秒殺業務。
秒殺活動
所謂的秒殺活動,簡單點來說,就是把某個稀缺商品或**商品,掛到頁面,供大量客戶搶購。這裡有兩個關鍵點,商品數量不多,客戶量非常大或搶購流量非常大。客戶量或搶購流量往往意味著併發量非常大,容易給伺服器造成很大的瞬時壓力。
同樣,為了簡化問題,我們把秒殺活動中的概念也進行簡化,分為庫存和搶購請求。庫存:待搶購商品的數量。搶購請求:客戶為了搶購商品的點選動作,也就是一次請求。搶購請求分為成功和不成功兩種,搶購成功會減少庫存,否則不會。
其實在活動中經常會有海量的、重複的、分布的搶購請求,因為同乙個客戶會點選多次或開多個頁面進行搶購。如果處理這些請求的伺服器只有一台,其他的不說,這個瞬時流量都不一定能夠承受,因為寬頻搞不定了啊。所以應該是多少個節點來處理。另外搶購請求是海量的、分布的,這就意味著併發量很大,如果處理不當還可能造成超賣的情況。傳統技術解決這個問題,無非就是加鎖、用事務、用佇列。
我們把搶購請求只劃分為成功和失敗兩種,請大家一定要注意,這裡其實我們忽略了付款的問題,因為這會增加問題的複雜度,不利於分析問題。其實吧,付款是可以規避掉的,提前讓使用者付款,然後再搶購不就好了?搶購成功意味著付款成功。
在akka中,重要的是如何通過actor實現我們的業務邏輯。其實細細分析可以發現,庫存就是乙個actor集,每乙個商品的例項就是乙個actor,也就是乙個sku對應乙個actor。搶購請求就是actor收到的訊息。
了解akka機制的同學,大概已經知道怎麼解決秒殺問題了,不就是把每個商品抽象成乙個actor,每個請求抽象成這個actor的訊息麼?這跟佇列沒啥區別啊,因為actor就是用佇列來接收訊息的。這跟傳統的使用佇列解決問題非常類似,但還是有些區別的。傳統的佇列只是用來解決併發問題的,畢竟解決併發的根本方法就是區域性序列化。抽象成actor除了使用佇列把併發程式設計序列之外,還分散了計算能力。傳統的方案中,可能就是把請求塞到佇列,然後使用多個消費者處理這些請求,很難做到分布式,如果做成了分布式,其實跟akka就差不多了。
那麼究竟該如何用akka解決這個問題呢?
首先每個商品(也就是sku)抽象成乙個actor,庫存有多少,就有多少個actor其次搶購請求抽象成actor的乙個訊息。那是不是就是簡單的把訊息傳送給actor呢?其實訊息路由的過程才是難點。比如使用者x傳送的搶購訊息該傳送給哪個商品的actor呢?隨機傳送?使用者傳送多次請求,不就會搶購到多個商品?使用者id計算hash之後傳送給固定的actor?那如果這個商品被其他人搶到了呢?其實我們還需要乙個處理搶購訊息的分發器。
還需要乙個搶購請求的router。當然也可以不需要。在每個搶購訊息傳送之前都需要經過該actor,由該actor對訊息進行分發。該router的作用,就是把同乙個客戶的搶購請求路由到固定的商品actor。那如何實現呢?其實也很簡單,那就是用一致性hash。每個商品和使用者都會有乙個hash值,總是把使用者id的hash值跟商品hash進行比較,把搶購訊息路由給與使用者id的hash值最接近的商品actor上就可以了。但這個hash演算法需要做到盡可能的分散使用者和商品,避免出現熱點。其實這個actor的設計比較關鍵,也比較難,我這裡只是提供了方案,並沒有說明具體的實現細節,請大家見諒。
另外商品actor的郵箱型別需要修改成有界佇列。因為每個商品就是乙個actor,而每個商品只能買個乙個客戶,所以嚴格上來說,這個actor只能接收乙個搶購請求。所以商品actor的郵箱型別必須是有界佇列,避免訊息過多撐爆記憶體,當然這個佇列的長度必須是大於1的。超過這個佇列長度的訊息怎麼辦呢?在這個商品被搶購成功之前,其實可以直接丟棄,因為已經有其他客戶占用這個商品了。當然,占用不意味著一定能夠搶成功,也可能失敗,比如觸發了反薅羊毛策略,或資料庫更新失敗。因為佇列中還有其他的搶購請求,前面的客戶搶購失敗,還是可以把該商品分配給其他客戶的。
最後還需要計算當前商品的庫存,這也需要乙個actor。每個商品actor啟動時,給stateactor發訊息;商品被搶購成功後,給stateactor發訊息,同時stop掉自身。這樣stateactor就可以非同步的獲取當前的庫存了。
補充。在上面的介紹中,我們還忽略了乙個很重要的操作,那就是更新資料庫,畢竟最終還是需要把訂單等資訊寫到資料庫的,而最終落庫才是真正的搶購成功。那麼該如何落庫呢?
我們知道每個商品都會處理搶購請求,也就是說,如果有n個商品,那麼至少會有n個寫資料庫的請求,該由誰來完成呢?
其實有兩種方案,一是由stateaggregation 來完成,在收到對應商品actor的stop訊息後,更新資料庫;二是單獨再用乙個actor來完成該功能。其實兩者都差不多,讀者可自行決定。不過我更建議單獨用乙個actor來完成該功能,因為更新資料庫的請求相對來說還是很大的。比如有1萬個商品,那麼同時就可能有1萬個寫資料庫的請求,相對資料庫來說,量還是很大的。此時我們可以用多個actor來完成資料庫的寫入,此時用乙個router,分散更新請求。
不過更新資料庫也有兩種方案,一種是單條寫入,一種是批量寫入。其實秒殺這個場景,更適合批量寫入。因為搶購的行為是短時間內的,也就意味著更新資料庫的請求也是短時間內的。此時我們可以每1000條更新一次資料庫,而不必過分考慮批量時間間隔的問題。因為我們可以把更新資料庫的請求看成,全部商品一次性傳送過來 。批量寫入可以大大節省寫資料庫的時間,這樣也能盡可能的把插入資料庫的結果返回給商品actor。只不過最後不滿1000條的請求會慢一點,要等下乙個超時時間後,批量寫入。不過這個時間可以設定的很短,比如1秒。這也就意味著,所有的入庫請求,最慢時間是1秒。
2023年9月20日
其實無論資料庫怎麼優化,最終都會成為秒殺活動的瓶頸的,因為是海量(對資料庫而言)的併發寫啊。有時候解決問題的最好方法,就是避免問題的出現。加一層快取應該可以的嘍?
上面是優化後的架構。外網使用者搶購的請求通過多台nginx進行**,nginx將同乙個使用者的請求**給後面的同乙個region。region根據使用者id將其請求**給相同的shard。shard裡面有一組商品,shard會將相同使用者的請求分配給相同的商品。但這會造成即使有商品,某特使用者也搶不到,其實吧,這算是變相的公平,也就是說,商品會被隨機的分發給不同的使用者。不過shard的分配演算法也可以修改,分給該shard的使用者共同爭搶該組內的商品。
其實商品actor的資訊最終是要落庫的,考慮到高併發對資料庫的壓力是不可承受的,在資料庫之前需要一層緩衝。可以使用kafka等其他可以迅速將訊息落地的技術或框架,緩衝的資料落地到資料庫,可以是乙個緩慢的過程,因為搶購成功到最終發貨還是需要一段時間的。但這裡還有乙個問題,就是使用者去**查詢自己已經搶到了呢?資料庫,還是快取?其實吧,要我說,只要告訴使用者搶購已經結束(庫存為0),不一定要立即告訴他有沒有搶到。等快取的訊息全都落庫,訂單最終生成,再告訴他有沒有搶到就好了。
2023年9月20日19:39:47
商品actor如何快速的落庫就成了我們的「心頭大患」,本來我想只是簡單的把訂單資訊寫入到kafka,kafka雖然也可以保證一定寫成功,但又引入了第三方框架。為了偷懶,又設計了上面的架構,即用cacheactor替換kafka等訊息佇列。
cacheactor對訂單訊息的持久化就是關鍵了,那該怎麼做呢?為了偷懶,我選擇了記憶體對映檔案,也就是把訂單資訊寫入本地檔案。然後等搶購結束後在讀取該檔案,再進行落庫。另外為了減少檔案的大小,我準備使用乙個位圖儲存這些資訊。即,使用者的序號為2345,則在檔案的第2345個byte上面寫入255,否則就是0。減少檔案大小還可以增加寫檔案的速度,避免錯誤的發生。
經測試,在1萬個商品,10萬個使用者,每個使用者重複點選100次搶購按鈕的情況下,搶購完成的時間為1341毫秒。
機器環境如下:
秒殺系統架構分析與實戰搶購(秒殺)業務的技術要點
電商技術解密——秒殺系統
秒殺活動一般怎麼做
akka設計模式系列 While模式
while模式嚴格來說是while迴圈在akka中的合理實現。while是開發過程中經常用到的語句之一,也是絕大部分程式語言都支援的語法。但while語句是乙個迴圈,如果迴圈條件沒有達到會一直執行while語句體的 且會阻塞while語句外的 如果在akka中簡單的使用while語句會極大的限制當前...
akka設計模式系列 Aggregate模式
所謂的aggregate模式,其實就是聚合模式,跟masterworker模式有點類似,但其出發點不同。masterworker模式是指master向worker傳送命令,worker完成某種業務邏輯。而聚合模式則剛好相反,由各個worker完成某種業務邏輯後,把結果彙總發給某個actor,這個ac...
Akka原始碼分析 ask模式
在我之前的博文中,已經介紹過要慎用actor的ask。這裡我們要分析一下ask的原始碼,看看它究竟是怎麼實現的。開發時,如果要使用ask方法,必須要引入akka.pattern.這樣才能使用ask 或者?方法,那麼想必ask是在akka.pattern.對應的包裡面實現的。implementatio...