Java之秒殺遇到的問題及解決方案


Java之秒殺遇到的問題及解決方案

什麼是秒殺?

“秒殺是在 瞬間擊殺 的意思。 也是網上競拍的一種新方式.

所謂“秒殺”,就是網絡賣家發佈一些超低價格的商品,所有買家在同一時間網上搶購的一種銷售方式。

由於商品價格低廉,往往一上架就被搶購一空,有時只用一秒鐘。

秒殺的流程

這裡通過兩張圖簡單介紹下秒殺的流程,真正的系統比這複雜很多.

Java之秒殺遇到的問題及解決方案

Java之秒殺遇到的問題及解決方案


秒殺系統痛點有哪些?

1.高併發: 時間極短、 瞬間用戶量大,而且用戶會在開始前不斷刷新頁面,還會積累一大堆重複請求的問題,請求過多把數據庫打宕機了,系統響應失敗,導致與這個系統耦合的系統也GG,一掛掛一片

2.鏈接暴露: 有人知道了你秒殺的url,然後在秒殺開始前通過這個url傳要傳遞的參數來進行購買操作

3.超賣: 你只有一百件商品,由於是高併發的問題,一起拿到了最後一件商品的信息,都認為還有,全賣出去了,最終賣了一百十件,倉庫里根本沒這麼多貨

4.惡意請求: 因為秒殺的價格比較低,有人會用腳本來秒殺,全給一個人買走了,他再轉賣,或者同時以腳本操作多個賬號一起秒殺。(就是我們常見的黃牛黨)

5.數據庫: 一瞬間高QPS把數據庫打宕機了,誰都搶不到,活動失敗GG,這可能與高併發有重疊的點,不過著眼於數據庫的具體方面

Java之秒殺遇到的問題及解決方案

解決方案

1.高併發的解決方案

既然是超高併發,就想著降低和分散流量,需要考慮一些加進來的系統會不會被打掛:

1.nginx做負載均衡(一個tomcat可能只能抗住幾百的的併發,nginx還可以對一些惡意ip做限制)

2.資源靜態化,把前端的模板頁面放到CDN服務器中(放到別的服務器中,減輕自己服務器的壓力)

3.頁面的按鈕,按一次後置灰X秒(防止一直用戶一直點,同一個用戶重複點擊,雖然不會再賣給他,但是請求還是會到後端給系統壓力,需要在前端按鈕上做限制,比如點一下限制五秒內不能點擊。)

4.同一個uid,限制訪問頻度,做頁面緩存,x秒內到達站點層的請求,均返回同一頁面(用來防止其他程序員跳過按鈕,直接用for循環不斷髮起http請求,具體的話可以請求每次進來都去redis查有沒有對應的id的key值,沒有就在redis中設置X秒過期的key)

5.對於寫請求,用消息隊列(比如商品有一萬件,就放一萬個請求進來,當然要做好每秒幾個的限制,不能一秒內全放進來,都成功了就繼續放下一批,沒成功就剩下的請求全部返回失敗),

6.讀請求用redis集群頂住(一個redis也只能頂住幾萬的併發,叫上兄弟)

7.記住一定一定要先把數據庫裡的東西提前加載到redis來,別等用戶查了再加

2.鏈接暴露的解決方案:

1.有人會說,在上面已經做好了請求X秒一條的限制了嘛,為什麼還要防止鏈接暴露?

其實在秒殺沒開始前,這個秒殺的接口也是存在的,如果這個接口的url被人知道了,他直接可以在秒殺開始前就通過請求傳輸必要的參數來進行秒殺。

我們要做的就是在秒殺開始前,誰都不知道秒殺(也就是付款、減少庫存的接口)這個接口的url是什麼。

2.如何防止秒殺的url暴露?

我們要做的是,在秒殺時間到的時候,才能獲得url

而且秒殺場景中,肯定會有一個倒計時的模塊,來告訴你還有幾秒開始秒殺。我們邏輯如下:

