轉載
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:123456@192.168.10.1: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