馬上雙十一,教你用Python實(shí)現(xiàn)秒殺系統(tǒng)(python搶秒殺)
堅(jiān)持學(xué)習(xí)很難,養(yǎng)成學(xué)習(xí)習(xí)慣更難
架構(gòu)搭建是重點(diǎn),代碼或語言實(shí)現(xiàn)較簡(jiǎn)單。
本篇用python redis rabbitmq搭建一個(gè)秒殺系統(tǒng)。 用flask編寫后端,只包含秒殺相關(guān)程序,省略具體的業(yè)務(wù)接口。.
關(guān)注,轉(zhuǎn)發(fā),私信小編“01”即可免費(fèi)領(lǐng)取python學(xué)習(xí)資料!
項(xiàng)目會(huì)持續(xù)更新,完整代碼見github: https://github.com/Sssmeb/seckilling
(如果覺得有幫助的話可以點(diǎn)個(gè)star~~~~ )
篇幅有限,不會(huì)介紹redis或rabbitmq的基本操作。 如果沒學(xué)過相關(guān)知識(shí)的只需先了解以下兩點(diǎn),也可以看懂本架構(gòu)。
- redis是內(nèi)存型數(shù)據(jù)庫(kù),讀寫速度遠(yuǎn)快于mysql這類磁盤型數(shù)據(jù)庫(kù),常用來作緩存。
- rabbitmq消息隊(duì)列,可以理解為生產(chǎn)者消費(fèi)者模型,用隊(duì)列來存儲(chǔ)任務(wù),生產(chǎn)與消費(fèi)解耦。
前言
在介紹架構(gòu)之前,我們需要先知道秒殺系統(tǒng)面臨的難點(diǎn)是什么。
首先在普通的系統(tǒng)中, 最大的瓶頸是在于底層的數(shù)據(jù)庫(kù)端 。 因?yàn)榈讓訑?shù)據(jù)庫(kù)(比如常見的mysql)是磁盤存儲(chǔ)的,所以讀寫IO較慢,而且連接數(shù)有限。
而在秒殺業(yè)務(wù)場(chǎng)景,最大的特點(diǎn)是 瞬時(shí)的高并發(fā) ,即在短時(shí)間內(nèi)會(huì)有大量的請(qǐng)求到來。 如果讓所有請(qǐng)求都打到底層數(shù)據(jù)庫(kù)上,很大可能數(shù)據(jù)庫(kù)會(huì)直接崩掉,即使數(shù)據(jù)庫(kù)能承受住大量的連接請(qǐng)求,但大量的請(qǐng)求讀寫都會(huì)導(dǎo)致大量的鎖沖突,導(dǎo)致響應(yīng)速度大大減慢。 而響應(yīng)速度對(duì)于用戶體驗(yàn)來說,無疑是十分重要的。
所以在這里,需要明確第一個(gè)目標(biāo): 讓盡可能少、盡可能有效的請(qǐng)求打到底層數(shù)據(jù)庫(kù) 。
當(dāng)我們回頭再考慮這個(gè)業(yè)務(wù)場(chǎng)景,其實(shí)絕大部分的請(qǐng)求都不應(yīng)該打到底層數(shù)據(jù)庫(kù)。 因?yàn)橐话闵唐穾?kù)存可能只有搶購(gòu)用戶數(shù)的百分之一,甚至更少。 所以我們需要一些機(jī)制、策略,提前將無效的請(qǐng)求返回。
而站在整個(gè)網(wǎng)站設(shè)計(jì)的角度,第二個(gè)目標(biāo): 越上層越容易實(shí)現(xiàn),越有效。
這里的層指:
- 頁(yè)面層
- 網(wǎng)絡(luò)層
- 應(yīng)用層
- 服務(wù)層
- 數(shù)據(jù)層
例如在前端頁(yè)面層,如果不做處理,用戶在點(diǎn)擊搶購(gòu)按鈕以后,見網(wǎng)頁(yè)沒有響應(yīng),可能會(huì)再點(diǎn)擊3-4次甚至更多,這樣可能會(huì)導(dǎo)致最終有80%的請(qǐng)求都是重復(fù)無效的。 但只需要前端在設(shè)計(jì)時(shí),將點(diǎn)擊后的按鈕置灰,防止用戶多次點(diǎn)擊發(fā)送請(qǐng)求。 即簡(jiǎn)單又有效。
以下簡(jiǎn)單指出各層可實(shí)施的策略:
- 頁(yè)面層(簡(jiǎn)單的實(shí)現(xiàn)可以屏蔽 90%的請(qǐng)求)
- 按鈕置灰,禁止用戶重復(fù)提交
- 驗(yàn)證碼
- 網(wǎng)絡(luò)層
- 通過ip限制一定時(shí)間內(nèi)的請(qǐng)求次數(shù)
- 應(yīng)用層
- 一個(gè)頁(yè)面最占用資源、帶寬的是cs js 圖片等靜態(tài)資源
- 避免所有請(qǐng)求都到服務(wù)器的硬盤上取
- 動(dòng)靜分離,壓縮緩存處理(CDN nginx)
- 根據(jù)uid限頻,頁(yè)面緩存技術(shù)(web服務(wù)器 nginx)
- 反向代理 負(fù)載均衡 (nginx)
- 服務(wù)層
- 微服務(wù)
- redis
- 消息隊(duì)列 削峰 異步處理
- 數(shù)據(jù)層
- 讀寫分離
- 分庫(kù)分表
- 集群
每一層具體實(shí)現(xiàn)起來都是一個(gè)很大的架構(gòu),這里我們主要專注于服務(wù)層,使用redis 消息隊(duì)列。
基礎(chǔ)架構(gòu)
架構(gòu).png
核心: 服務(wù)異步拆分,減少耦合,使用緩存,加快響應(yīng)。
避免同步 的請(qǐng)求執(zhí)行,如: 請(qǐng)求→訂單→支付→修改庫(kù)存→結(jié)束返回,這種模型在高并發(fā)場(chǎng)景下,阻塞多,響應(yīng)慢,服務(wù)器壓力大,不可取。
這里實(shí)現(xiàn)的架構(gòu)是: 1. 請(qǐng)求→返回 2. 支付→返回 3. 修改庫(kù)存
這種服務(wù)拆分歸功于 消息隊(duì)列。 核心思想是,將接收到的請(qǐng)求 存儲(chǔ)到隊(duì)列中就可以響應(yīng)用戶了,后端在隊(duì)列中取出請(qǐng)求再做后續(xù)操作即可。 簡(jiǎn)單理解就是,我們將請(qǐng)求記錄下來,晚點(diǎn)空閑了再處理。
基礎(chǔ)數(shù)據(jù)存儲(chǔ)
數(shù)據(jù)、請(qǐng)求的存儲(chǔ)情況如:
- mysql中存儲(chǔ)商品信息、訂單信息
- redis存入商品信息、設(shè)置計(jì)數(shù)器、存儲(chǔ)成功訂單的數(shù)據(jù)結(jié)構(gòu)等
- rabbitmq創(chuàng)建隊(duì)列
- 訂單隊(duì)列(用戶提交請(qǐng)求)
- 延遲隊(duì)列(訂單必須在15分鐘內(nèi)支付)
- 成交隊(duì)列(訂單支付成功,等待寫入數(shù)據(jù)庫(kù))
流程
以下所有代碼都是截取核心部分,完整代碼參看
訂單請(qǐng)求
redis計(jì)數(shù)器
假設(shè)我們只有100件商品庫(kù)存,但可能會(huì)收到10萬條搶購(gòu)請(qǐng)求。 也就是會(huì)有將近9.9萬條無效的請(qǐng)求,所以我們要將這些請(qǐng)求阻隔。
最簡(jiǎn)單的方法,也是我們使用的方法: 實(shí)現(xiàn)一個(gè)count變量,每個(gè)請(qǐng)求進(jìn)入都加一,當(dāng)count大于100時(shí)則直接返回失敗即可 。
這里我們使用redis也是因?yàn)閮?nèi)存讀寫速度要遠(yuǎn)大于類似mysql的磁盤讀寫。
代碼實(shí)現(xiàn)增加了分布式鎖。相關(guān)知識(shí)可以看:https://www.jianshu.com/p/cf311cfb1689
訂單隊(duì)列
異步拆分服務(wù)的關(guān)鍵。 為了提高響應(yīng)速度,我們只需要 將請(qǐng)求訂單任務(wù)保存下來 (消息隊(duì)列),就可以 直接返回用戶 了。 而 不需要將請(qǐng)求轉(zhuǎn)到后端做大量的判斷、處理、數(shù)據(jù)庫(kù)讀寫操作后才返回用戶 。 所有可以 大大的加快響應(yīng)速度 。 后端可以隨時(shí)從隊(duì)列中取出請(qǐng)求再做各自處理,即使等搶購(gòu)活動(dòng)結(jié)束再進(jìn)行底層數(shù)據(jù)庫(kù)讀寫也沒有問題。
所以核心思路就是把請(qǐng)求放入隊(duì)列,然后直接返回用戶即可。
# 計(jì)數(shù)器 1flag = plus_counter(goods_id)# 成功申請(qǐng)if flag:# 生成唯一的訂單號(hào)order_id = uuid.uuid1()# 訂單信息(也是請(qǐng)求任務(wù)信息)order_info = {“goods_id” : goods_id,”user_id” : user_id,”order_id” : str(order_id)}try :# 進(jìn)入訂單隊(duì)列enter_order_queue(order_info)res[ “status” ] = Trueres[ “msg” ] = “搶購(gòu)成功,請(qǐng)?jiān)?5分鐘之內(nèi)付款!”res[ “order_id” ] = str(order_id)return jsonify(res)except Exception as e:print( “log: ” , e)res[ “status” ] = Falseres[ “msg” ] = “搶購(gòu)出錯(cuò),請(qǐng)重試.” str(e)returnjsonify(res), 202
enter_order_queue是將訂單請(qǐng)求(訂單信息),也就是order_info發(fā)送到對(duì)應(yīng)的隊(duì)列。 與之對(duì)應(yīng)的消費(fèi)者,只需要將該訂單信息寫入數(shù)據(jù)庫(kù)對(duì)應(yīng)的訂單表即可。
注意: 此時(shí)訂單還沒支付,所以數(shù)據(jù)庫(kù)表中可以設(shè)置一個(gè)status字段,標(biāo)識(shí)訂單的狀態(tài)。
唯一標(biāo)識(shí)
不局限于uuid,可用毫秒時(shí)間戳之類的唯一標(biāo)識(shí)。
可以看到上面代碼中,我們利用uuid生成了一個(gè)唯一標(biāo)識(shí)作為訂單號(hào),并且返回給用戶。
主要的作用是:
- 標(biāo)識(shí)訂單。因?yàn)橛唵握?qǐng)求僅僅只是被我們?nèi)腙?duì)列,消費(fèi)者可能還沒開始處理。(即訂單可能還未被創(chuàng)建在數(shù)據(jù)庫(kù)中)
- 返回給用戶,可用于后續(xù)的支付操作。
當(dāng)用戶支付時(shí)需要校驗(yàn)用戶與對(duì)應(yīng)的單號(hào)是否正確,這里我們?nèi)杂胷edis,以提高查詢速度。
所以在上面的基礎(chǔ)上,我們需要加多一步,將訂單信息寫入redis。
order_info = {“goods_id” : goods_id,”user_id” : user_id,”order_id” : str(order_id)}try :# 在redis中創(chuàng)建這個(gè)訂單create_order(order_info)enter_order_queue(order_info)res[ “status” ] = Trueres[ “msg” ] = “搶購(gòu)成功,請(qǐng)?jiān)?5分鐘之內(nèi)付款!”res[ “order_id” ] = str(order_id)returnjsonify(res)訂單的結(jié)構(gòu)這里采用字典,提高檢索效率。 插入如:redis_conn.hset( “order:” str(goods_id), str(order_id), str(user_id))超時(shí)隊(duì)列
正如前面所見,我們提示用戶在15分鐘之內(nèi)支付,符合日常業(yè)務(wù)場(chǎng)景。
在消息隊(duì)列中有延遲隊(duì)列的應(yīng)用,符合我們的超時(shí)需求。 所以我們同樣用消息隊(duì)列來實(shí)現(xiàn)這一業(yè)務(wù)需求。 即我們?cè)趧?chuàng)建訂單時(shí),同樣將訂單信息傳入隊(duì)列中。
try :
# redis保存訂單信息
create_order(order_info)
# 訂單隊(duì)列
enter_order_queue(order_info)
# 超時(shí)隊(duì)列
enter_overtime_queue(order_info)
最終,當(dāng)一個(gè)訂單請(qǐng)求通過計(jì)數(shù)器后,需要經(jīng)歷的三個(gè)過程如上。 無論是redis或是rabbitmq消息隊(duì)列,都是內(nèi)存操作,速度都是足夠快的。 不需要經(jīng)過數(shù)據(jù)層即可響應(yīng)用戶。
至此,一個(gè)訂單“創(chuàng)建”完成。
支付請(qǐng)求
訂單請(qǐng)求完成后,用戶會(huì)獲得訂單號(hào)。 用戶必須在15分鐘內(nèi)完成支付操作。 在執(zhí)行支付時(shí)需要考慮:
- 檢查用戶和對(duì)應(yīng)的訂單號(hào)是否正確
- create_order(order_info) 時(shí),我們已將訂單信息寫入redis??蓮倪@里取得數(shù)據(jù)做校驗(yàn)
- 檢查訂單是否超時(shí)
- 如果我們?cè)O(shè)置的超時(shí)隊(duì)列超過指定時(shí)間,則隊(duì)列里的請(qǐng)求會(huì)被處理(消費(fèi))
- 我們只需要將超時(shí)的單號(hào)寫入redis即可做校驗(yàn)
- 支付成功入成交隊(duì)列
- 同理于訂單隊(duì)列。只需將成交的訂單信息寫入消息隊(duì)列中,后續(xù)系統(tǒng)空閑時(shí)再寫入數(shù)據(jù)庫(kù)即可。
- 也是為了提高用戶響應(yīng)速度,用戶不需要等待數(shù)據(jù)庫(kù)io完成后才收到結(jié)果。
代碼流程為:
但訂單通過檢查、并支付完成后。 我們還需要將成交的訂單寫入redis,記錄狀態(tài)(用于其他判斷)。 再將訂單請(qǐng)求寫入隊(duì)列即可返回。 全程內(nèi)存操作,速度快,帶來了快響應(yīng)。 之后,我們可以等搶購(gòu)活動(dòng)結(jié)束后,系統(tǒng)比較空閑的時(shí)間將訂單同步到底層數(shù)據(jù)庫(kù),同步數(shù)據(jù)。
總覽
所以兩個(gè)核心的操作是:
- 通過rabbitmq消息隊(duì)列異步拆分服務(wù),加快了響應(yīng)的速度
- 通過redis內(nèi)存讀寫,減少操作時(shí)間
再總結(jié)整個(gè)框架:
- 用戶提交訂單
- 通過redis計(jì)數(shù)器篩選
- 成功則返回標(biāo)識(shí),然后入訂單隊(duì)列 超時(shí)隊(duì)列
- 標(biāo)識(shí)與用戶信息寫入redis,用于后續(xù)驗(yàn)證支付
- 訂單隊(duì)列,mysql監(jiān)聽,寫入mysql的訂單歷史表
- 超時(shí)訂單隊(duì)列有計(jì)時(shí)功能,一定時(shí)間內(nèi)未支付,訂單失效,搶購(gòu)失敗。寫入redis(標(biāo)志失?。?/li>
- 失敗直接返回
- 訂單服務(wù)結(jié)束
- 用戶支付訂單
- 驗(yàn)證訂單以及檢查是否已超時(shí)(是否已在redis相關(guān)結(jié)構(gòu)內(nèi))
- 成功支付則入支付隊(duì)列
- mysql監(jiān)聽這個(gè)隊(duì)列,執(zhí)行庫(kù)存同步操作。
- 寫入redis
- 失敗或超時(shí)直接返回
- 支付服務(wù)結(jié)束
流程
注意
- 代碼持續(xù)更新,完整代碼: https://github.com/Sssmeb/seckilling (覺得有幫助的可以給個(gè)star)
- 本架構(gòu)只專注于服務(wù)層的業(yè)務(wù)架構(gòu),有很多沒有涉及的點(diǎn)(高可用,數(shù)據(jù)一致性等),一個(gè)完整的搶購(gòu)系統(tǒng)是一個(gè)非常龐大的。
- 文中沒有介紹mysql數(shù)據(jù)層相關(guān)的操作,一方面是為了提示大家,在高并發(fā)的情景下應(yīng)該盡可能的避免這類的磁盤io操作。 另一方面,mysql數(shù)據(jù)層相關(guān)操作是在消息隊(duì)列 消費(fèi)者進(jìn)行操作的,這里不詳解操作。 只注重整體架構(gòu)。 具體操作見代碼。