①頁面中有一個計時模塊,是訪問秒殺頁面的時候去從服務器裡拿的,計時結束,顯示秒殺的按鈕。

問題:(為什麼不直接取用戶的時間?)

②點擊秒殺按鈕後,再次請求服務器時間,與秒殺的時間對比.

如果秒殺進行中,返回一個由加密過的秒殺url 問題:

(為什麼還要再次請求服務器時間?怎麼加密url?)

③通過這個加密過的url來進行支付、減庫存操作。

解決問題: 問題1:為什麼不直接取用戶的時間?

用戶的本機時間是可以修改的。

問題2:為什麼還要再次請求服務器時間?

雖然第一次請求到了服務器時間,但是時間的倒計時操作是頁面來完成的.

比如在幾天前就打開這個秒殺頁面等待倒計時,等秒殺開始時可能與服務器時間存在幾秒的誤差。

問題3:怎麼加密url?

通過一個無序的String字符串,也就是我們常說的鹽值字符串,進行MD5加密得到哈希值,

在步驟②中點擊秒殺按鈕返回url的時候,返回這個MD5值進行url的拼接。

真正秒殺接口的mapping格式為:

因為這個md5是等秒殺開始才得到的,使得秒殺的鏈接得以保護。

3.超賣問題的解決方案:

先設想這麼一個場景:

我們假設現在商品只剩下一件了,此時數據庫中 num = 1;

但有100個線程同時讀取到了這個 num = 1,所以100個線程都開始減庫存了。

這裡有三個解決思路,分別是悲觀鎖樂觀鎖redis 悲觀鎖:

將減少庫存的操作加入到事務中.

(注意此時數據庫的引擎需要是Innodb的只有innodb才支持事務和行級鎖,而事務和行級鎖這兩個概念,一般也是同時出現).

此時一個線程開始修改這一行的時候,會先獲得排他鎖,獲得鎖後開始修改,沒有獲得鎖的線程會阻塞。

在我實現的秒殺系統中用的正是事務來實現的,將訂單信息的插入(insert)和減少庫存(update)放到一個事務中。

可以思考下先做insert還是update?

答案應該是先insert而後update,因為insert的鎖的行不會有人競爭,而update的排它鎖的行會出現大量競爭,而事務鎖釋放的時機是整個事務完成.

而不是這個方法執行完畢的時候,所以需要後update,儘量減少庫存行鎖的獲取時間,來提高併發效率。

樂觀鎖: 需要在數據庫表中加入版本號字段.

sql語句如下,先查詢出版本號,在下次更新的時候通過判斷版本號是否更改和庫存是否不為0來決定是否update:

1 select version from goods WHERE id= 1001

2 update goods set num = num - 1,

version = version + 1 WHERE id= 1001 AND num > 0 AND version = @version(上面查到的version);

這種方式採用了版本號的方式,其實也就是CAS的原理。

假設此時version = 100, num = 1; 100個線程進入到了這裡,同時他們select出來版本號都是version = 100。

然後直接update的時候,只有其中一個先update了,同時更新了版本號。

那麼其他99個在更新的時候,會發覺version並不等於上次select的version.

就說明version被其他線程修改過了。那麼我就放棄這次update.

Java之秒殺遇到的問題及解決方案

redis:

首先要了解,redis是單線程串行執行的 利用redis的單線程預減庫存。

比如商品有100件。

那麼我在redis存儲一個k,v。

例如 <gs1001> 每一個用戶線程進來,key值就減1,等減到0的時候,全部拒絕剩下的請求。/<gs1001>

那麼也就是隻有100個線程會進入到後續操作。

所以一定不會出現超賣的現象.

4.惡意請求的解決方案:

惡意請求我們分析下來有兩個問題.

1.怎麼限制讓一個人只能秒殺一件商品?

這個其實比較容易想到,數據庫肯定有一張訂單表(包含用戶id字段、商品id字段).

