#0 系列目錄#
秒殺系統(tǒng)架構(gòu)
秒殺系統(tǒng)架構(gòu)分析與實(shí)戰(zhàn)
#1 秒殺業(yè)務(wù)分析#
正常電子商務(wù)流程
(1)查詢商品枫笛;(2)創(chuàng)建訂單通今;(3)扣減庫存;(4)更新訂單进胯;(5)付款;(6)賣家發(fā)貨
秒殺業(yè)務(wù)的特性
(1)低廉價(jià)格原押;(2)大幅推廣胁镐;(3)瞬時(shí)售空;(4)一般是定時(shí)上架诸衔;(5)時(shí)間短盯漂、瞬時(shí)并發(fā)量高;
#2 秒殺技術(shù)挑戰(zhàn)# 假設(shè)某網(wǎng)站秒殺活動(dòng)只推出一件商品署隘,預(yù)計(jì)會(huì)吸引1萬人參加活動(dòng)宠能,也就說最大并發(fā)請(qǐng)求數(shù)是10000,秒殺系統(tǒng)需要面對(duì)的技術(shù)挑戰(zhàn)有:
對(duì)現(xiàn)有網(wǎng)站業(yè)務(wù)造成沖擊
秒殺活動(dòng)只是網(wǎng)站營銷的一個(gè)附加活動(dòng)磁餐,這個(gè)活動(dòng)具有時(shí)間短违崇,并發(fā)訪問量大的特點(diǎn),如果和網(wǎng)站原有應(yīng)用部署在一起诊霹,必然會(huì)對(duì)現(xiàn)有業(yè)務(wù)造成沖擊羞延,稍有不慎可能導(dǎo)致整個(gè)網(wǎng)站癱瘓。
解決方案:將秒殺系統(tǒng)獨(dú)立部署脾还,甚至使用獨(dú)立域名伴箩,使其與網(wǎng)站完全隔離。
高并發(fā)下的應(yīng)用鄙漏、數(shù)據(jù)庫負(fù)載
用戶在秒殺開始前嗤谚,通過不停刷新瀏覽器頁面以保證不會(huì)錯(cuò)過秒殺,這些請(qǐng)求如果按照一般的網(wǎng)站應(yīng)用架構(gòu)怔蚌,訪問應(yīng)用服務(wù)器巩步、連接數(shù)據(jù)庫,會(huì)對(duì)應(yīng)用服務(wù)器和數(shù)據(jù)庫服務(wù)器造成負(fù)載壓力桦踊。
解決方案:重新設(shè)計(jì)秒殺商品頁面椅野,不使用網(wǎng)站原來的商品詳細(xì)頁面,頁面內(nèi)容靜態(tài)化,用戶請(qǐng)求不需要經(jīng)過應(yīng)用服務(wù)竟闪。
突然增加的網(wǎng)絡(luò)及服務(wù)器帶寬
假設(shè)商品頁面大小200K(主要是商品圖片大欣敫!),那么需要的網(wǎng)絡(luò)和服務(wù)器帶寬是2G(200K×10000)炼蛤,這些網(wǎng)絡(luò)帶寬是因?yàn)槊霘⒒顒?dòng)新增的妖爷,超過網(wǎng)站平時(shí)使用的帶寬。
解決方案:因?yàn)槊霘⑿略龅木W(wǎng)絡(luò)帶寬鲸湃,必須和運(yùn)營商重新購買或者租借赠涮。為了減輕網(wǎng)站服務(wù)器的壓力,需要將秒殺商品頁面緩存在CDN暗挑,同樣需要和CDN服務(wù)商臨時(shí)租借新增的出口帶寬笋除。
直接下單
秒殺的游戲規(guī)則是到了秒殺才能開始對(duì)商品下單購買,在此時(shí)間點(diǎn)之前炸裆,只能瀏覽商品信息垃它,不能下單。而下單頁面也是一個(gè)普通的URL烹看,如果得到這個(gè)URL国拇,不用等到秒殺開始就可以下單了。
解決方案:為了避免用戶直接訪問下單頁面URL惯殊,需要將改URL動(dòng)態(tài)化酱吝,即使秒殺系統(tǒng)的開發(fā)者也無法在秒殺開始前訪問下單頁面的URL。辦法是在下單頁面URL加入由服務(wù)器端生成的隨機(jī)數(shù)作為參數(shù)土思,在秒殺開始的時(shí)候才能得到务热。
如何控制秒殺商品頁面購買按鈕的點(diǎn)亮
購買按鈕只有在秒殺開始的時(shí)候才能點(diǎn)亮,在此之前是灰色的己儒。如果該頁面是動(dòng)態(tài)生成的崎岂,當(dāng)然可以在服務(wù)器端構(gòu)造響應(yīng)頁面輸出,控制該按鈕是灰色還 是點(diǎn)亮闪湾,但是為了減輕服務(wù)器端負(fù)載壓力冲甘,更好地利用CDN、反向代理等性能優(yōu)化手段途样,該頁面被設(shè)計(jì)為靜態(tài)頁面江醇,緩存在CDN、反向代理服務(wù)器上何暇,甚至用戶瀏覽器上陶夜。秒殺開始時(shí),用戶刷新頁面赖晶,請(qǐng)求根本不會(huì)到達(dá)應(yīng)用服務(wù)器律适。
解決方案:使用JavaScript腳本控制,在秒殺商品靜態(tài)頁面中加入一個(gè)JavaScript文件引用遏插,該JavaScript文件中包含 秒殺開始標(biāo)志為否捂贿;當(dāng)秒殺開始的時(shí)候生成一個(gè)新的JavaScript文件(文件名保持不變,只是內(nèi)容不一樣)胳嘲,更新秒殺開始標(biāo)志為是厂僧,加入下單頁面的URL及隨機(jī)數(shù)參數(shù)(這個(gè)隨機(jī)數(shù)只會(huì)產(chǎn)生一個(gè),即所有人看到的URL都是同一個(gè)了牛,服務(wù)器端可以用redis這種分布式緩存服務(wù)器來保存隨機(jī)數(shù))颜屠,并被用戶瀏覽器加載,控制秒殺商品頁面的展示鹰祸。這個(gè)JavaScript文件的加載可以加上隨機(jī)版本號(hào)(例如xx.js?v=32353823)甫窟,這樣就不會(huì)被瀏覽器、CDN和反向代理服務(wù)器緩存蛙婴。
這個(gè)JavaScript文件非常小粗井,即使每次瀏覽器刷新都訪問JavaScript文件服務(wù)器也不會(huì)對(duì)服務(wù)器集群和網(wǎng)絡(luò)帶寬造成太大壓力。
如何只允許第一個(gè)提交的訂單被發(fā)送到訂單子系統(tǒng)
由于最終能夠成功秒殺到商品的用戶只有一個(gè)街图,因此需要在用戶提交訂單時(shí)浇衬,檢查是否已經(jīng)有訂單提交。如果已經(jīng)有訂單提交成功餐济,則需要更新 JavaScript文件耘擂,更新秒殺開始標(biāo)志為否,購買按鈕變灰絮姆。事實(shí)上醉冤,由于最終能夠成功提交訂單的用戶只有一個(gè),為了減輕下單頁面服務(wù)器的負(fù)載壓力滚朵, 可以控制進(jìn)入下單頁面的入口冤灾,只有少數(shù)用戶能進(jìn)入下單頁面,其他用戶直接進(jìn)入秒殺結(jié)束頁面辕近。
解決方案:假設(shè)下單服務(wù)器集群有10臺(tái)服務(wù)器韵吨,每臺(tái)服務(wù)器只接受最多10個(gè)下單請(qǐng)求。在還沒有人提交訂單成功之前移宅,如果一臺(tái)服務(wù)器已經(jīng)有十單了归粉,而有的一單都沒處理,可能出現(xiàn)的用戶體驗(yàn)不佳的場(chǎng)景是用戶第一次點(diǎn)擊購買按鈕進(jìn)入已結(jié)束頁面漏峰,再刷新一下頁面糠悼,有可能被一單都沒有處理的服務(wù)器處理,進(jìn)入了填寫訂單的頁面浅乔,可以考慮通過cookie的方式來應(yīng)對(duì)倔喂,符合一致性原則铝条。當(dāng)然可以采用最少連接的負(fù)載均衡算法,出現(xiàn)上述情況的概率大大降低席噩。
如何進(jìn)行下單前置檢查
下單服務(wù)器檢查本機(jī)已處理的下單請(qǐng)求數(shù)目:
如果超過10條班缰,直接返回已結(jié)束頁面給用戶;
如果未超過10條悼枢,則用戶可進(jìn)入填寫訂單及確認(rèn)頁面埠忘;
檢查全局已提交訂單數(shù)目:
已超過秒殺商品總數(shù),返回已結(jié)束頁面給用戶馒索;
未超過秒殺商品總數(shù)莹妒,提交到子訂單系統(tǒng);
秒殺一般是定時(shí)上架
該功能實(shí)現(xiàn)方式很多绰上。不過目前比較好的方式是:提前設(shè)定好商品的上架時(shí)間旨怠,用戶可以在前臺(tái)看到該商品,但是無法點(diǎn)擊“立即購買”的按鈕蜈块。但是需要考慮的是运吓,有人可以繞過前端的限制,直接通過URL的方式發(fā)起購買疯趟,這就需要在前臺(tái)商品頁面拘哨,以及bug頁面到后端的數(shù)據(jù)庫,都要進(jìn)行時(shí)鐘同步信峻。越在后端控制倦青,安全性越高纳决。
定時(shí)秒殺的話硫狞,就要避免賣家在秒殺前對(duì)商品做編輯帶來的不可預(yù)期的影響。這種特殊的變更需要多方面評(píng)估九串。一般禁止編輯踢步,如需變更癣亚,可以走數(shù)據(jù)訂正多的流程。
減庫存的操作
有兩種選擇获印,一種是拍下減庫存 另外一種是付款減庫存述雾;目前采用的“拍下減庫存”的方式,拍下就是一瞬間的事兼丰,對(duì)用戶體驗(yàn)會(huì)好些玻孟。
庫存會(huì)帶來“超賣”的問題:售出數(shù)量多于庫存數(shù)量
由于庫存并發(fā)更新的問題,導(dǎo)致在實(shí)際庫存已經(jīng)不足的情況下鳍征,庫存依然在減黍翎,導(dǎo)致賣家的商品賣得件數(shù)超過秒殺的預(yù)期。方案:采用樂觀鎖
update auction_auctions set
quantity = #inQuantity#
where auction_id = #itemId# and quantity = #dbQuantity#
還有一種方式艳丛,會(huì)更好些匣掸,叫做嘗試扣減庫存趟紊,扣減庫存成功才會(huì)進(jìn)行下單邏輯:
update auction_auctions set?
quantity = quantity-#count#?
where auction_id = #itemId# and quantity >= #count#
秒殺器的應(yīng)對(duì)
秒殺器一般下單個(gè)購買及其迅速,根據(jù)購買記錄可以甄別出一部分碰酝≈簦可以通過校驗(yàn)碼達(dá)到一定的方法,這就要求校驗(yàn)碼足夠安全砰粹,不被破解,采用的方式有:秒殺專用驗(yàn)證碼造挽,電視公布驗(yàn)證碼碱璃,秒殺答題。
#3 秒殺架構(gòu)原則#
盡量將請(qǐng)求攔截在系統(tǒng)上游
傳統(tǒng)秒殺系統(tǒng)之所以掛饭入,請(qǐng)求都?jí)旱沽撕蠖藬?shù)據(jù)層嵌器,數(shù)據(jù)讀寫鎖沖突嚴(yán)重,并發(fā)高響應(yīng)慢谐丢,幾乎所有請(qǐng)求都超時(shí)爽航,流量雖大,下單成功的有效流量甚小【一趟火車其實(shí)只有2000張票乾忱,200w個(gè)人來買讥珍,基本沒有人能買成功,請(qǐng)求有效率為0】窄瘟。
讀多寫少的常用多使用緩存
這是一個(gè)典型的讀多寫少的應(yīng)用場(chǎng)景【一趟火車其實(shí)只有2000張票衷佃,200w個(gè)人來買,最多2000個(gè)人下單成功蹄葱,其他人都是查詢庫存氏义,寫比例只有0.1%,讀比例占99.9%】图云,非常適合使用緩存惯悠。
#4 秒殺架構(gòu)設(shè)計(jì)# 秒殺系統(tǒng)為秒殺而設(shè)計(jì),不同于一般的網(wǎng)購行為竣况,參與秒殺活動(dòng)的用戶更關(guān)心的是如何能快速刷新商品頁面克婶,在秒殺開始的時(shí)候搶先進(jìn)入下單頁面,而不是商品詳情等用戶體驗(yàn)細(xì)節(jié)丹泉,因此秒殺系統(tǒng)的頁面設(shè)計(jì)應(yīng)盡可能簡(jiǎn)單鸠补。
商品頁面中的購買按鈕只有在秒殺活動(dòng)開始的時(shí)候才變亮,在此之前及秒殺商品賣出后嘀掸,該按鈕都是灰色的紫岩,不可以點(diǎn)擊。
下單表單也盡可能簡(jiǎn)單睬塌,購買數(shù)量只能是一個(gè)且不可以修改泉蝌,送貨地址和付款方式都使用用戶默認(rèn)設(shè)置歇万,沒有默認(rèn)也可以不填,允許等訂單提交后修改勋陪;只有第一個(gè)提交的訂單發(fā)送給網(wǎng)站的訂單子系統(tǒng)贪磺,其余用戶提交訂單后只能看到秒殺結(jié)束頁面。
要做一個(gè)這樣的秒殺系統(tǒng)诅愚,業(yè)務(wù)會(huì)分為兩個(gè)階段寒锚,第一個(gè)階段是秒殺開始前某個(gè)時(shí)間到秒殺開始, 這個(gè)階段可以稱之為準(zhǔn)備階段违孝,用戶在準(zhǔn)備階段等待秒殺刹前; 第二個(gè)階段就是秒殺開始到所有參與秒殺的用戶獲得秒殺結(jié)果, 這個(gè)就稱為秒殺階段吧雌桑。
##4.1 前端層設(shè)計(jì)## 首先要有一個(gè)展示秒殺商品的頁面喇喉, 在這個(gè)頁面上做一個(gè)秒殺活動(dòng)開始的倒計(jì)時(shí), 在準(zhǔn)備階段內(nèi)用戶會(huì)陸續(xù)打開這個(gè)秒殺的頁面校坑, 并且可能不停的刷新頁面拣技。這里需要考慮兩個(gè)問題:
第一個(gè)是秒殺頁面的展示
我們知道一個(gè)html頁面還是比較大的,即使做了壓縮耍目,http頭和內(nèi)容的大小也可能高達(dá)數(shù)十K膏斤,加上其他的css, js邪驮,圖片等資源掸绞,如果同時(shí)有幾千萬人參與一個(gè)商品的搶購,一般機(jī)房帶寬也就只有1G~10G耕捞,網(wǎng)絡(luò)帶寬就極有可能成為瓶頸衔掸,所以這個(gè)頁面上各類靜態(tài)資源首先應(yīng)分開存放,然后放到cdn節(jié)點(diǎn)上分散壓力俺抽,由于CDN節(jié)點(diǎn)遍布全國各地敞映,能緩沖掉絕大部分的壓力,而且還比機(jī)房帶寬便宜~
第二個(gè)是倒計(jì)時(shí)
出于性能原因這個(gè)一般由js調(diào)用客戶端本地時(shí)間磷斧,就有可能出現(xiàn)客戶端時(shí)鐘與服務(wù)器時(shí)鐘不一致振愿,另外服務(wù)器之間也是有可能出現(xiàn)時(shí)鐘不一致〕诜梗客戶端與服務(wù)器時(shí)鐘不一致可以采用客戶端定時(shí)和服務(wù)器同步時(shí)間冕末,這里考慮一下性能問題,用于同步時(shí)間的接口由于不涉及到后端邏輯侣颂,只需要將當(dāng)前web服務(wù)器的時(shí)間發(fā)送給客戶端就可以了档桃,因此速度很快,就我以前測(cè)試的結(jié)果來看憔晒,一臺(tái)標(biāo)準(zhǔn)的web服務(wù)器2W+QPS不會(huì)有問題藻肄,如果100W人同時(shí)刷蔑舞,100W QPS也只需要50臺(tái)web,一臺(tái)硬件LB就可以了~嘹屯,并且web服務(wù)器群是可以很容易的橫向擴(kuò)展的(LB+DNS輪詢)攻询,這個(gè)接口可以只返回一小段json格式的數(shù)據(jù),而且可以優(yōu)化一下減少不必要cookie和其他http頭的信息州弟,所以數(shù)據(jù)量不會(huì)很大钧栖,一般來說網(wǎng)絡(luò)不會(huì)成為瓶頸,即使成為瓶頸也可以考慮多機(jī)房專線連通婆翔,加智能DNS的解決方案拯杠;web服務(wù)器之間時(shí)間不同步可以采用統(tǒng)一時(shí)間服務(wù)器的方式,比如每隔1分鐘所有參與秒殺活動(dòng)的web服務(wù)器就與時(shí)間服務(wù)器做一次時(shí)間同步浙滤。
瀏覽器層請(qǐng)求攔截
(1)產(chǎn)品層面,用戶點(diǎn)擊“查詢”或者“購票”后气堕,按鈕置灰纺腊,禁止用戶重復(fù)提交請(qǐng)求;
(2)JS層面,限制用戶在x秒之內(nèi)只能提交一次請(qǐng)求;
##4.2 站點(diǎn)層設(shè)計(jì)## 前端層的請(qǐng)求攔截茎芭,只能攔住小白用戶(不過這是99%的用戶喲)揖膜,高端的程序員根本不吃這一套,寫個(gè)for循環(huán)梅桩,直接調(diào)用你后端的http請(qǐng)求壹粟,怎么整?
(1)同一個(gè)uid宿百,限制訪問頻度趁仙,做頁面緩存,x秒內(nèi)到達(dá)站點(diǎn)層的請(qǐng)求垦页,均返回同一頁面
(2)同一個(gè)item的查詢雀费,例如手機(jī)車次,做頁面緩存痊焊,x秒內(nèi)到達(dá)站點(diǎn)層的請(qǐng)求盏袄,均返回同一頁面
如此限流,又有99%的流量會(huì)被攔截在站點(diǎn)層薄啥。 ##4.3 服務(wù)層設(shè)計(jì)## 站點(diǎn)層的請(qǐng)求攔截辕羽,只能攔住普通程序員,高級(jí)***垄惧,假設(shè)他控制了10w臺(tái)肉雞(并且假設(shè)買票不需要實(shí)名認(rèn)證)刁愿,這下uid的限制不行了吧?怎么整到逊?
(1)大哥酌毡,我是服務(wù)層克握,我清楚的知道小米只有1萬部手機(jī),我清楚的知道一列火車只有2000張車票枷踏,我透10w個(gè)請(qǐng)求去數(shù)據(jù)庫有什么意義呢菩暗?對(duì)于寫請(qǐng)求,做請(qǐng)求隊(duì)列旭蠕,每次只透過有限的寫請(qǐng)求去數(shù)據(jù)層停团,如果均成功再放下一批,如果庫存不夠則隊(duì)列里的寫請(qǐng)求全部返回“已售完”掏熬;
(2)對(duì)于讀請(qǐng)求佑稠,還用說么?cache來抗旗芬,不管是memcached還是redis舌胶,單機(jī)抗個(gè)每秒10w應(yīng)該都是沒什么問題的;
如此限流疮丛,只有非常少的寫請(qǐng)求幔嫂,和非常少的讀緩存mis的請(qǐng)求會(huì)透到數(shù)據(jù)層去,又有99.9%的請(qǐng)求被攔住了誊薄。
用戶請(qǐng)求分發(fā)模塊:使用Nginx或Apache將用戶的請(qǐng)求分發(fā)到不同的機(jī)器上履恩。
用戶請(qǐng)求預(yù)處理模塊:判斷商品是不是還有剩余來決定是不是要處理該請(qǐng)求。
用戶請(qǐng)求處理模塊:把通過預(yù)處理的請(qǐng)求封裝成事務(wù)提交給數(shù)據(jù)庫呢蔫,并返回是否成功切心。
數(shù)據(jù)庫接口模塊:該模塊是數(shù)據(jù)庫的唯一接口,負(fù)責(zé)與數(shù)據(jù)庫交互片吊,提供RPC接口供查詢是否秒殺結(jié)束绽昏、剩余數(shù)量等信息。
用戶請(qǐng)求預(yù)處理模塊
經(jīng)過HTTP服務(wù)器的分發(fā)后俏脊,單個(gè)服務(wù)器的負(fù)載相對(duì)低了一些而涉,但總量依然可能很大,如果后臺(tái)商品已經(jīng)被秒殺完畢联予,那么直接給后來的請(qǐng)求返回秒殺失敗即可啼县,不必再進(jìn)一步發(fā)送事務(wù)了,示例代碼可以如下所示:
package seckill;
import org.apache.http.HttpRequest;
/**
預(yù)處理階段沸久,把不必要的請(qǐng)求直接駁回季眷,必要的請(qǐng)求添加到隊(duì)列中進(jìn)入下一階段.
*/
public class PreProcessor {
// 商品是否還有剩余
private static boolean reminds = true;
private static void forbidden() {
// Do something.
}
public static boolean checkReminds() {
if (reminds) {
// 遠(yuǎn)程檢測(cè)是否還有剩余,該RPC接口應(yīng)由數(shù)據(jù)庫服務(wù)器提供卷胯,不必完全嚴(yán)格檢查.
if (!RPC.checkReminds()) {
reminds = false;
}
}
return reminds;
}
/**
每一個(gè)HTTP請(qǐng)求都要經(jīng)過該預(yù)處理.
*/
public static void preProcess(HttpRequest request) {
if (checkReminds()) {
// 一個(gè)并發(fā)的隊(duì)列
RequestQueue.queue.add(request);
} else {
// 如果已經(jīng)沒有商品了子刮,則直接駁回請(qǐng)求即可.
forbidden();
}
}
}
并發(fā)隊(duì)列的選擇
Java的并發(fā)包提供了三個(gè)常用的并發(fā)隊(duì)列實(shí)現(xiàn),分別是:ConcurrentLinkedQueue 、 LinkedBlockingQueue 和 ArrayBlockingQueue挺峡。
ArrayBlockingQueue是初始容量固定的阻塞隊(duì)列葵孤,我們可以用來作為數(shù)據(jù)庫模塊成功競(jìng)拍的隊(duì)列,比如有10個(gè)商品橱赠,那么我們就設(shè)定一個(gè)10大小的數(shù)組隊(duì)列尤仍。
ConcurrentLinkedQueue使用的是CAS原語無鎖隊(duì)列實(shí)現(xiàn),是一個(gè)異步隊(duì)列狭姨,入隊(duì)的速度很快宰啦,出隊(duì)進(jìn)行了加鎖,性能稍慢饼拍。
LinkedBlockingQueue也是阻塞的隊(duì)列赡模,入隊(duì)和出隊(duì)都用了加鎖,當(dāng)隊(duì)空的時(shí)候線程會(huì)暫時(shí)阻塞师抄。
由于我們的系統(tǒng)入隊(duì)需求要遠(yuǎn)大于出隊(duì)需求漓柑,一般不會(huì)出現(xiàn)隊(duì)空的情況,所以我們可以選擇ConcurrentLinkedQueue來作為我們的請(qǐng)求隊(duì)列實(shí)現(xiàn):
package seckill;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ConcurrentLinkedQueue;
import org.apache.http.HttpRequest;
public class RequestQueue {
public static ConcurrentLinkedQueue queue = new ConcurrentLinkedQueue();
}
> 用戶請(qǐng)求模塊
package seckill;
import org.apache.http.HttpRequest;
public class Processor {
/**
發(fā)送秒殺事務(wù)到數(shù)據(jù)庫隊(duì)列.
*/
public static void kill(BidInfo info) {
DB.bids.add(info);
}
public static void process() {
BidInfo info = new BidInfo(RequestQueue.queue.poll());
if (info != null) {
kill(info);
}
}
}
class BidInfo {
BidInfo(HttpRequest request) {
// Do something.
}
}
數(shù)據(jù)庫模塊
數(shù)據(jù)庫主要是使用一個(gè)ArrayBlockingQueue來暫存有可能成功的用戶請(qǐng)求叨吮。
package seckill;
import java.util.concurrent.ArrayBlockingQueue;
/**
DB應(yīng)該是數(shù)據(jù)庫的唯一接口.
*/
public class DB {
public static int count = 10;
public static ArrayBlockingQueue bids = new ArrayBlockingQueue(10);
public static boolean checkReminds() {
// TODO
return true;
}
// 單線程操作
public static void bid() {
BidInfo info = bids.poll();
while (count-- > 0) {
// insert into table Bids values(item_id, user_id, bid_date, other)
// select count(id) from Bids where item_id = ?
// 如果數(shù)據(jù)庫商品數(shù)量大約總數(shù)辆布,則標(biāo)志秒殺已完成,設(shè)置標(biāo)志位reminds = false.
info = bids.poll();
}
}
}
##4.4 數(shù)據(jù)庫設(shè)計(jì)## ###4.4.1 基本概念### 概念一“單庫”
概念二“分片”
分片解決的是“數(shù)據(jù)量太大”的問題挤安,也就是通常說的“水平切分”谚殊。一旦引入分片丧鸯,勢(shì)必有“數(shù)據(jù)路由”的概念蛤铜,哪個(gè)數(shù)據(jù)訪問哪個(gè)庫。路由規(guī)則通常有3種方法:
范圍:range
優(yōu)點(diǎn):簡(jiǎn)單丛肢,容易擴(kuò)展
缺點(diǎn):各庫壓力不均(新號(hào)段更活躍)
哈希:hash 【大部分互聯(lián)網(wǎng)公司采用的方案二:哈希分庫围肥,哈希路由】
優(yōu)點(diǎn):簡(jiǎn)單,數(shù)據(jù)均衡蜂怎,負(fù)載均勻
缺點(diǎn):遷移麻煩(2庫擴(kuò)3庫數(shù)據(jù)要遷移)
路由服務(wù):router-config-server
優(yōu)點(diǎn):靈活性強(qiáng)穆刻,業(yè)務(wù)與路由算法解耦
缺點(diǎn):每次訪問數(shù)據(jù)庫前多一次查詢
概念三“分組”
分組解決“可用性”問題,分組通常通過主從復(fù)制的方式實(shí)現(xiàn)杠步。
互聯(lián)網(wǎng)公司數(shù)據(jù)庫實(shí)際軟件架構(gòu)是:又分片氢伟,又分組(如下圖)
###4.4.2 設(shè)計(jì)思路### 數(shù)據(jù)庫軟件架構(gòu)師平時(shí)設(shè)計(jì)些什么東西呢?至少要考慮以下四點(diǎn):
如何保證數(shù)據(jù)可用性幽歼;
如何提高數(shù)據(jù)庫讀性能(大部分應(yīng)用讀多寫少朵锣,讀會(huì)先成為瓶頸);
如何保證一致性甸私;
如何提高擴(kuò)展性诚些;
如何保證數(shù)據(jù)的可用性?
解決可用性問題的思路是=>冗余
如何保證站點(diǎn)的可用性皇型?復(fù)制站點(diǎn)诬烹,冗余站點(diǎn)
如何保證服務(wù)的可用性砸烦?復(fù)制服務(wù),冗余服務(wù)
如何保證數(shù)據(jù)的可用性绞吁?復(fù)制數(shù)據(jù)幢痘,冗余數(shù)據(jù)
數(shù)據(jù)的冗余,會(huì)帶來一個(gè)副作用=>引發(fā)一致性問題(先不說一致性問題掀泳,先說可用性)雪隧。
如何保證數(shù)據(jù)庫“讀”高可用?
冗余讀庫
冗余讀庫帶來的副作用员舵?讀寫有延時(shí)脑沿,可能不一致
上面這個(gè)圖是很多互聯(lián)網(wǎng)公司mysql的架構(gòu),寫仍然是單點(diǎn)马僻,不能保證寫高可用庄拇。
如何保證數(shù)據(jù)庫“寫”高可用?
冗余寫庫
采用雙主互備的方式韭邓,可以冗余寫庫帶來的副作用措近?雙寫同步,數(shù)據(jù)可能沖突(例如“自增id”同步?jīng)_突),如何解決同步?jīng)_突女淑,有兩種常見解決方案:
兩個(gè)寫庫使用不同的初始值瞭郑,相同的步長來增加id:1寫庫的id為0,2,4,6...;2寫庫的id為1,3,5,7...鸭你;
不使用數(shù)據(jù)的id屈张,業(yè)務(wù)層自己生成唯一的id,保證數(shù)據(jù)不沖突袱巨;
實(shí)際中沒有使用上述兩種架構(gòu)來做讀寫的“高可用”阁谆,采用的是“雙主當(dāng)主從用”的方式:
仍是雙主,但只有一個(gè)主提供服務(wù)(讀+寫)愉老,另一個(gè)主是“shadow-master”场绿,只用來保證高可用,平時(shí)不提供服務(wù)嫉入。 master掛了焰盗,shadow-master頂上(vip漂移,對(duì)業(yè)務(wù)層透明咒林,不需要人工介入)熬拒。這種方式的好處:
讀寫沒有延時(shí);
讀寫高可用映九;
不足:
不能通過加從庫的方式擴(kuò)展讀性能梦湘;
資源利用率為50%,一臺(tái)冗余主沒有提供服務(wù);
那如何提高讀性能呢捌议?進(jìn)入第二個(gè)話題哼拔,如何提供讀性能。
如何擴(kuò)展讀性能
提高讀性能的方式大致有三種瓣颅,第一種是建立索引倦逐。這種方式不展開,要提到的一點(diǎn)是宫补,不同的庫可以建立不同的索引檬姥。
寫庫不建立索引;
線上讀庫建立線上訪問索引粉怕,例如uid健民;
線下讀庫建立線下訪問索引,例如time贫贝;
第二種擴(kuò)充讀性能的方式是秉犹,增加從庫,這種方法大家用的比較多稚晚,但是崇堵,存在兩個(gè)缺點(diǎn):
從庫越多,同步越慢客燕;
同步越慢鸳劳,數(shù)據(jù)不一致窗口越大(不一致后面說,還是先說讀性能的提高)也搓;
實(shí)際中沒有采用這種方法提高數(shù)據(jù)庫讀性能(沒有從庫)赏廓,采用的是增加緩存。常見的緩存架構(gòu)如下:
上游是業(yè)務(wù)應(yīng)用还绘,下游是主庫楚昭,從庫(讀寫分離)栖袋,緩存拍顷。
實(shí)際的玩法:服務(wù)+數(shù)據(jù)庫+緩存一套
業(yè)務(wù)層不直接面向db和cache,服務(wù)層屏蔽了底層db塘幅、cache的復(fù)雜性昔案。為什么要引入服務(wù)層,今天不展開电媳,采用了“服務(wù)+數(shù)據(jù)庫+緩存一套”的方式提供數(shù)據(jù)訪問踏揣,用cache提高讀性能。
不管采用主從的方式擴(kuò)展讀性能匾乓,還是緩存的方式擴(kuò)展讀性能捞稿,數(shù)據(jù)都要復(fù)制多份(主+從,db+cache),一定會(huì)引發(fā)一致性問題娱局。
如何保證一致性彰亥?
主從數(shù)據(jù)庫的一致性,通常有兩種解決方案:
中間件
如果某一個(gè)key有寫操作衰齐,在不一致時(shí)間窗口內(nèi)任斋,中間件會(huì)將這個(gè)key的讀操作也路由到主庫上。這個(gè)方案的缺點(diǎn)是耻涛,數(shù)據(jù)庫中間件的門檻較高(百度废酷,騰訊,阿里抹缕,360等一些公司有)澈蟆。
強(qiáng)制讀主
上面實(shí)際用的“雙主當(dāng)主從用”的架構(gòu),不存在主從不一致的問題卓研。
第二類不一致丰介,是db與緩存間的不一致:
常見的緩存架構(gòu)如上,此時(shí)寫操作的順序是:
(1)淘汰cache鉴分;
(2)寫數(shù)據(jù)庫哮幢;
讀操作的順序是:
(1)讀cache,如果cache hit則返回志珍;
(2)如果cache miss橙垢,則讀從庫;
(3)讀從庫后伦糯,將數(shù)據(jù)放回cache柜某;
在一些異常時(shí)序情況下,有可能從【從庫讀到舊數(shù)據(jù)(同步還沒有完成)敛纲,舊數(shù)據(jù)入cache后】喂击,數(shù)據(jù)會(huì)長期不一致。解決辦法是“緩存雙淘汰”淤翔,寫操作時(shí)序升級(jí)為:
(1)淘汰cache翰绊;
(2)寫數(shù)據(jù)庫;
(3)在經(jīng)驗(yàn)“主從同步延時(shí)窗口時(shí)間”后旁壮,再次發(fā)起一個(gè)異步淘汰cache的請(qǐng)求监嗜;
這樣,即使有臟數(shù)據(jù)如cache抡谐,一個(gè)小的時(shí)間窗口之后裁奇,臟數(shù)據(jù)還是會(huì)被淘汰。帶來的代價(jià)是麦撵,多引入一次讀miss(成本可以忽略)刽肠。
除此之外溃肪,最佳實(shí)踐之一是:建議為所有cache中的item設(shè)置一個(gè)超時(shí)時(shí)間。
如何提高數(shù)據(jù)庫的擴(kuò)展性音五?
原來用hash的方式路由乍惊,分為2個(gè)庫,數(shù)據(jù)量還是太大放仗,要分為3個(gè)庫润绎,勢(shì)必需要進(jìn)行數(shù)據(jù)遷移,有一個(gè)很帥氣的“數(shù)據(jù)庫秒級(jí)擴(kuò)容”方案诞挨。
如何秒級(jí)擴(kuò)容莉撇?
首先,我們不做2庫變3庫的擴(kuò)容惶傻,我們做2庫變4庫(庫加倍)的擴(kuò)容(未來4->8->16)
服務(wù)+數(shù)據(jù)庫是一套(省去了緩存)棍郎,數(shù)據(jù)庫采用“雙主”的模式。
擴(kuò)容步驟:
第一步银室,將一個(gè)主庫提升;
第二步涂佃,修改配置,2庫變4庫(原來MOD2蜈敢,現(xiàn)在配置修改后MOD4)辜荠,擴(kuò)容完成;
原MOD2為偶的部分抓狭,現(xiàn)在會(huì)MOD4余0或者2伯病;原MOD2為奇的部分,現(xiàn)在會(huì)MOD4余1或者3否过;數(shù)據(jù)不需要遷移午笛,同時(shí),雙主互相同步苗桂,一遍是余0药磺,一邊余2,兩邊數(shù)據(jù)同步也不會(huì)沖突煤伟,秒級(jí)完成擴(kuò)容癌佩!
最后,要做一些收尾工作:
將舊的雙主同步解除持偏;
增加新的雙主(雙主是保證可用性的驼卖,shadow-master平時(shí)不提供服務(wù))氨肌;
刪除多余的數(shù)據(jù)(余0的主鸿秆,可以將余2的數(shù)據(jù)刪除掉);
這樣怎囚,秒級(jí)別內(nèi)卿叽,我們就完成了2庫變4庫的擴(kuò)展桥胞。
#5 大并發(fā)帶來的挑戰(zhàn)# ##5.1 請(qǐng)求接口的合理設(shè)計(jì)## 一個(gè)秒殺或者搶購頁面,通常分為2個(gè)部分考婴,一個(gè)是靜態(tài)的HTML等內(nèi)容贩虾,另一個(gè)就是參與秒殺的Web后臺(tái)請(qǐng)求接口。
通常靜態(tài)HTML等內(nèi)容沥阱,是通過CDN的部署缎罢,一般壓力不大,核心瓶頸實(shí)際上在后臺(tái)請(qǐng)求接口上考杉。這個(gè)后端接口策精,必須能夠支持高并發(fā)請(qǐng)求,同時(shí)崇棠,非常重要的一點(diǎn)咽袜,必須盡可能“快”,在最短的時(shí)間里返回用戶的請(qǐng)求結(jié)果枕稀。為了實(shí)現(xiàn)盡可能快這一點(diǎn)询刹,接口的后端存儲(chǔ)使用內(nèi)存級(jí)別的操作會(huì)更好一點(diǎn)。仍然直接面向MySQL之類的存儲(chǔ)是不合適的萎坷,如果有這種復(fù)雜業(yè)務(wù)的需求凹联,都建議采用異步寫入。
當(dāng)然哆档,也有一些秒殺和搶購采用“滯后反饋”匕垫,就是說秒殺當(dāng)下不知道結(jié)果,一段時(shí)間后才可以從頁面中看到用戶是否秒殺成功虐呻。但是象泵,這種屬于“偷懶”行為,同時(shí)給用戶的體驗(yàn)也不好斟叼,容易被用戶認(rèn)為是“暗箱操作”偶惠。
##5.2 高并發(fā)的挑戰(zhàn):一定要“快”## 我們通常衡量一個(gè)Web系統(tǒng)的吞吐率的指標(biāo)是QPS(Query Per Second,每秒處理請(qǐng)求數(shù))朗涩,解決每秒數(shù)萬次的高并發(fā)場(chǎng)景忽孽,這個(gè)指標(biāo)非常關(guān)鍵。舉個(gè)例子谢床,我們假設(shè)處理一個(gè)業(yè)務(wù)請(qǐng)求平均響應(yīng)時(shí)間為100ms兄一,同時(shí),系統(tǒng)內(nèi)有20臺(tái)Apache的Web服務(wù)器识腿,配置MaxClients為500個(gè)(表示Apache的最大連接數(shù)目)出革。
那么,我們的Web系統(tǒng)的理論峰值QPS為(理想化的計(jì)算方式):
20*500/0.1 = 100000 (10萬QPS)
咦渡讼?我們的系統(tǒng)似乎很強(qiáng)大骂束,1秒鐘可以處理完10萬的請(qǐng)求耳璧,5w/s的秒殺似乎是“紙老虎”哈。實(shí)際情況展箱,當(dāng)然沒有這么理想旨枯。在高并發(fā)的實(shí)際場(chǎng)景下,機(jī)器都處于高負(fù)載的狀態(tài)混驰,在這個(gè)時(shí)候平均響應(yīng)時(shí)間會(huì)被大大增加攀隔。
就Web服務(wù)器而言,Apache打開了越多的連接進(jìn)程栖榨,CPU需要處理的上下文切換也越多竞慢,額外增加了CPU的消耗,然后就直接導(dǎo)致平均響應(yīng)時(shí)間增加治泥。因此上述的MaxClient數(shù)目筹煮,要根據(jù)CPU、內(nèi)存等硬件因素綜合考慮居夹,絕對(duì)不是越多越好败潦。可以通過Apache自帶的abench來測(cè)試一下准脂,取一個(gè)合適的值劫扒。然后,我們選擇內(nèi)存操作級(jí)別的存儲(chǔ)的Redis狸膏,在高并發(fā)的狀態(tài)下沟饥,存儲(chǔ)的響應(yīng)時(shí)間至關(guān)重要。網(wǎng)絡(luò)帶寬雖然也是一個(gè)因素湾戳,不過贤旷,這種請(qǐng)求數(shù)據(jù)包一般比較小,一般很少成為請(qǐng)求的瓶頸砾脑。負(fù)載均衡成為系統(tǒng)瓶頸的情況比較少幼驶,在這里不做討論哈统诺。
那么問題來了侠讯,假設(shè)我們的系統(tǒng)伙菊,在5w/s的高并發(fā)狀態(tài)下碌尔,平均響應(yīng)時(shí)間從100ms變?yōu)?50ms(實(shí)際情況,甚至更多):
20*500/0.25 = 40000 (4萬QPS)
于是梭伐,我們的系統(tǒng)剩下了4w的QPS读整,面對(duì)5w每秒的請(qǐng)求狂秘,中間相差了1w硕噩。
然后假残,這才是真正的惡夢(mèng)開始。舉個(gè)例子榴徐,高速路口守问,1秒鐘來5部車匀归,每秒通過5部車坑资,高速路口運(yùn)作正常耗帕。突然,這個(gè)路口1秒鐘只能通過4部車袱贮,車流量仍然依舊仿便,結(jié)果必定出現(xiàn)大塞車。(5條車道忽然變成4條車道的感覺)攒巍。
同理嗽仪,某一個(gè)秒內(nèi),20*500個(gè)可用連接進(jìn)程都在滿負(fù)荷工作中柒莉,卻仍然有1萬個(gè)新來請(qǐng)求闻坚,沒有連接進(jìn)程可用,系統(tǒng)陷入到異常狀態(tài)也是預(yù)期之內(nèi)兢孝。
其實(shí)在正常的非高并發(fā)的業(yè)務(wù)場(chǎng)景中窿凤,也有類似的情況出現(xiàn),某個(gè)業(yè)務(wù)請(qǐng)求接口出現(xiàn)問題跨蟹,響應(yīng)時(shí)間極慢雳殊,將整個(gè)Web請(qǐng)求響應(yīng)時(shí)間拉得很長,逐漸將Web服務(wù)器的可用連接數(shù)占滿窗轩,其他正常的業(yè)務(wù)請(qǐng)求夯秃,無連接進(jìn)程可用。
更可怕的問題是痢艺,是用戶的行為特點(diǎn)仓洼,系統(tǒng)越是不可用,用戶的點(diǎn)擊越頻繁堤舒,惡性循環(huán)最終導(dǎo)致“雪崩”(其中一臺(tái)Web機(jī)器掛了衬潦,導(dǎo)致流量分散到其他正常工作的機(jī)器上,再導(dǎo)致正常的機(jī)器也掛植酥,然后惡性循環(huán))镀岛,將整個(gè)Web系統(tǒng)拖垮。
##5.3 重啟與過載保護(hù)## 如果系統(tǒng)發(fā)生“雪崩”友驮,貿(mào)然重啟服務(wù)漂羊,是無法解決問題的。最常見的現(xiàn)象是卸留,啟動(dòng)起來后走越,立刻掛掉。這個(gè)時(shí)候耻瑟,最好在入口層將流量拒絕旨指,然后再將重啟赏酥。如果是redis/memcache這種服務(wù)也掛了,重啟的時(shí)候需要注意“預(yù)熱”谆构,并且很可能需要比較長的時(shí)間裸扶。
秒殺和搶購的場(chǎng)景,流量往往是超乎我們系統(tǒng)的準(zhǔn)備和想象的搬素。這個(gè)時(shí)候呵晨,過載保護(hù)是必要的。如果檢測(cè)到系統(tǒng)滿負(fù)載狀態(tài)熬尺,拒絕請(qǐng)求也是一種保護(hù)措施摸屠。在前端設(shè)置過濾是最簡(jiǎn)單的方式,但是粱哼,這種做法是被用戶“千夫所指”的行為季二。更合適一點(diǎn)的是,將過載保護(hù)設(shè)置在CGI入口層揭措,快速將客戶的直接請(qǐng)求返回胯舷。
#6 作弊的手段:進(jìn)攻與防守# 秒殺和搶購收到了“海量”的請(qǐng)求,實(shí)際上里面的水分是很大的蜂筹。不少用戶需纳,為了“搶“到商品,會(huì)使用“刷票工具”等類型的輔助工具艺挪,幫助他們發(fā)送盡可能多的請(qǐng)求到服務(wù)器不翩。還有一部分高級(jí)用戶,制作強(qiáng)大的自動(dòng)請(qǐng)求腳本麻裳。這種做法的理由也很簡(jiǎn)單口蝠,就是在參與秒殺和搶購的請(qǐng)求中,自己的請(qǐng)求數(shù)目占比越多津坑,成功的概率越高妙蔗。
這些都是屬于“作弊的手段”,不過疆瑰,有“進(jìn)攻”就有“防守”眉反,這是一場(chǎng)沒有硝煙的戰(zhàn)斗哈。
##6.1 同一個(gè)賬號(hào)穆役,一次性發(fā)出多個(gè)請(qǐng)求## 部分用戶通過瀏覽器的插件或者其他工具寸五,在秒殺開始的時(shí)間里,以自己的賬號(hào)耿币,一次發(fā)送上百甚至更多的請(qǐng)求梳杏。實(shí)際上,這樣的用戶破壞了秒殺和搶購的公平性。
這種請(qǐng)求在某些沒有做數(shù)據(jù)安全處理的系統(tǒng)里十性,也可能造成另外一種破壞叛溢,導(dǎo)致某些判斷條件被繞過。例如一個(gè)簡(jiǎn)單的領(lǐng)取邏輯劲适,先判斷用戶是否有參與記錄楷掉,如果沒有則領(lǐng)取成功,最后寫入到參與記錄中减响。這是個(gè)非常簡(jiǎn)單的邏輯靖诗,但是郭怪,在高并發(fā)的場(chǎng)景下支示,存在深深的漏洞。多個(gè)并發(fā)請(qǐng)求通過負(fù)載均衡服務(wù)器鄙才,分配到內(nèi)網(wǎng)的多臺(tái)Web服務(wù)器颂鸿,它們首先向存儲(chǔ)發(fā)送查詢請(qǐng)求,然后攒庵,在某個(gè)請(qǐng)求成功寫入?yún)⑴c記錄的時(shí)間差內(nèi)嘴纺,其他的請(qǐng)求獲查詢到的結(jié)果都是“沒有參與記錄”。這里浓冒,就存在邏輯判斷被繞過的風(fēng)險(xiǎn)栽渴。
應(yīng)對(duì)方案:
在程序入口處,一個(gè)賬號(hào)只允許接受1個(gè)請(qǐng)求稳懒,其他請(qǐng)求過濾闲擦。不僅解決了同一個(gè)賬號(hào),發(fā)送N個(gè)請(qǐng)求的問題场梆,還保證了后續(xù)的邏輯流程的安全墅冷。實(shí)現(xiàn)方案,可以通過Redis這種內(nèi)存緩存服務(wù)或油,寫入一個(gè)標(biāo)志位(只允許1個(gè)請(qǐng)求寫成功寞忿,結(jié)合watch的樂觀鎖的特性),成功寫入的則可以繼續(xù)參加顶岸。
或者腔彰,自己實(shí)現(xiàn)一個(gè)服務(wù),將同一個(gè)賬號(hào)的請(qǐng)求放入一個(gè)隊(duì)列中辖佣,處理完一個(gè)霹抛,再處理下一個(gè)。
##6.2 多個(gè)賬號(hào)凌简,一次性發(fā)送多個(gè)請(qǐng)求## 很多公司的賬號(hào)注冊(cè)功能上炎,在發(fā)展早期幾乎是沒有限制的,很容易就可以注冊(cè)很多個(gè)賬號(hào)。因此藕施,也導(dǎo)致了出現(xiàn)了一些特殊的工作室寇损,通過編寫自動(dòng)注冊(cè)腳本,積累了一大批“僵尸賬號(hào)”裳食,數(shù)量龐大矛市,幾萬甚至幾十萬的賬號(hào)不等,專門做各種刷的行為(這就是微博中的“僵尸粉“的來源)诲祸。舉個(gè)例子浊吏,例如微博中有轉(zhuǎn)發(fā)抽獎(jiǎng)的活動(dòng),如果我們使用幾萬個(gè)“僵尸號(hào)”去混進(jìn)去轉(zhuǎn)發(fā)救氯,這樣就可以大大提升我們中獎(jiǎng)的概率找田。
這種賬號(hào),使用在秒殺和搶購里着憨,也是同一個(gè)道理墩衙。例如,iPhone官網(wǎng)的搶購甲抖,火車票黃牛黨漆改。
應(yīng)對(duì)方案:
這種場(chǎng)景,可以通過檢測(cè)指定機(jī)器IP請(qǐng)求頻率就可以解決准谚,如果發(fā)現(xiàn)某個(gè)IP請(qǐng)求頻率很高挫剑,可以給它彈出一個(gè)驗(yàn)證碼或者直接禁止它的請(qǐng)求:
彈出驗(yàn)證碼,最核心的追求柱衔,就是分辨出真實(shí)用戶樊破。因此,大家可能經(jīng)常發(fā)現(xiàn)秀存,網(wǎng)站彈出的驗(yàn)證碼捶码,有些是“鬼神亂舞”的樣子,有時(shí)讓我們根本無法看清或链。他們這樣做的原因惫恼,其實(shí)也是為了讓驗(yàn)證碼的圖片不被輕易識(shí)別,因?yàn)閺?qiáng)大的“自動(dòng)腳本”可以通過圖片識(shí)別里面的字符澳盐,然后讓腳本自動(dòng)填寫驗(yàn)證碼祈纯。實(shí)際上,有一些非常創(chuàng)新的驗(yàn)證碼叼耙,效果會(huì)比較好腕窥,例如給你一個(gè)簡(jiǎn)單問題讓你回答,或者讓你完成某些簡(jiǎn)單操作(例如百度貼吧的驗(yàn)證碼)筛婉。
直接禁止IP簇爆,實(shí)際上是有些粗暴的癞松,因?yàn)橛行┱鎸?shí)用戶的網(wǎng)絡(luò)場(chǎng)景恰好是同一出口IP的,可能會(huì)有“誤傷“入蛆。但是這一個(gè)做法簡(jiǎn)單高效响蓉,根據(jù)實(shí)際場(chǎng)景使用可以獲得很好的效果。
##6.3 多個(gè)賬號(hào)哨毁,不同IP發(fā)送不同請(qǐng)求## 所謂道高一尺枫甲,魔高一丈。有進(jìn)攻扼褪,就會(huì)有防守想幻,永不休止。這些“工作室”话浇,發(fā)現(xiàn)你對(duì)單機(jī)IP請(qǐng)求頻率有控制之后脏毯,他們也針對(duì)這種場(chǎng)景,想出了他們的“新進(jìn)攻方案”凳枝,就是不斷改變IP抄沮。
有同學(xué)會(huì)好奇跋核,這些隨機(jī)IP服務(wù)怎么來的岖瑰。有一些是某些機(jī)構(gòu)自己占據(jù)一批獨(dú)立IP,然后做成一個(gè)隨機(jī)代理IP的服務(wù)砂代,×××給這些“工作室”使用蹋订。還有一些更為黑暗一點(diǎn)的,就是通過***黑掉普通用戶的電腦刻伊,這個(gè)***也不破壞用戶電腦的正常運(yùn)作露戒,只做一件事情,就是轉(zhuǎn)發(fā)IP包捶箱,普通用戶的電腦被變成了IP代理出口智什。通過這種做法,***就拿到了大量的獨(dú)立IP丁屎,然后搭建為隨機(jī)IP服務(wù)荠锭,就是為了掙錢。
應(yīng)對(duì)方案:
說實(shí)話晨川,這種場(chǎng)景下的請(qǐng)求证九,和真實(shí)用戶的行為,已經(jīng)基本相同了共虑,想做分辨很困難愧怜。再做進(jìn)一步的限制很容易“誤傷“真實(shí)用戶,這個(gè)時(shí)候妈拌,通常只能通過設(shè)置業(yè)務(wù)門檻高來限制這種請(qǐng)求了拥坛,或者通過賬號(hào)行為的”數(shù)據(jù)挖掘“來提前清理掉它們。
僵尸賬號(hào)也還是有一些共同特征的,例如賬號(hào)很可能屬于同一個(gè)號(hào)碼段甚至是連號(hào)的猜惋,活躍度不高疾党,等級(jí)低,資料不全等等惨奕。根據(jù)這些特點(diǎn)雪位,適當(dāng)設(shè)置參與門檻,例如限制參與秒殺的賬號(hào)等級(jí)梨撞。通過這些業(yè)務(wù)手段雹洗,也是可以過濾掉一些僵尸號(hào)。
#7 高并發(fā)下的數(shù)據(jù)安全# 我們知道在多線程寫入同一個(gè)文件的時(shí)候卧波,會(huì)存現(xiàn)“線程安全”的問題(多個(gè)線程同時(shí)運(yùn)行同一段代碼时肿,如果每次運(yùn)行結(jié)果和單線程運(yùn)行的結(jié)果是一樣的,結(jié)果和預(yù)期相同港粱,就是線程安全的)螃成。如果是MySQL數(shù)據(jù)庫,可以使用它自帶的鎖機(jī)制很好的解決問題查坪,但是寸宏,在大規(guī)模并發(fā)的場(chǎng)景中,是不推薦使用MySQL的偿曙。秒殺和搶購的場(chǎng)景中氮凝,還有另外一個(gè)問題,就是“超發(fā)”望忆,如果在這方面控制不慎罩阵,會(huì)產(chǎn)生發(fā)送過多的情況。我們也曾經(jīng)聽說過启摄,某些電商搞搶購活動(dòng)稿壁,買家成功拍下后,商家卻不承認(rèn)訂單有效歉备,拒絕發(fā)貨傅是。這里的問題,也許并不一定是商家奸詐威创,而是系統(tǒng)技術(shù)層面存在超發(fā)風(fēng)險(xiǎn)導(dǎo)致的落午。
##7.1 超發(fā)的原因## 假設(shè)某個(gè)搶購場(chǎng)景中,我們一共只有100個(gè)商品肚豺,在最后一刻溃斋,我們已經(jīng)消耗了99個(gè)商品,僅剩最后一個(gè)吸申。這個(gè)時(shí)候梗劫,系統(tǒng)發(fā)來多個(gè)并發(fā)請(qǐng)求享甸,這批請(qǐng)求讀取到的商品余量都是99個(gè),然后都通過了這一個(gè)余量判斷梳侨,最終導(dǎo)致超發(fā)蛉威。
在上面的這個(gè)圖中,就導(dǎo)致了并發(fā)用戶B也“搶購成功”走哺,多讓一個(gè)人獲得了商品蚯嫌。這種場(chǎng)景,在高并發(fā)的情況下非常容易出現(xiàn)丙躏。
##7.2 悲觀鎖思路## 解決線程安全的思路很多择示,可以從“悲觀鎖”的方向開始討論。
悲觀鎖晒旅,也就是在修改數(shù)據(jù)的時(shí)候栅盲,采用鎖定狀態(tài),排斥外部請(qǐng)求的修改废恋。遇到加鎖的狀態(tài)谈秫,就必須等待。
雖然上述的方案的確解決了線程安全的問題鱼鼓,但是拟烫,別忘記,我們的場(chǎng)景是“高并發(fā)”蚓哩。也就是說构灸,會(huì)很多這樣的修改請(qǐng)求,每個(gè)請(qǐng)求都需要等待“鎖”岸梨,某些線程可能永遠(yuǎn)都沒有機(jī)會(huì)搶到這個(gè)“鎖”,這種請(qǐng)求就會(huì)死在那里稠氮。同時(shí)曹阔,這種請(qǐng)求會(huì)很多,瞬間增大系統(tǒng)的平均響應(yīng)時(shí)間隔披,結(jié)果是可用連接數(shù)被耗盡赃份,系統(tǒng)陷入異常。
##7.3 FIFO隊(duì)列思路## 那好奢米,那么我們稍微修改一下上面的場(chǎng)景抓韩,我們直接將請(qǐng)求放入隊(duì)列中的,采用FIFO(First Input First Output鬓长,先進(jìn)先出)谒拴,這樣的話,我們就不會(huì)導(dǎo)致某些請(qǐng)求永遠(yuǎn)獲取不到鎖涉波∮⑸希看到這里炭序,是不是有點(diǎn)強(qiáng)行將多線程變成單線程的感覺哈。
然后苍日,我們現(xiàn)在解決了鎖的問題惭聂,全部請(qǐng)求采用“先進(jìn)先出”的隊(duì)列方式來處理。那么新的問題來了相恃,高并發(fā)的場(chǎng)景下辜纲,因?yàn)檎?qǐng)求很多,很可能一瞬間將隊(duì)列內(nèi)存“撐爆”拦耐,然后系統(tǒng)又陷入到了異常狀態(tài)侨歉。或者設(shè)計(jì)一個(gè)極大的內(nèi)存隊(duì)列揩魂,也是一種方案幽邓,但是,系統(tǒng)處理完一個(gè)隊(duì)列內(nèi)請(qǐng)求的速度根本無法和瘋狂涌入隊(duì)列中的數(shù)目相比火脉。也就是說牵舵,隊(duì)列內(nèi)的請(qǐng)求會(huì)越積累越多,最終Web系統(tǒng)平均響應(yīng)時(shí)候還是會(huì)大幅下降倦挂,系統(tǒng)還是陷入異常畸颅。
##7.4 樂觀鎖思路## 這個(gè)時(shí)候,我們就可以討論一下“樂觀鎖”的思路了方援。樂觀鎖没炒,是相對(duì)于“悲觀鎖”采用更為寬松的加鎖機(jī)制,大都是采用帶版本號(hào)(Version)更新犯戏。實(shí)現(xiàn)就是送火,這個(gè)數(shù)據(jù)所有請(qǐng)求都有資格去修改,但會(huì)獲得一個(gè)該數(shù)據(jù)的版本號(hào)先匪,只有版本號(hào)符合的才能更新成功种吸,其他的返回?fù)屬徥 _@樣的話呀非,我們就不需要考慮隊(duì)列的問題坚俗,不過,它會(huì)增大CPU的計(jì)算開銷岸裙。但是猖败,綜合來說,這是一個(gè)比較好的解決方案降允。
有很多軟件和服務(wù)都“樂觀鎖”功能的支持恩闻,例如Redis中的watch就是其中之一。通過這個(gè)實(shí)現(xiàn)拟糕,我們保證了數(shù)據(jù)的安全判呕。
#8 總結(jié)# 互聯(lián)網(wǎng)正在高速發(fā)展倦踢,使用互聯(lián)網(wǎng)服務(wù)的用戶越多,高并發(fā)的場(chǎng)景也變得越來越多侠草。電商秒殺和搶購辱挥,是兩個(gè)比較典型的互聯(lián)網(wǎng)高并發(fā)場(chǎng)景。雖然我們解決問題的具體技術(shù)方案可能千差萬別边涕,但是遇到的挑戰(zhàn)卻是相似的晤碘,因此解決問題的思路也異曲同工。
?著作權(quán)歸作者所有:來自51CTO博客作者優(yōu)秀android的原創(chuàng)作品功蜓,如需轉(zhuǎn)載园爷,請(qǐng)注明出處,否則將追究法律責(zé)任