優惠劵系統庫存設計淺談

zhong0316發表於2019-02-24

優惠劵系統活動庫存一般分為:總庫存和日庫存。在一個使用者來領取優惠劵時,需要判斷當前剩餘總庫存和日庫存是否充足,如果充足則進行庫存扣減,否則提示使用者領取失敗。總庫存和日庫存的扣減是一個原子操作,要麼都成功,要麼都失敗。我們知道資料庫事務滿足"ACID"特性,因此可以將這兩個操作放到一個事務中進行。

原始階段庫存設計

在最初階段,系統使用者數量較少,優惠劵領取請求流量不大。我們可以將一個活動的總庫存和日庫存放到同一個MySQL資料庫中。一個活動的總庫存對應一條記錄,一個活動的日庫存在活動期間每天都有一條日庫存記錄。在使用者來領取優惠劵時,我們開啟一個事務,首先扣減總庫存,其次扣減日庫存,如果任一步驟失敗,則回滾事務,前端提示使用者領取優惠劵失敗。

考慮到系統資料庫模組的可擴充套件性,我們採用“分庫分表”的方式進行資料的“sharding”,按照具體的業務ID來分庫,例如我們可以按照活動號來分片我們的資料,這樣同一個活動的總庫存和日庫存都落在一個資料庫中,可以做成一個事務。出於效能考慮我們不採用分散式事務。

這種系統設計的優點是:實現簡單,庫存具有強一致性(由事務保證);但是其缺點也很明顯:事務開啟時,MySQL會給總庫存和日庫存加行鎖(InnoDB儲存引擎),行鎖具有排他性,在一個事務提交之前其他事務都必須等待。因此,雖然在領劵時我們系統是開啟多執行緒方式併發處理請求,但是在更新資料庫時所有的請求還是“序列化”操作。假設,我們更新總庫存和日庫存這兩個操作一次耗時10ms,那麼系統針對每個活動的領劵請求“TPS”最高也只有100,這對於一些“熱點”活動高併發領取來說是無法忍受的。

Redis記憶體資料庫庫存扣減

Redis是一個高效能的分散式快取系統,它既可以用作快取,也可以用作記憶體資料庫。它的特點是效能極高,因為操作都是純記憶體操作。單機最高能夠達到10W的QPS,對於一般的系統,這個已經能夠滿足要求。"Every coin has two sides",雖然Redis的效能極高,但是它也有缺點,例如redis的事務不能像MySQL一樣能夠保證事務的"ACID"特性,Redis 的事務保證了 ACID 中的一致性(C)和隔離性(I),但並不保證原子性(A)和永續性(D)。這樣就可能產生問題,例如在扣減庫存時,首先扣減總庫存,再扣減日庫存。假設總庫存扣減成功,日庫存扣減失敗。Redis沒有回滾機制,這樣即使事務失敗了,也沒法回滾總庫存,從而產生問題。

Redis高可用

Redis支援叢集部署,Redis 叢集有16384個槽,通過計算key的CRC16校驗和再對16384取模得到key落在哪個slot上(CRC16(key) % 16384)。 當單個Redis節點撐不住時可以考慮用Redis叢集的方式來實現庫存扣減。主從複製,master節點和slave節點之間用Sentinel做故障轉移。

Redis同步扣減庫存,非同步同步到資料庫方案

雖然Redis提供了持久化方案(RDB,AOF),但是對於一些重要的業務資料,Redis本身的持久化方案可能還不夠,我們需要將Redis中記錄的庫存資料非同步同步到資料庫中。在極端情況下Redis掛了,還有資料庫作為"憑證"。 因此,根據業務需要,可以考慮開啟一個後臺任務,定時地將Redis中記錄的庫存資料同步到資料庫中。

相關文章