2021-01-29 分類: 網站建設
每到節假日期間,一二線城市返鄉、外出游玩的人們幾乎都面臨著一個問題:搶火車票!
從上邊兩種方案的考慮,我們可以得出結論:只要創建訂單,就要頻繁操作數據庫 IO。
那么有沒有一種不需要直接操作數據庫 IO 的方案呢,這就是預扣庫存。先扣除了庫存,保證不超賣,然后異步生成用戶訂單,這樣響應給用戶的速度就會快很多;那么怎么保證不少賣呢?用戶拿到了訂單,不支付怎么辦?
我們都知道現在訂單都有有效期,比如說用戶五分鐘內不支付,訂單就失效了,訂單一旦失效,就會加入新的庫存,這也是現在很多網上零售企業保證商品不少賣采用的方案。
訂單的生成是異步的,一般都會放到 MQ、Kafka 這樣的即時消費隊列中處理,訂單量比較少的情況下,生成訂單非常快,用戶幾乎不用排隊。
扣庫存的藝術
從上面的分析可知,顯然預扣庫存的方案最合理。我們進一步分析扣庫存的細節,這里還有很大的優化
為了保證扣庫存和生成訂單的原子性,需要采用事務處理,然后取庫存判斷、減庫存,最后提交事務,整個流程有很多 IO,對數據庫的操作又是阻塞的。
這種方式根本不適合高并發的秒殺系統。接下來我們對單機扣庫存的方案做優化:本地扣庫存。
我們把一定的庫存量分配到本地機器,直接在內存中減庫存,然后按照之前的邏輯異步創建訂單。
改進過之后的單機系統是這樣的:
這樣就避免了對數據庫頻繁的 IO 操作,只在內存中做運算,極大的提高了單機抗并發的能力。
但是百萬的用戶請求量單機是無論如何也抗不住的,雖然 Nginx 處理網絡請求使用 Epoll 模型,c10k 的問題在業界早已得到了解決。
但是 Linux 系統下,一切資源皆文件,網絡請求也是這樣,大量的文件描述符會使操作系統瞬間失去響應。
上面我們提到了 Nginx 的加權均衡策略,我們不妨假設將 100W 的用戶請求量平均均衡到 100 臺服務器上,這樣單機所承受的并發量就小了很多。
然后我們每臺機器本地庫存 100 張火車票,100 臺服務器上的總庫存還是 1 萬,這樣保證了庫存訂單不超賣,下面是我們描述的集群架構:
問題接踵而至,在高并發情況下,現在我們還無法保證系統的高可用,假如這 100 臺服務器上有兩三臺機器因為扛不住并發的流量或者其他的原因宕機了。那么這些服務器上的訂單就賣不出去了,這就造成了訂單的少賣。
要解決這個問題,我們需要對總訂單量做統一的管理,這就是接下來的容錯方案。服務器不僅要在本地減庫存,另外要遠程統一減庫存。
有了遠程統一減庫存的操作,我們就可以根據機器負載情況,為每臺機器分配一些多余的“Buffer 庫存”用來防止機器中有機器宕機的情況。
我們結合下面架構圖具體分析一下:
我們采用 Redis 存儲統一庫存,因為 Redis 的性能非常高,號稱單機 QPS 能抗 10W 的并發。
在本地減庫存以后,如果本地有訂單,我們再去請求 Redis 遠程減庫存,本地減庫存和遠程減庫存都成功了,才返回給用戶搶票成功的提示,這樣也能有效的保證訂單不會超賣。
當機器中有機器宕機時,因為每個機器上有預留的 Buffer 余票,所以宕機機器上的余票依然能夠在其他機器上得到彌補,保證了不少賣。
Buffer 余票設置多少合適呢,理論上 Buffer 設置的越多,系統容忍宕機的機器數量就越多,但是 Buffer 設置的太大也會對 Redis 造成一定的影響。
雖然 Redis 內存數據庫抗并發能力非常高,請求依然會走一次網絡 IO,其實搶票過程中對 Redis 的請求次數是本地庫存和 Buffer 庫存的總量。
因為當本地庫存不足時,系統直接返回用戶“已售罄”的信息提示,就不會再走統一扣庫存的邏輯。
這在一定程度上也避免了巨大的網絡請求量把 Redis 壓跨,所以 Buffer 值設置多少,需要架構師對系統的負載能力做認真的考量。
代碼演示
Go 語言原生為并發設計,我采用 Go 語言給大家演示一下單機搶票的具體流程。
初始化工作
Go 包中的 Init 函數先于 Main 函數執行,在這個階段主要做一些準備性工作。
我們系統需要做的準備工作有:初始化本地庫存、初始化遠程 Redis 存儲統一庫存的 Hash 鍵值、初始化 Redis 連接池。
另外還需要初始化一個大小為 1 的 Int 類型 Chan,目的是實現分布式鎖的功能。
也可以直接使用讀寫鎖或者使用 Redis 等其他的方式避免資源競爭,但使用 Channel 更加高效,這就是 Go 語言的哲學:不要通過共享內存來通信,而要通過通信來共享內存。
Redis 庫使用的是 Redigo,下面是代碼實現:
- ...
- //localSpike包結構體定義
- package localSpike
- type LocalSpike struct {
- LocalInStock int64
- LocalSalesVolume int64
- }
- ...
- //remoteSpike對hash結構的定義和redis連接池
- package remoteSpike
- //遠程訂單存儲健值
- type RemoteSpikeKeys struct {
- SpikeOrderHashKey string //redis中秒殺訂單hash結構key
- TotalInventoryKey string //hash結構中總訂單庫存key
- QuantityOfOrderKey string //hash結構中已有訂單數量key
- }
- //初始化redis連接池
- func NewPool() *redis.Pool {
- return &redis.Pool{
- MaxIdle: 10000,
- MaxActive: 12000, // max number of connections
- Dial: func() (redis.Conn, error) {
- c, err := redis.Dial("tcp", ":6379")
- if err != nil {
- panic(err.Error())
- }
- return c, err
- },
- }
- }
- ...
- func init() {
- localSpike = localSpike2.LocalSpike{
- LocalInStock: 150,
- LocalSalesVolume: 0,
- }
- remoteSpike = remoteSpike2.RemoteSpikeKeys{
- SpikeOrderHashKey: "ticket_hash_key",
- TotalInventoryKey: "ticket_total_nums",
- QuantityOfOrderKey: "ticket_sold_nums",
- }
- redisPool = remoteSpike2.NewPool()
- done = make(chan int, 1)
- done <- 1
- }
“12306”是如何支撐百萬QPS的?
瀏覽地址:http://m.newbst.com/news17/98017.html
成都網站建設公司_創新互聯,為您提供服務器托管、手機網站建設、建站公司、營銷型網站建設、網站維護、品牌網站建設
聲明:本網站發布的內容(圖片、視頻和文字)以用戶投稿、用戶轉載內容為主,如果涉及侵權請盡快告知,我們將會在第一時間刪除。文章觀點不代表本網站立場,如需處理請聯系客服。電話:028-86922220;郵箱:631063699@qq.com。內容未經允許不得轉載,或轉載時需注明來源: 創新互聯
猜你還喜歡下面的內容