當(dāng)前位置:首頁 > 公眾號(hào)精選 > 小林coding
[導(dǎo)讀]雙十一又要到了,我有點(diǎn)慌,以前一個(gè)人的時(shí)候,一分錢都不花,現(xiàn)在有了女票,不僅得剁手,還得幫忙搶各種秒殺商品。今年,我真的不想再去搶秒殺了,為什么呢?太難了,成千上萬的人就盯著秒殺放出來的那點(diǎn)商品。我憑著單身十幾年的手速也搶不過啊。我苦思妙想,終于想出一條完(zuo)美(si)妙計(jì)...



雙十一又要到了,我有點(diǎn)慌,以前一個(gè)人的時(shí)候,一分錢都不花,現(xiàn)在有了女票,不僅得剁手,還得幫忙搶各種秒殺商品。


今年,我真的不想再去搶秒殺了,為什么呢?
太難了,成千上萬的人就盯著秒殺放出來的那點(diǎn)商品。我憑著單身十幾年的手速也搶不過啊。
我苦思妙想,終于想出一條完(zuo)美(si)妙計(jì):給女朋友講講程序員是如何做一個(gè)秒殺系統(tǒng)的。
對頭,就是要用知識(shí)的海洋淹沒她。如果她不愿意聽,或者聽不懂,那么今年就不參加雙十一了。
至于拒絕理由嘛。。。那就是【你都不認(rèn)真聽我說話,你一定是不愛我了】;如果不幸她聽懂了,也不礙事,至少讓她知道了我們程序員兄弟多么牛(jian)逼(xin)。
于是,我找到了女朋友阿醬。
:吶,你知道我工作上也經(jīng)常做秒殺系統(tǒng)嗎?今天我就給你講講秒殺是怎么做的,如果你聽懂了,今年我就幫你搶秒殺!
:可是要是我聽不懂怎么辦啊?
:我的寶貝怎么可能聽不懂,要是聽不懂一定是我講得不夠好!
:那。。。我試試吧

問題拋出首先,秒殺有哪些要考慮的地方呢?


第一點(diǎn),海量請求,服務(wù)要能扛住。
秒殺活動(dòng)一開始,瞬間會(huì)有海量流量涌入,熱門的商品甚至?xí)袔装偃f人來搶。這個(gè)規(guī)模的流量砸下來,服務(wù)可能就掛了,活動(dòng)也就GG了,收獲的只有罵聲。
怎么讓服務(wù)能打能抗,是需要考慮的問題。


第二點(diǎn),不能超賣。
因?yàn)槊霘⒂袝r(shí)候就是賠本賺吆喝,價(jià)格可能比成本價(jià)還低。而這時(shí)候要是比原計(jì)劃的數(shù)量賣多了,那到底發(fā)不發(fā)貨呢?
發(fā)貨會(huì)超預(yù)算虧損,要是超賣數(shù)量過多,說不定廠子都要倒閉了;不發(fā)貨會(huì)被投訴,影響商家聲譽(yù)。
不管怎樣,都是硬傷,只能找程序員賠錢了。

第三點(diǎn),盡量避免少賣。
少賣會(huì)比超賣好一些,商家不存在經(jīng)濟(jì)上的損失。但要是被眼尖的消費(fèi)者發(fā)現(xiàn)的話,也是免不了一場麻煩的。所以我們還是要盡可能避免這種情況。
第四點(diǎn),保證觸達(dá)到用戶而不是黃牛。
黃??赡苁情_腳本,一次發(fā)很多請求過來,搶到之后再轉(zhuǎn)賣。但我們做活動(dòng),希望的就是回饋客戶,進(jìn)而吸引用戶,而不是去讓黃牛賺外快。因此,我們要盡量擋住黃牛的魔爪。
不聽了,不聽了,腦殼痛。
那今年不用剁手啦~
:???你繼續(xù),我能行!
:問題我說完了,下面才是重點(diǎn),來說說解決方案。
:我好像已經(jīng)開始聽不懂了。。。



對癥下藥

硬抗高并發(fā)


在高并發(fā)的情況下,MySQL就顯得有些力不從心了。
一方面是MySQL本身要支持事務(wù)的ACID,單機(jī)性能不高。
另一方面,MySQL是個(gè)單機(jī)數(shù)據(jù)庫,本身是不能水平擴(kuò)展的,如果要搞分庫分表,費(fèi)時(shí)費(fèi)力。
這時(shí)候就可以借助MySQL的好伙伴Redis的能力。
Redis小哥可是單機(jī)支撐每秒幾萬的寫入,并且可以做成集群,提高擴(kuò)展能力的。
我們可以先將庫存名額預(yù)加載到Redis,然后在Redis中進(jìn)行扣減,扣減成功的再通過消息隊(duì)列,傳遞到MySQL做真正的訂單生成。
為什么要通過消息隊(duì)列呢?
主要有兩點(diǎn)好處,一個(gè)是這種投遞的方式,可以讓搶和購解耦。另一個(gè)是可以很方便地限頻,不至于讓MySQL過度承壓。


我們說回Redis,如果請求量超過6W每秒,就要考慮使用多個(gè)Redis來分流。預(yù)計(jì)有100W請求量,我們就可以臨時(shí)調(diào)度20個(gè)Redis實(shí)例來支持,一個(gè)5W/s,留點(diǎn)Buffer。
這種模式倒是不需要使用Redis Cluster那種一致性Hash的做法,直接前面接個(gè)Nginx,做負(fù)載均衡就可以了。