只要每次去查一下用戶的id和這件商品的id有沒有在這張表裡就行了,存在說明該用戶買過這個商品了,就不給買了。

問題在於,是不是每次都需要去查詢然後插入呢?有沒有更快的方法? 一般支付成功扣款和把訂單信息加入到訂單表中這兩個操作設為一個事務。

用戶id商品id作為聯合主鍵索引存儲到數據庫中,下次如果再想插入一樣用戶id和商品id的行,是會報錯的。(主鍵索引的唯一性),事務中出現錯誤,支付操作自然也失敗了。

2.如果一個人用腳本掌握了多個賬號去執行秒殺,怎麼辦?

可以讓用戶付款的時候回答問題,防止腳本的操作。

比如12306買火車票的時候,是不是會有按順序點擊圖中相同的文字?這就是為了防止腳本。

這邊插入一條與惡意請求無關的需求,比如餓了麼,他創建訂單的時間不是在支付成功的時候,而是你選擇完菜品點擊支付的時候,如果你返回,有十五分鐘的時間去支付。

沒支付前應該是放在redis中的,設置一個key的過期時間,如果支付成功了,就落地到mysql並在redis中刪除這個數據。(當然還有別的實現方式,這裡只是我想到的一種)

5.數據庫層面的解決方案:

1.用消息隊列來削峰

比如一秒鐘進來1W個寫請求,我們數據庫只能頂住一秒5000個,那我們就每秒放出來4000個。

消息隊列的好處和帶來的問題請移步我另一篇文章 為什麼要用消息隊列?會帶來什麼問題?

2.用緩存來頂住大量的查詢請求

引入redis,如果redis中有要查詢的數據,就直接返回,如果沒有,從數據庫查詢的時候,把查詢的結果放到redis中,以後查詢都會落到redis層。

在這裡又會出現引用redis常見的問題.

問題①:一開始沒有這個key,需要第一次查詢才會到redis,此時秒殺開始,又是一堆併發進來把數據庫打掛了,活動失敗,GG。

解決方案:在活動沒開始前,就把可能會訪問到的數據都加載到redis中,雖然解決方法很簡單,但是一開始沒想到這個情況,會是很嚴重的問題。

問題②:如果這個數據特別熱,突然這個key過期了,然後一堆併發請求過來查詢這個數據,還是把數據庫打掛了。

解決方案: 這也是我們常說的緩存擊穿的問題,先看下這個問題的解決方案

1.設置這個key不過期(比如淘寶首頁,只需要在有修改的時候去更新這個key就行)

2.數據庫的查詢時使用互斥鎖,這個時候只有一個線程來查詢,不會有一瞬間大量的查詢sql,查詢之後也放在了redis中,後面的查詢就不會打到sql。

(查詢的互斥鎖會把查詢的結果集加入互斥鎖,其他的不受影響,也就是說大大降低了相同查詢的併發量) 引入redis中還會出現其他的問題,比如

緩存雪崩緩存穿透.

3.數據庫層面讀寫分離

設置主從數據庫,主數據庫負責寫,從數據庫負責讀

可以極大程度的緩解X鎖和S鎖爭用。

以上就是Java之秒殺遇到的問題及解決方案,下面展示了部分資料,希望也能幫助到大家,對編程感興趣想進階的朋友,如果能幫到你請點贊、點贊、點贊:

整理的 pdf 文檔:

Java之秒殺遇到的問題及解決方案

Java之秒殺遇到的問題及解決方案

Java之秒殺遇到的問題及解決方案

Java之秒殺遇到的問題及解決方案

源碼分析專題部分課程:

Java之秒殺遇到的問題及解決方案

Java之秒殺遇到的問題及解決方案

獲取方式

點贊,收藏並轉發文章後點擊小編頭像或暱稱,關注後私信回覆:【11】 即可

舉手之勞,非常感謝!!!


分享到:


相關文章: