RabbitMQ在秒殺場景中的簡單應用
轉載
https://www.cnblogs.com/hello-/articles/10345026.html
一、秒殺:全過程
1、秒殺業務為什麼難做?
1)im系統,例如qq或者微博,每個人都讀自己的資料(好友列表、群列表、個人資訊);
2)微博系統,每個人讀你關注的人的資料,一個人讀多個人的資料;
3)秒殺系統,庫存只有一份,所有人會在集中的時間讀和寫這些資料,多個人讀一個數據。
例如:小米手機每週二的秒殺,可能手機只有1萬部,但瞬時進入的流量可能是幾百幾千萬。
又例如:12306搶票,票是有限的,庫存一份,瞬時流量非常多,都讀相同的庫存。讀寫衝突,鎖非常嚴重,這是秒殺業務難的地方。那我們怎麼優化秒殺業務的架構呢?
2、優化方向
優化方向有兩個(今天就講這兩個點):
(1)將請求儘量攔截在系統上游(不要讓鎖衝突落到資料庫上去)。傳統秒殺系統之所以掛,請求都壓倒了後端資料層,資料讀寫鎖衝突嚴重,併發高響應慢,幾乎所有請求都超時,流量雖大,下單成功的有效流量甚小。以12306為例,一趟火車其實只有2000張票,200w個人來買,基本沒有人能買成功,請求有效率為0。
(2)充分利用快取,秒殺買票,這是一個典型的讀多寫少的應用場景,大部分請求是車次查詢,票查詢,下單和支付才是寫請求。一趟火車其實只有2000張票,200w個人來買,最多2000個人下單成功,其他人都是查詢庫存,寫比例只有0.1%,讀比例佔99.9%,非常適合使用快取來優化。好,後續講講怎麼個“將請求儘量攔截在系統上游”法,以及怎麼個“快取”法,講講細節。
3、常見秒殺架構
常見的站點架構基本是這樣的(絕對不畫忽悠類的架構圖)
(1)瀏覽器端,最上層,會執行到一些JS程式碼
(2)站點層,這一層會訪問後端資料,拼html頁面返回給瀏覽器
(3)服務層,向上遊遮蔽底層資料細節,提供資料訪問
(4)資料層,最終的庫存是存在這裡的,mysql是一個典型(當然還有會快取)
這個圖雖然簡單,但能形象的說明大流量高併發的秒殺業務架構,大家要記得這一張圖。後面細細解析各個層級怎麼優化。
4、各層次優化細節
第一層,客戶端怎麼優化(瀏覽器層,APP層)
問大家一個問題,大家都玩過微信的搖一搖搶紅包對吧,每次搖一搖,就會往後端傳送請求麼?回顧我們下單搶票的場景,點選了“查詢”按鈕之後,系統那個卡呀,進度條漲的慢呀,作為使用者,我會不自覺的再去點選“查詢”,對麼?繼續點,繼續點,點點點。。。有用麼?平白無故的增加了系統負載,一個使用者點5次,80%的請求是這麼多出來的,怎麼整?
(a)產品層面,使用者點選“查詢”或者“購票”後,按鈕置灰,禁止使用者重複提交請求;
(b)JS層面,限制使用者在x秒之內只能提交一次請求;
APP層面,可以做類似的事情,雖然你瘋狂的在搖微信,其實x秒才向後端發起一次請求。這就是所謂的“將請求儘量攔截在系統上游”,越上游越好,瀏覽器層,APP層就給攔住,這樣就能擋住80%+的請求,這種辦法只能攔住普通使用者(但99%的使用者是普通使用者)對於群內的高階程式設計師是攔不住的。firebug一抓包,http長啥樣都知道,js是萬萬攔不住程式設計師寫for迴圈,呼叫http介面的,這部分請求怎麼處理?
第二層,站點層面的請求攔截
怎麼攔截?怎麼防止程式設計師寫for迴圈呼叫,有去重依據麼?ip?cookie-id?…想複雜了,這類業務都需要登入,用uid即可。在站點層面,對uid進行請求計數和去重,甚至不需要統一儲存計數,直接站點層記憶體儲存(這樣計數會不準,但最簡單)。一個uid,5秒只准透過1個請求,這樣又能攔住99%的for迴圈請求。
5s只透過一個請求,其餘的請求怎麼辦?快取,頁面快取,同一個uid,限制訪問頻度,做頁面快取,x秒內到達站點層的請求,均返回同一頁面。同一個item的查詢,例如車次,做頁面快取,x秒內到達站點層的請求,均返回同一頁面。如此限流,既能保證使用者有良好的使用者體驗(沒有返回404)又能保證系統的健壯性(利用頁面快取,把請求攔截在站點層了)。
頁面快取不一定要保證所有站點返回一致的頁面,直接放在每個站點的記憶體也是可以的。優點是簡單,壞處是http請求落到不同的站點,返回的車票資料可能不一樣,這是站點層的請求攔截與快取優化。
好,這個方式攔住了寫for迴圈發http請求的程式設計師,有些高階程式設計師(黑客)控制了10w個肉雞,手裡有10w個uid,同時發請求(先不考慮實名制的問題,小米搶手機不需要實名制),這下怎麼辦,站點層按照uid限流攔不住了。
第三層 服務層來攔截(反正就是不要讓請求落到資料庫上去)
服務層怎麼攔截?大哥,我是服務層,我清楚的知道小米只有1萬部手機,我清楚的知道一列火車只有2000張車票,我透10w個請求去資料庫有什麼意義呢?沒錯,請求佇列!
對於寫請求,做請求佇列,每次只透有限的寫請求去資料層(下訂單,支付這樣的寫業務)
1w部手機,只透1w個下單請求去db
3k張火車票,只透3k個下單請求去db
如果均成功再放下一批,如果庫存不夠則佇列裡的寫請求全部返回“已售完”。
對於讀請求,怎麼優化?cache抗,不管是memcached還是redis,單機抗個每秒10w應該都是沒什麼問題的。如此限流,只有非常少的寫請求,和非常少的讀快取mis的請求會透到資料層去,又有99.9%的請求被攔住了。
當然,還有業務規則上的一些優化。回想12306所做的,分時分段售票,原來統一10點賣票,現在8點,8點半,9點,...每隔半個小時放出一批:將流量攤勻。
其次,資料粒度的優化:你去購票,對於餘票查詢這個業務,票剩了58張,還是26張,你真的關注麼,其實我們只關心有票和無票?流量大的時候,做一個粗粒度的“有票”“無票”快取即可。
第三,一些業務邏輯的非同步:例如下單業務與 支付業務的分離。這些優化都是結合 業務 來的,我之前分享過一個觀點“一切脫離業務的架構設計都是耍流氓”架構的優化也要針對業務。
第四層 最後是資料庫層
瀏覽器攔截了80%,站點層攔截了99.9%並做了頁面快取,服務層又做了寫請求佇列與資料快取,每次透到資料庫層的請求都是可控的。db基本就沒什麼壓力了,閒庭信步,單機也能扛得住,還是那句話,庫存是有限的,小米的產能有限,透這麼多請求來資料庫沒有意義。
全部透到資料庫,100w個下單,0個成功,請求有效率0%。透3k個到資料,全部成功,請求有效率100%。
5、總結
上文應該描述的非常清楚了,沒什麼總結了,對於秒殺系統,再次重複下我個人經驗的兩個架構優化思路:
(1)儘量將請求攔截在系統上游(越上游越好);
(2)讀多寫少的常用多使用快取(快取抗讀壓力);
瀏覽器和APP:做限速
站點層:按照uid做限速,做頁面快取
服務層:按照業務做寫請求佇列控制流量,做資料快取
資料層:閒庭信步
並且:結合業務做優化
6、Q&A
問題1、按你的架構,其實壓力最大的反而是站點層,假設真實有效的請求數有1000萬,不太可能限制請求連線數吧,那麼這部分的壓力怎麼處理?
答:每秒鐘的併發可能沒有1kw,假設有1kw,解決方案2個:
(1)站點層是可以通過加機器擴容的,最不濟1k臺機器來唄。
(2)如果機器不夠,拋棄請求,拋棄50%(50%直接返回稍後再試),原則是要保護系統,不能讓所有使用者都失敗。
問題2、“控制了10w個肉雞,手裡有10w個uid,同時發請求” 這個問題怎麼解決哈?
答:上面說了,服務層寫請求佇列控制
問題3:限制訪問頻次的快取,是否也可以用於搜尋?例如A使用者搜尋了“手機”,B使用者搜尋“手機”,優先使用A搜尋後生成的快取頁面?
答:這個是可以的,這個方法也經常用在“動態”運營活動頁,例如短時間推送4kw使用者app-push運營活動,做頁面快取。
問題4:如果佇列處理失敗,如何處理?肉雞把佇列被撐爆了怎麼辦?
答:處理失敗返回下單失敗,讓使用者再試。佇列成本很低,爆了很難吧。最壞的情況下,快取了若干請求之後,後續請求都直接返回“無票”(佇列裡已經有100w請求了,都等著,再接受請求也沒有意義了)
問題5:站點層過濾的話,是把uid請求數單獨儲存到各個站點的記憶體中麼?如果是這樣的話,怎麼處理多臺伺服器叢集經過負載均衡器將相同使用者的響應分佈到不同伺服器的情況呢?還是說將站點層的過濾放到負載均衡前?
答:可以放在記憶體,這樣的話看似一臺伺服器限制了5s一個請求,全域性來說(假設有10臺機器),其實是限制了5s 10個請求,解決辦法:
1)加大限制(這是建議的方案,最簡單)
2)在nginx層做7層均衡,讓一個uid的請求儘量落到同一個機器上
問題6:服務層過濾的話,佇列是服務層統一的一個佇列?還是每個提供服務的伺服器各一個佇列?如果是統一的一個佇列的話,需不需要在各個伺服器提交的請求入佇列前進行鎖控制?
答:可以不用統一一個佇列,這樣的話每個服務透過更少量的請求(總票數/服務個數),這樣簡單。統一一個佇列又複雜了。
問題7:秒殺之後的支付完成,以及未支付取消佔位,如何對剩餘庫存做及時的控制更新?
答:資料庫裡一個狀態,未支付。如果超過時間,例如45分鐘,庫存會重新會恢復(大家熟知的“回倉”),給我們搶票的啟示是,開動秒殺後,45分鐘之後再試試看,說不定又有票喲~
問題8:不同的使用者瀏覽同一個商品 落在不同的快取例項顯示的庫存完全不一樣 請問老師怎麼做快取資料一致或者是允許髒讀?
答:目前的架構設計,請求落到不同的站點上,資料可能不一致(頁面快取不一樣),這個業務場景能接受。但資料庫層面真實資料是沒問題的。
問題9:就算處於業務把優化考慮“3k張火車票,只透3k個下單請求去db”那這3K個訂單就不會發生擁堵了嗎?
答:(1)資料庫抗3k個寫請求還是ok的;(2)可以資料拆分;(3)如果3k扛不住,服務層可以控制透過去的併發數量,根據壓測情況來吧,3k只是舉例;
問題10;如果在站點層或者服務層處理後臺失敗的話,需不需要考慮對這批處理失敗的請求做重放?還是就直接丟棄?
答:別重放了,返回使用者查詢失敗或者下單失敗吧,架構設計原則之一是“fail fast”。
問題11.對於大型系統的秒殺,比如12306,同時進行的秒殺活動很多,如何分流?
答:垂直拆分
問題12、額外又想到一個問題。這套流程做成同步還是非同步的?如果是同步的話,應該還存在會有響應反饋慢的情況。但如果是非同步的話,如何控制能夠將響應結果返回正確的請求方?
答:使用者層面肯定是同步的(使用者的http請求是夯住的),服務層面可以同步可以非同步。
問題13、秒殺群提問:減庫存是在那個階段減呢?如果是下單鎖庫存的話,大量惡意使用者下單鎖庫存而不支付如何處理呢?
答:資料庫層面寫請求量很低,還好,下單不支付,等時間過完再“回倉”,之前提過了。
二、秒殺全過程
業務介紹
什麼是秒殺?通俗一點講就是網路商家為促銷等目的組織的網上限時搶購活動
比如說京東秒殺,就是一種定時定量秒殺,在規定的時間內,無論商品是否秒殺完畢,該場次的秒殺活動都會結束。這種秒殺,對時間不是特別嚴格,只要下手快點,秒中的概率還是比較大的。
淘寶以前就做過一元搶購,一般都是限量 1 件商品,同時價格低到「令人發齒」,這種秒殺一般都在開始時間 1 到 3 秒內就已經搶光了,參與這個秒殺一般都是看運氣的,不必太強求
務特點
同時併發量大
秒殺時會有大量使用者在同一時間進行搶購,瞬時併發訪問量突增 10 倍,甚至 100 倍以上都有。
庫存量少
一般秒殺活動商品量很少,這就導致了只有極少量使用者能成功購買到。
業務簡單
流程比較簡單,一般都是下訂單、扣庫存、支付訂單
技術難點
有業務的衝擊
秒殺是營銷活動中的一種,如果和其他營銷活動應用部署在同一伺服器上,肯定會對現有其他活動造成衝擊,極端情況下可能導致整個電商系統服務宕機
直接下訂單
下單頁面是一個正常的 URL 地址,需要控制在秒殺開始前,不能下訂單,只能瀏覽對應活動商品的資訊。簡單來說,需要 Disable 訂單按鈕
頁面流量突增
秒殺活動開始前後,會有很多使用者請求對應商品頁面,會造成後臺伺服器的流量突增,同時對應的網路頻寬增加,需要控制商品頁面的流量不會對後臺伺服器、DB、Redis 等元件的造成過大的壓力
架構設計思想
限流
由於活動庫存量一般都是很少,對應的只有少部分使用者才能秒殺成功。所以我們需要限制大部分使用者流量,只准少量使用者流量進入後端伺服器
削峰
秒殺開始的那一瞬間,會有大量使用者衝擊進來,所以在開始時候會有一個瞬間流量峰值。如何把瞬間的流量峰值變得更平緩,是能否成功設計好秒殺系統的關鍵因素。實現流量削峰填谷,一般的採用快取和 MQ 中介軟體來解決
非同步
秒殺其實可以當做高併發系統來處理,在這個時候,可以考慮從業務上做相容,將同步的業務,設計成非同步處理的任務,提高網站的整體可用性
快取
秒殺系統的瓶頸主要體現在下訂單、扣減庫存流程中。在這些流程中主要用到 OLTP 的資料庫,類似 MySQL、SQLServer、Oracle。由於資料庫底層採用 B+ 樹的儲存結構,對應我們隨機寫入與讀取的效率,相對較低。如果我們把部分業務邏輯遷移到記憶體的快取或者 Redis 中,會極大的提高併發效率
整體架構
客戶端優化
客戶端優化主要有兩個問題
秒殺頁面
秒殺活動開始前,其實就有很多使用者訪問該頁面了。如果這個頁面的一些資源,比如 CSS、JS、圖片、商品詳情等,都訪問後端伺服器,甚至 DB 的話,服務肯定會出現不可用的情況。所以一般我們會把這個頁面整體進行靜態化,並將頁面靜態化之後的頁面分發到 CDN 邊緣節點上,起到壓力分散的作用
防止提前下單
防止提前下單主要是在靜態化頁面中加入一個 JS 檔案引用,該 JS 檔案包含活動是否開始的標記以及開始時的動態下單頁面的 URL 引數。同時,這個 JS 檔案是不會被 CDN 系統快取的,會一直請求後端服務的,所以這個 JS 檔案一定要很小。當活動快開始的時候(比如提前),通過後臺介面修改這個 JS 檔案使之生效
API 接入層優化
客戶端優化,對於不是搞計算機方面的使用者還是可以防止住的。但是稍有一定網路基礎的使用者就起不到作用了,因此服務端也需要加些對應控制,不能信任客戶端的任何操作。一般控制分為 2 大類
限制使用者維度訪問頻率
針對同一個使用者( Userid 維度),做頁面級別快取,單元時間內的請求,統一走快取,返回同一個頁面
限制商品維度訪問頻率
大量請求同時間段查詢同一個商品時,可以做頁面級別快取,不管下回是誰來訪問,只要是這個頁面就直接返回
SOA 服務層優化
上面兩層只能限制異常使用者訪問,如果秒殺活動運營的比較好,很多使用者都參加了,就會造成系統壓力過大甚至宕機,因此需要後端流量控制
對於後端系統的控制可以通過訊息佇列、非同步處理、提高併發等方式解決。對於超過系統水位線的請求,直接採取 「Fail-Fast」原則,拒絕掉
秒殺整體流程圖
秒殺系統核心在於層層過濾,逐漸遞減瞬時訪問壓力,減少最終對資料庫的衝擊。通過上面流程圖就會發現壓力最大的地方在哪裡?
MQ 排隊服務,只要 MQ 排隊服務頂住,後面下訂單與扣減庫存的壓力都是自己能控制的,根據資料庫的壓力,可以定製化建立訂單消費者的數量,避免出現消費者資料量過多,導致資料庫壓力過大或者直接宕機。
庫存服務專門為秒殺的商品提供庫存管理,實現提前鎖定庫存,避免超賣的現象。同時,通過超時處理任務發現已搶到商品,但未付款的訂單,並在規定付款時間後,處理這些訂單,將恢復訂單商品對應的庫存量
總結
核心思想:層層過濾
- 儘量將請求攔截在上游,降低下游的壓力
- 充分利用快取與訊息佇列,提高請求處理速度以及削峰填谷的作用
三、秒殺服務層和後端,示例1:rabbitMQ佇列
秒殺業務的核心是庫存處理,使用者購買成功後會進行減庫存操作,並記錄購買明細。
當秒殺開始時,大量使用者同時發起請求,這是一個並行操作,多條更新庫存數量的SQL語句會同時競爭秒殺商品所處資料庫表裡的那行資料,導致庫存的減少數量與購買明細的增加數量不一致,因此,我們使用RabbitMQ進行削峰限流並且將請求資料序列處理。
首先我先設計了兩張表,一張是秒殺庫存表,另一張是秒殺成功表。
CREATE TABLE seckill ( seckill_id BIGINT NOT NULL AUTO_INCREMENT COMMENT '商品庫存id', name VARCHAR (120) NOT NULL COMMENT '商品名稱', number INT NOT NULL COMMENT '庫存數量', initial_price BIGINT NOT NULL COMMENT '原價', seckill_price BIGINT NOT NULL COMMENT '秒殺價', sell_point VARCHAR (500) NOT NULL COMMENT '賣點', create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '秒殺建立時間', start_time TIMESTAMP NOT NULL COMMENT '秒殺開始時間', end_time TIMESTAMP NOT NULL COMMENT '秒殺結束時間', PRIMARY KEY (seckill_id) );
ALTER TABLE seckill COMMENT '秒殺庫存表'; CREATE INDEX idx_create_time ON seckill (create_time); CREATE INDEX idx_start_time ON seckill (start_time); CREATE INDEX idx_end_time ON seckill (end_time);
CREATE TABLE success_killed ( success_id BIGINT NOT NULL AUTO_INCREMENT COMMENT '秒殺成功id', seckill_id BIGINT NOT NULL COMMENT '秒殺商品id', user_phone BIGINT NOT NULL COMMENT '使用者手機號', state TINYINT NOT NULL DEFAULT - 1 COMMENT '狀態標誌:-1:無效;0:成功', create_time TIMESTAMP NOT NULL COMMENT '秒殺成功建立時間', PRIMARY KEY (success_id) );
ALTER TABLE success_killed COMMENT '秒殺成功表'; CREATE INDEX idx_create_time ON success_killed (create_time);
接下來我開始模擬使用者請求,往RabbitMQ中傳送100個手機號。
import pika connection = pika.BlockingConnection(pika.ConnectionParameters( host='localhost')) channel = connection.channel() channel.queue_declare("task_seckill") def request_goods(seckill_id, user_phone): for i in range(100): set_goods(seckill_id, user_phone) def set_goods(seckill_id, user_phone): goods = "%s-%s" % (seckill_id, user_phone) channel.basic_publish(exchange='', routing_key='task_seckill', properties=pika.BasicProperties( delivery_mode=2 ), body=goods)
然後我用RabbitMQ監聽seckill_queue佇列,當佇列中接收到訊息就會自動觸發RabbitMQService類中的executeSeckill方法,訊息將作為方法的引數傳遞進來執行秒殺操作。
class RabbitMQService: def __init__(self): url = "mysql+pymysql://root:[email protected]:3306/lb4?charset=utf8" from sqlalchemy import create_engine engine = create_engine(url) self.con = engine.connect() self.connection = pika.BlockingConnection(pika.ConnectionParameters( host='localhost')) self.channel = self.connection.channel() self.channel.queue_declare("task_seckill") self.channel.basic_consume(self.execute_seckill, queue='task_seckill') def execute_seckill(self, ch, method, pros, body): good_list = body.split("-") if not good_list: return seckill_id, user_phone = good_list if not seckill_id: return seckill_id = int(seckill_id) with self.con.begin(): good_number = self.con.execute("select number from seckill where seckill_id=%s", (seckill_id)).fetchone()["number"] if good_number <= 0: return lastid = self.con.execute("update seckill set number=number-1 where seckill_id=%s", (seckill_id)).lastid if lastid: self.con.execute("insert into success_killed........") def start_consuming(self): self.channel.start_consuming() RabbitMQService().start_consuming()
在前端頁面使用倒計時外掛增強使用者體驗效果。
四、秒殺服務層之redis示例
# redis命令: # exists key: 判斷key是否存在---如果返回key說明存在,否則不存在。 # 遞增和遞減都是原子操作: # incr key: 每次遞增1.如果key不存在,則建立value為0的key # decr key: 每次遞減1.如果key不存在,則建立value為0的key # incrby key increment: 每次增加increment。如果key不存在,則建立value為0的key # decrby key increment: 每次減少increment。如果key不存在,則建立value為0的key import logging from logging import handlers import redis from flask import Flask app = Flask(__name__) # 定義logger: rf_handler = handlers.TimedRotatingFileHandler('redis.log', when='midnight', interval=1, backupCount=7) format = "%(asctime)s %(filename)s line:%(lineno)d [%(levelname)s] %(message)s" rf_handler.setFormatter(logging.Formatter(format)) logging.getLogger().setLevel(logging.INFO) logging.getLogger().addHandler(rf_handler) # 連線redis pool = redis.ConnectionPool(host="localhost", port=6379, decode_response=True) rs = redis.Redis(connection_pool=pool) def limit_handler(): # return: True: 允許; False: 拒絕 amount_limit = 100 # 限制數量 key_name = 'xxx_goods_limit' # redis key name incr_amount = 1 # 每次增加數量 # 判斷key是否存在 if not rs.exists(key_name): # setnx是原子性的,允許併發操作 rs.setnx(key_name, 100) # 資料插入後再判斷是否大於限制數 if rs.incrby(key_name, incr_amount) <= amount_limit: return True return False @app.route("/limit") def v2(): if limit_handler(): logging.info("successful") else: logging.info("failed") return 'limit' if __name__ == '__main__': app.run(debug=True)
簡單測試,安裝工具:sudo apt install apache2-utils
測試命令:ab -c 100 -n 200 http://127.0.0.1:5000/limit
# -c表示併發數, -n表示請求數
測試結果:通過日誌可以看到最多隻有5個successful
部署測試方案:
supervisor + gunicorn + gevent
1).安裝依賴:
- apt-get install supervisor
- pip install gevent
- pip install gunicorn
2).生成配置:
echo_supervisord_conf > supervisord.conf
3).修改配置, 在supervisord.conf最後新增
[program:redis-limit] directory = /home/dong/projects/py ;程式的啟動目錄 command = gunicorn -k gevent -w 4 -b 0.0.0.0:5000 app:app ; 啟動命令, 使用gevent, 開啟4個程序 autostart = true ; 在 supervisord 啟動的時候也自動啟動 startsecs = 5 ; 啟動 5 秒後沒有異常退出,就當作已經正常啟動了 autorestart = true ; 程式異常退出後自動重啟 startretries = 3 ; 啟動失敗自動重試次數,預設是 3 redirect_stderr = true ; 把 stderr 重定向到 stdout,預設 false stdout_logfile_maxbytes = 20MB ; stdout 日誌檔案大小,預設 50MB stdout_logfile_backups = 20 ; stdout 日誌檔案備份數 stdout_logfile = /home/dong/projects/py/limit_stdout.log
4).啟動supervisor服務
supervisord -c ./supervisord.conf
5).檢視supervisor應用
# 如果沒有啟動可以手動start redis-limit
supervisorctl -c ./supervisord.conf
6).測試
ab -c 100 -n 200 http://127.0.0.1:5000/limit
五、秒殺服務層和後端,示例2:RabbitMQ+Redis叢集+Quartz實現簡單高併發秒殺
花了兩天時間實現了一個使用rabbitMQ佇列和redis叢集存取資料以及使用Quartz觸發新增秒殺商品。
這一塊小功能很早就想做的,自從自學了redis的命令,發現了expire能夠設定自動消亡的時候,我就已經開始蠢蠢欲動了,接著在接觸rabbitMQ工作模式(多個消費者爭搶資料)的時候,我已經下決心要實現秒殺了。
上個專案是9月底和朋友做完的,一個高併發分散式的專案,開6臺centOS虛擬機器搭建nginx、主從伺服器、redis叢集,rabbitMQ佇列,amoeba實現讀寫分離和主從資料庫,以及Solr搜尋。這個專案是用來練手linux與分散式的,大多數精力都花在搭環境上了,基本步驟也都能百度到,不想寫到部落格。正好這個秒殺的功能不多不少,思路還有點意思,所以寫一下與大家分享。
秒殺的設計理念:
限流: 鑑於只有少部分使用者能夠秒殺成功,所以要限制大部分流量,只允許少部分流量進入服務後端。前臺頁面控制
削峰:對於秒殺系統瞬時會有大量使用者湧入,所以在搶購一開始會有很高的瞬間峰值。高峰值流量是壓垮系統很重要的原因,所以如何把瞬間的高流量變成一段時間平穩的流量也是設計秒殺系統很重要的思路。實現削峰的常用的方法有利用快取和訊息中介軟體(RabbitMQ)等技術。
非同步處理:秒殺系統是一個高併發系統,採用非同步處理模式可以極大地提高系統併發量,其實非同步處理就是削峰的一種實現方式。(RabbitMQ實現)
記憶體快取:秒殺系統最大的瓶頸一般都是資料庫讀寫,由於資料庫讀寫屬於磁碟IO,效能很低,如果能夠把部分資料或業務邏輯轉移到記憶體快取,效率會有極大地提升。
第一次嘗試:
純粹使用一臺redis實現秒殺,是有同步安全問題的
因為redis是支援高併發的,一秒可以承受10000次的請求,所以暫且使用一臺redis試試效果,畢竟單臺redis是單執行緒,併發安全問題會少一點。
首先建立秒殺商品表,這只是簡單的一個Demo,只需要id, title, price, num, KillTime 五個屬性,分別指代商品id,商品標題,商品價格,秒殺商品的數量,以及秒殺開始的時間。
這個我是模仿淘寶的整點搶購,每一個小時掃描一次秒殺商品表,將商品按搶購時間釋出出來,將id作為key,num作為value寫入redis,並設定消亡時間為1s(為了測試方便設了5秒)。
當用戶點選搶購按鈕,首先在前端進行控制,如果時間還沒到整點前後兩分鐘的區間,直接在前端攔截(沒寫),else才傳送請求,使用ajax與restful方式傳送請求的url,根據接收的引數,反饋不一樣的資訊。
後臺收到請求之後,首先根據id從redis中get對應的num,如果為null,返回”notbegin”,判斷num>0則decr自減,返回true,否則返回finished,如果catch到了錯誤,返回false。
前端function:
<script type="text/javascript"> function startKill(btn) { var id = $(btn).attr("id"); $.ajax({ type : "GET", url : "${app}/SecKill/startKill/" + id, dataType : 'text', success : function(data) { if (data == "true") { alert("恭喜,搶購成功"); } else if (data == "notbegin") { alert("活動還沒開始哦!"); } else if (data == "finished") { alert("商品已經搶完"); } else { alert("抱歉,搶購失敗"); } } }); } function tick(){ var today = new Date(); var timeString = today.toLocaleString(); $(".clock").innerHTML = timeString; window.setTimeout("tick();", 100); } window.onload = tick;
服務與後端代java碼略。
參考:
https://blog.csdn.net/mydistance/article/details/85236513
https://blog.csdn.net/a724888/article/details/81038138
https://blog.csdn.net/G626316/article/details/78650508
https://blog.csdn.net/sijg16/article/details/79144406