拒絕超賣


解決了高并發(fā)的問題,我們再來看看怎么防止超賣。
既然我們將庫存名額加載到了Redis,那就需要精確計(jì)數(shù)。
我們搶購場景最核心的,有兩個(gè)步驟:
第一步,判斷庫存名額是否充足;
第二步,減少庫存名額,扣減成功就是搶到。


這里有一個(gè)問題要考慮,如果第一步判斷的時(shí)候還有庫存,但是由于是并發(fā)操作,實(shí)際調(diào)用的時(shí)候,可能已經(jīng)沒有庫存了,這樣就會(huì)造成超賣。
所以第一步和第二步都是需要原子操作的。
但是Redis沒有直接提供這種場景原子化的操作。
遇事不要慌,仔細(xì)想一想,Redis是不是還有個(gè)特性,專門整合原子操作,對,就是它——Lua。

Redis?Lua,可以說是專門為解決原子問題而生,在Lua腳本中調(diào)用Redis的多個(gè)命令,這些命令整體上會(huì)作為原子操作來進(jìn)行。

盡量避免少賣


少賣什么情況會(huì)出現(xiàn)呢?
庫存減少了,但用戶訂單沒生成。
什么情況會(huì)這樣呢?
在Redis操作成功,但是向Kafka發(fā)送消息失敗,這種情況就會(huì)白白消耗Redis中的庫存。


作為一個(gè)專業(yè)的程序員,只要知道問題是什么、怎么發(fā)生的,問題就解決了一半。說白了,我們只需要保證Redis庫存 Kafka消耗的最終一致性。
但是一致性問題,一直是分布式場景的惡龍,要對付并不容易。
第一種,也最簡單的方式,在投遞Kafka失敗的情況下,增加漸進(jìn)式重試;
第二種,更安全一點(diǎn),就是在第一種的基礎(chǔ)上,將這條消息記錄在磁盤上,慢慢重試;
第三種,寫磁盤之前就可能失敗,可以考慮走WAL路線,但是這樣做下去說不定就做成MySQL的undo log,redo log這種WAL技術(shù)了,會(huì)相當(dāng)復(fù)雜,沒有必要。

針對少賣這種極端場景可接受的問題,一般選擇第二種方式即可,畢竟是異常情況的小概率事件,真出問題了大不了人工介入。

打擊黃牛


黃牛的惡劣影響,很多時(shí)候是被低估了。
不僅僅是侵害了正常用戶的權(quán)益,同時(shí)由于黃牛善于使用腳本,很容易造成大量的惡意請求,讓本就不富裕的服務(wù)器資源,雪上加霜。
通常來說,為了打擊黃牛,最常見的方式是限購,一個(gè)用戶最多只能搶到N份,這樣可以大大保障正常用戶的權(quán)益。
具體怎么做呢,為了性能,我們還是將限制邏輯加入到Redis中,所以我們的Lua腳本中,第一步查詢庫存,第二步扣減庫存,需要優(yōu)化為第一步查詢庫存,第二步查詢用戶已購買個(gè)數(shù),第三步扣減庫存,第四步記錄用戶購買數(shù)。


這里需要注意的是,如果使用Redis集群,那么Redis的一致性Hash Key,需要根據(jù)用戶來分Key,不然用戶數(shù)據(jù)會(huì)查詢不到。
有了限購,我們可以保證貨品不會(huì)被黃牛占據(jù)太多,那么還剩一個(gè)問題,黃牛大多是通過代碼來搶購,點(diǎn)擊速度比人點(diǎn)擊快得多,這樣就導(dǎo)致了競爭不公平。
作為追求極致的coder,我們希望還能更進(jìn)一步,做到競爭公平。
怎么解決呢?某個(gè)用戶請求接口次數(shù)過于頻繁,一般說明是用腳本在跑,可以只針對該用戶做限制。
針對IP做限制也是常見做的做法,但這樣容易誤殺,主要考慮到使用同一個(gè)網(wǎng)絡(luò)的用戶,可能都是一個(gè)出口IP。限制IP,會(huì)導(dǎo)致正常用戶也受到影響。

更好用的方案是加上一個(gè)驗(yàn)證碼驗(yàn)證。驗(yàn)證碼符合91原則,90%的時(shí)間,都用在驗(yàn)證碼輸入上,所以使用腳本點(diǎn)擊的影響會(huì)降到很低。


當(dāng)然,我們要明白沒有銀彈,這種方式缺點(diǎn)在于降低了用戶的體驗(yàn)感。

故事尾聲:這樣一來,我們的秒殺場景就基本OK啦!

:怎么樣,聽懂了嗎?
:嗯嗯?。ㄐ奶摰挠昧c(diǎn)頭)我們能去秒殺了嗎?
:那我來檢驗(yàn)一下?
:我難道不是你的寶貝了嗎?
無奈地嘆氣:說吧,你這次又想搶什么東西?
本站聲明: 本文章由作者或相關(guān)機(jī)構(gòu)授權(quán)發(fā)布,目的在于傳遞更多信息,并不代表本站贊同其觀點(diǎn),本站亦不保證或承諾內(nèi)容真實(shí)性等。需要轉(zhuǎn)載請聯(lián)系該專欄作者,如若文章內(nèi)容侵犯您的權(quán)益,請及時(shí)聯(lián)系本站刪除。
關(guān)閉
關(guān)閉