高并發(fā)系統(tǒng)的分析和設(shè)計(jì)
任何系統(tǒng)都不是獨(dú)立于業(yè)務(wù)進(jìn)行開發(fā)的,真正的系統(tǒng)是為了實(shí)現(xiàn)業(yè)務(wù)而開發(fā)的,所以開發(fā)高并發(fā)網(wǎng)站搶購(gòu)時(shí),都應(yīng)該先分析業(yè)務(wù)需求和實(shí)際的場(chǎng)景,在完善這些需求之后才能進(jìn)入系統(tǒng)開發(fā)階段。
沒(méi)有對(duì)業(yè)務(wù)進(jìn)行分析就貿(mào)然開發(fā)系統(tǒng)是開發(fā)者的大忌。對(duì)于業(yè)務(wù)分析,首先是有效請(qǐng)求和無(wú)效請(qǐng)求,有效請(qǐng)求是指真實(shí)的需求,而無(wú)效請(qǐng)求則是虛假的搶購(gòu)請(qǐng)求。
有效請(qǐng)求和無(wú)效請(qǐng)求
無(wú)效請(qǐng)求有很多種類,比如通過(guò)腳本連續(xù)刷新網(wǎng)站首頁(yè),使得網(wǎng)站頻繁訪問(wèn)數(shù)據(jù)庫(kù)和其他資源,造成性能持續(xù)下降,還有一些為了得到搶購(gòu)商品,使用刷票軟件連續(xù)請(qǐng)求的行為。
鑒別有效請(qǐng)求和無(wú)效請(qǐng)求是獲取有效請(qǐng)求的高并發(fā)網(wǎng)站業(yè)務(wù)分析的第一步,我們現(xiàn)在來(lái)分析哪些是無(wú)效請(qǐng)求的場(chǎng)景,以及應(yīng)對(duì)方法。
首先,一個(gè)賬號(hào)連續(xù)請(qǐng)求,對(duì)于一些懂技術(shù)或者使用作弊軟件的用戶,可以使用軟件對(duì)請(qǐng)求的服務(wù)接口連續(xù)請(qǐng)求,使得后臺(tái)壓力變大,甚至在一秒內(nèi)發(fā)送成百上千個(gè)請(qǐng)求到服務(wù)器。
這樣的請(qǐng)求顯然可以認(rèn)為是無(wú)效請(qǐng)求,應(yīng)對(duì)它的方法很多,常見的做法是加入驗(yàn)證碼。一般而言,首次無(wú)驗(yàn)證碼以便用戶減少錄入,第二次請(qǐng)求開始加入驗(yàn)證碼,可以是圖片驗(yàn)證碼、等式運(yùn)算等。
使用圖片驗(yàn)證碼可能存在識(shí)別圖片作弊軟件的攻擊,所以在一些互聯(lián)網(wǎng)網(wǎng)站中,圖片驗(yàn)證碼還會(huì)被加工成為東倒西歪的形式,這樣增加了圖片識(shí)別作弊軟件的辨別難度,以壓制作弊軟件的使用。簡(jiǎn)單的等式運(yùn)算,也會(huì)使圖片識(shí)別作弊軟件更加難以辨認(rèn)。
其次,使用短信服務(wù),把驗(yàn)證碼發(fā)送到短信平臺(tái)以規(guī)避部分作弊軟件。
在企業(yè)應(yīng)用中,這類問(wèn)題的邏輯判斷,不應(yīng)該放在Web服務(wù)器中實(shí)現(xiàn),而應(yīng)放在負(fù)載均衡器上完成,即在進(jìn)入 Web 服務(wù)器之前完成,做完這一步就能避免大量的無(wú)效請(qǐng)求,對(duì)保證高并發(fā)服務(wù)器可用性很有效果。
僅僅做這一步或許還不夠,畢竟驗(yàn)證碼或許還有其他作弊軟件可以快速讀取圖片或者短信信息,從而發(fā)送大量的請(qǐng)求。進(jìn)一步的限制請(qǐng)求,比如限制用戶在單位時(shí)間的購(gòu)買次數(shù)以壓制其請(qǐng)求量,使得這些請(qǐng)求排除在服務(wù)器之外。判斷驗(yàn)證碼邏輯,如圖 1 所示。

圖 1 判斷驗(yàn)證碼邏輯
這里的判斷是在負(fù)載均衡轉(zhuǎn)發(fā)給 Web 服務(wù)器前,對(duì)驗(yàn)證碼和單位時(shí)間單個(gè)賬號(hào)請(qǐng)求數(shù)量進(jìn)行判斷。這里使用了 C 語(yǔ)言和 Redis 進(jìn)行判斷,那么顯然這套方案會(huì)比 Java 語(yǔ)言和數(shù)據(jù)庫(kù)機(jī)制的性能要高得多,通過(guò)這套體系,基本能夠壓制一個(gè)用戶對(duì)系統(tǒng)的作弊,也提高了整個(gè)系統(tǒng)驗(yàn)證的性能。
這是對(duì)一個(gè)賬號(hào)連續(xù)無(wú)效請(qǐng)求的壓制,有時(shí)候有些用戶可能申請(qǐng)多個(gè)賬號(hào)來(lái)迷惑服務(wù)器,使得他可以避開對(duì)單個(gè)賬戶的驗(yàn)證,從而獲得更多的服務(wù)器資源。
一個(gè)人多個(gè)賬戶的場(chǎng)景還是比較好應(yīng)付的,可以通過(guò)提高賬戶的等級(jí)來(lái)壓制多個(gè)請(qǐng)求,比如對(duì)于支付交易的網(wǎng)站,可以通過(guò)銀行卡驗(yàn)證,實(shí)名制獲取相關(guān)證件號(hào)碼,從而使用證件號(hào)碼使得多個(gè)賬戶歸結(jié)為一人,通過(guò)這層關(guān)系來(lái)屏蔽多個(gè)賬號(hào)的頻繁請(qǐng)求,這樣就有效地規(guī)避了一個(gè)人多個(gè)賬號(hào)的頻繁請(qǐng)求。
對(duì)于有組織的請(qǐng)求,則不是那么容易了,因?yàn)閷?duì)于一些黃牛組織,可能通過(guò)多人的賬號(hào)來(lái)發(fā)送請(qǐng)求,統(tǒng)一組織偽造有效請(qǐng)求,如圖 2 所示。

圖 2 統(tǒng)一組織偽造有效請(qǐng)求
對(duì)于這樣的請(qǐng)求,我們會(huì)考慮使用僵尸賬號(hào)排除法對(duì)可交易的賬號(hào)進(jìn)行排除,所謂僵尸賬號(hào),是指那些平時(shí)沒(méi)有任何交易的賬號(hào),只是在特殊的日子交易,比如春運(yùn)期間進(jìn)行大批量搶購(gòu)的賬號(hào)。
當(dāng)請(qǐng)求達(dá)到服務(wù)器,我們通過(guò)僵尸賬號(hào),排除掉一些無(wú)效請(qǐng)求。當(dāng)然還能使用 IP 封禁,尤其是通過(guò)同一 IP 或者網(wǎng)段頻繁請(qǐng)求的,但是這樣也許會(huì)誤傷有效請(qǐng)求,所以使用 IP 封禁還是要慎重一些。
系統(tǒng)設(shè)計(jì)
高并發(fā)系統(tǒng)往往需要分布式的系統(tǒng)分?jǐn)傉?qǐng)求的壓力,這就需要使用負(fù)載均衡服務(wù)了,它進(jìn)行簡(jiǎn)易判斷后就會(huì)分發(fā)到具體 Web 服務(wù)器。
我們要盡量根據(jù) Web 服務(wù)器的性能進(jìn)行均衡分配請(qǐng)求,使得單個(gè) Web 服務(wù)器壓力不至于過(guò)大,導(dǎo)致服務(wù)癱瘓,這可以參考 Nginx 的請(qǐng)求分發(fā),這樣使得請(qǐng)求能夠均衡發(fā)布到服務(wù)器中去,服務(wù)器可以按業(yè)務(wù)劃分。
比如當(dāng)前的購(gòu)買分為產(chǎn)品維護(hù)、交易維護(hù)、資金維護(hù)、報(bào)表統(tǒng)計(jì)和用戶維護(hù)等模塊,按照功能模塊進(jìn)行區(qū)分,使得它們相互隔離,就可以降低數(shù)據(jù)的復(fù)雜性,圖 3 就是一種典型的按業(yè)務(wù)劃分,或者稱為水平分法。

圖 3 按業(yè)務(wù)劃分
按照業(yè)務(wù)劃分的好處是:首先,一個(gè)服務(wù)管理一種業(yè)務(wù),業(yè)務(wù)簡(jiǎn)單了,提高了開發(fā)效率,其次,數(shù)據(jù)庫(kù)的設(shè)計(jì)也方便許多,畢竟各管各的東西。
但是,這也會(huì)帶來(lái)很多麻煩,比如由于各個(gè)系統(tǒng)業(yè)務(wù)存在著關(guān)聯(lián),還要通過(guò) RPC(Remote Procedure Call Protoco,遠(yuǎn)程過(guò)程調(diào)用協(xié)議)處理這些關(guān)聯(lián)信息。
比較流行的 RPC 有 Dubbo、Thrift 和 Hessian 等。其原理是,每一個(gè)服務(wù)都會(huì)暴露一些公共的接口給 RPC 服務(wù),這樣對(duì)于任何一個(gè)服務(wù)器都能夠通過(guò) RPC 服務(wù)獲取其他服務(wù)器對(duì)應(yīng)的接口去調(diào)度各個(gè)服務(wù)器的邏輯來(lái)完成功能,但是接口的相互調(diào)用也會(huì)造成一定的緩慢。
有了水平分法也會(huì)有垂直分法,所謂垂直分法就是將一個(gè)很大的請(qǐng)求量,不按子系統(tǒng)分,而是將它們按照互不相干的幾個(gè)同樣的系統(tǒng)分?jǐn)傁氯ァ?/p>
比如一臺(tái)服務(wù)器的最大負(fù)荷為每秒 1 萬(wàn)個(gè)請(qǐng)求,而測(cè)得系統(tǒng)高峰為每秒 2 萬(wàn)個(gè)請(qǐng)求,如果我們把各個(gè)請(qǐng)求按照一定的算法合理分配到 4 臺(tái)服務(wù)器上,那么 4 臺(tái)服務(wù)器平均 5 千個(gè)請(qǐng)求就屬于正常服務(wù)了,這樣的路由算法被稱為垂直分法,如圖 4 所示。

圖 4 垂直分法
垂直分法不按業(yè)務(wù)分,對(duì)于負(fù)載均衡器的算法往往可以通過(guò)用戶編號(hào)把路由分發(fā)到對(duì)應(yīng)的服務(wù)器上。
每一個(gè)服務(wù)器處理自己獨(dú)立的業(yè)務(wù),互不干擾,但是每一個(gè)服務(wù)器都包含所有的業(yè)務(wù)邏輯功能,會(huì)造成開發(fā)上的業(yè)務(wù)困難,對(duì)于數(shù)據(jù)庫(kù)設(shè)計(jì)而言也是如此。
對(duì)于大型網(wǎng)站還會(huì)有更細(xì)的分法,比如水平和垂直結(jié)合的分法,如圖 5 所示。

圖 5 水平和垂直結(jié)合分法
首先將系統(tǒng)按照業(yè)務(wù)區(qū)分為多個(gè)子系統(tǒng),然后在每一個(gè)子系統(tǒng)下再分多個(gè)服務(wù)器,通過(guò)每一個(gè)子系統(tǒng)的路由器找到對(duì)應(yīng)的子系統(tǒng)服務(wù)器提供服務(wù)。
分法是多樣性的,每一個(gè)企業(yè)都會(huì)根據(jù)自己的需要而進(jìn)行不同的設(shè)計(jì),但是無(wú)論系統(tǒng)如何分,秉承的原則是不變的。
首先,服務(wù)器的負(fù)載均衡,要使得每一個(gè)服務(wù)器都能比較平均地得到請(qǐng)求數(shù)量,從而提高系統(tǒng)的吞吐和性能。其次,業(yè)務(wù)簡(jiǎn)化,按照模塊劃分可以使得系統(tǒng)劃分為各個(gè)子系統(tǒng),這樣開發(fā)者的業(yè)務(wù)單一化,就更容易理解和開發(fā)了。
數(shù)據(jù)庫(kù)設(shè)計(jì)
對(duì)于數(shù)據(jù)庫(kù)的設(shè)計(jì)而言,為了得到高性能,可以使用分表或分庫(kù)技術(shù),從而提高系統(tǒng)的響應(yīng)能力。
分表是指在一個(gè)數(shù)據(jù)庫(kù)內(nèi)本來(lái)一張表可以保存的數(shù)據(jù),設(shè)計(jì)成多張表去保存,比如交易表 t_transaction。
由于存儲(chǔ)數(shù)據(jù)多會(huì)造成查詢和統(tǒng)計(jì)的緩慢,這個(gè)時(shí)候可以使用多個(gè)表存儲(chǔ),比如 2016 年的數(shù)據(jù)用表 t_transaction_2016 存儲(chǔ),2017 年的數(shù)據(jù)使用表 t_transaction_2017 存儲(chǔ),2018 年的數(shù)據(jù)則用表 t_transaction_2018 存儲(chǔ),依此類推,開發(fā)者只要根據(jù)查詢的年份確定需要查找哪張表就可以了,如圖 6 所示。

圖 6 通過(guò)年份路由分表
分庫(kù)則不一樣,它把表數(shù)據(jù)分配在不同的數(shù)據(jù)庫(kù)中,比如上述的交易表 t_transaction 可以存放在多個(gè)數(shù)據(jù)庫(kù)中,如圖 7 所示。

圖 7 分庫(kù)設(shè)計(jì)
分庫(kù)數(shù)據(jù)庫(kù)首先需要一個(gè)路由算法確定數(shù)據(jù)在哪個(gè)數(shù)據(jù)庫(kù)上,然后才能進(jìn)行查詢,比如我們可以把用戶和對(duì)應(yīng)業(yè)務(wù)的數(shù)據(jù)庫(kù)的信息緩存到 Redis 中,這樣路由算法就可以通過(guò) Redis 讀取的數(shù)據(jù)來(lái)決定使用哪個(gè)數(shù)據(jù)庫(kù)進(jìn)行查詢了。
一些會(huì)員很多的網(wǎng)站還可以區(qū)分活躍會(huì)員和非活躍會(huì)員?;钴S會(huì)員可以通過(guò)數(shù)據(jù)遷徙的手段,也就是先記錄在某個(gè)時(shí)間段(比如一個(gè)月的月底)會(huì)員的活躍度,然后通過(guò)數(shù)據(jù)遷徙,將活躍會(huì)員較平均分?jǐn)偟礁鱾€(gè)數(shù)據(jù)庫(kù)中,以避免某個(gè)庫(kù)過(guò)多的集中活躍會(huì)員,而導(dǎo)致個(gè)別數(shù)據(jù)庫(kù)被訪問(wèn)過(guò)多,從而達(dá)到數(shù)據(jù)庫(kù)的負(fù)載均衡。
做完這些還可以考慮優(yōu)化 SQL,建立索引等優(yōu)化,提高數(shù)據(jù)庫(kù)的性能。性能低下的 SQL 對(duì)于高并發(fā)網(wǎng)站的影響是很大的,這些對(duì)開發(fā)者提出了更高的要求。
在開發(fā)網(wǎng)站中使用更新語(yǔ)句和復(fù)雜查詢語(yǔ)句要時(shí)刻記住更新是表鎖定還是行鎖定,比如 id 是主鍵,而 user_name 是用戶名稱,也是唯一索引,更新用戶的生日,可以使用以下兩條SQL中的任何一條:
update t_user set birthday = #{birthday} where id = #{id}; update t_user set birthday = #{birthday} where user_name = #{userName};
上述邏輯都是正確的,但是優(yōu)選使用主鍵更新,其原因是在 MySQL 的運(yùn)行過(guò)程中,第二句 SQL 會(huì)鎖表,即不僅鎖定更新的數(shù)據(jù),而且鎖定其他表的數(shù)據(jù),從而影響并發(fā),而使用主鍵的更新則是行鎖定。
對(duì)于 SQL 的優(yōu)化還有很多細(xì)節(jié),比如可以使用連接查詢代替子查詢。查詢一個(gè)沒(méi)有分配角色的用戶 id,可能有人使用這樣的一個(gè) SQL:
<pre spellcheck="false" class="md-fences md-end-block md-fences-with-lineno ty-contain-cm modeLoaded" lang="sql" cid="n63" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px 0px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"> select u.id from t_user u
where u.id not in (select ur.user_id from t_user_role ur);</pre>
這是一個(gè) not in 語(yǔ)句,性能低下,對(duì)于這樣的 not in 和 not exists 語(yǔ)句,應(yīng)該全部修改為連接語(yǔ)句去執(zhí)行,從而極大地提高 SQL 的性能,比如這條 not in 語(yǔ)句可以修改為:
<pre spellcheck="false" class="md-fences md-end-block md-fences-with-lineno ty-contain-cm modeLoaded" lang="sql" cid="n65" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px 0px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"> select u.id from t_user u left join t_user_role ur
on u.id = ur.user_id
where ur.user_id is null;</pre>
not in 語(yǔ)句消失了,使用了連接查詢,大大提高了 SQL 的執(zhí)行性能。
此外還可以通過(guò)讀/寫分離等技術(shù),進(jìn)行進(jìn)一步的優(yōu)化,這樣就可以有一臺(tái)主機(jī)主要負(fù)責(zé)寫業(yè)務(wù),一臺(tái)或者多臺(tái)備機(jī)負(fù)責(zé)讀業(yè)務(wù),有助于性能的提高。
對(duì)于分布式數(shù)據(jù)庫(kù)而言,還會(huì)有另外一個(gè)麻煩,就是事務(wù)的一致性,事務(wù)的一致性比較復(fù)雜,目前流行的有兩段提交協(xié)議,即 XA 協(xié)議、Paxos 協(xié)議。
動(dòng)靜分離技術(shù)
動(dòng)靜分離技術(shù)是目前互聯(lián)網(wǎng)的主流技術(shù),對(duì)于互聯(lián)網(wǎng)而言大部分?jǐn)?shù)據(jù)都是靜態(tài)數(shù)據(jù),只有少數(shù)使用動(dòng)態(tài)數(shù)據(jù),動(dòng)態(tài)數(shù)據(jù)的數(shù)據(jù)包很小,不會(huì)造成網(wǎng)絡(luò)瓶頸。
而靜態(tài)的數(shù)據(jù)則不一樣,靜態(tài)數(shù)據(jù)包含圖片、CSS(樣式)、JavaScript(腳本)和視頻等互聯(lián)網(wǎng)的應(yīng)用,尤其是圖片和視頻占據(jù)的流量很大,如果都從動(dòng)態(tài)服務(wù)器(比如 Tomcat、WildFly 和 WebLogic 等)獲取,那么動(dòng)態(tài)服務(wù)器的帶寬壓力會(huì)很大,這個(gè)時(shí)候應(yīng)該考慮使用動(dòng)靜分離技術(shù)。
對(duì)于一些有條件的企業(yè)也可以考慮使用 CDN(Content Delivery Network,即內(nèi)容分發(fā)網(wǎng)絡(luò))技術(shù),它允許企業(yè)將自己的靜態(tài)數(shù)據(jù)緩存到網(wǎng)絡(luò) CDN 的節(jié)點(diǎn)中。比如企業(yè)將數(shù)據(jù)緩存在北京的節(jié)點(diǎn)上,當(dāng)在天津的客戶發(fā)送請(qǐng)求時(shí),通過(guò)一定的算法,會(huì)找到北京 CDN 節(jié)點(diǎn),從而把 CDN 緩存的數(shù)據(jù)發(fā)送給天津的客戶,完成請(qǐng)求。
對(duì)于深圳的客戶,如果企業(yè)將數(shù)據(jù)緩存到廣州 CDN 節(jié)點(diǎn)上,那么它也可以從廣州的 CDN 節(jié)點(diǎn)上取出數(shù)據(jù),由于就近取出緩存節(jié)點(diǎn)的數(shù)據(jù),所以速度會(huì)很快,如圖 8 所示。

圖 8 圖解CDN
一些企業(yè)也許需要自己的靜態(tài) HTTP 服務(wù)器,將靜態(tài)數(shù)據(jù)分離到靜態(tài) HTTP 服務(wù)器上。其原理大同小異,就是將資源分配到靜態(tài)服務(wù)器上,這樣圖片、HTML、腳本等資源都可以從靜態(tài)服務(wù)器上獲取,盡量使用 Cookie 等技術(shù),讓客戶端緩存能夠緩存數(shù)據(jù),避免多次請(qǐng)求,降低服務(wù)器的壓力。
對(duì)于動(dòng)態(tài)數(shù)據(jù),則需要根據(jù)會(huì)員登錄來(lái)獲取后臺(tái)數(shù)據(jù),這樣的動(dòng)態(tài)數(shù)據(jù)是高并發(fā)網(wǎng)站關(guān)注的重點(diǎn)。
鎖和高并發(fā)
無(wú)論區(qū)分有效請(qǐng)求和無(wú)效請(qǐng)求,水平劃分和垂直劃分,動(dòng)靜分離技術(shù),還是數(shù)據(jù)庫(kù)分表、分庫(kù)等技術(shù)的應(yīng)用,都無(wú)法避免動(dòng)態(tài)數(shù)據(jù),而動(dòng)態(tài)數(shù)據(jù)的請(qǐng)求最終也會(huì)落在一臺(tái) Web 服務(wù)器上。
對(duì)于一臺(tái) Web 服務(wù)器而言,如果是 Java 服務(wù)器,它極有可能采用 SSM 框架結(jié)合數(shù)據(jù)庫(kù)和 Redis 等技術(shù)提供服務(wù),那么它會(huì)面臨何種困難呢?高并發(fā)系統(tǒng)存在的一個(gè)麻煩是并發(fā)數(shù)據(jù)不一致問(wèn)題。
以搶紅包為例,發(fā)放了一個(gè)總額為 20 萬(wàn)元的紅包,它可以拆分為 2 萬(wàn)個(gè)可搶的小紅包。假設(shè)每個(gè)小紅包都是 10 元,供給網(wǎng)站會(huì)員搶奪,網(wǎng)站同時(shí)存在 3 萬(wàn)會(huì)員在線搶奪,這就是一個(gè)典型的高并發(fā)的場(chǎng)景。
以上會(huì)出現(xiàn)多個(gè)線程同時(shí)享有大紅包數(shù)據(jù)的場(chǎng)景,在高并發(fā)的場(chǎng)景中,由于線程每一步完成的順序不一樣,這樣會(huì)導(dǎo)致數(shù)據(jù)的一致性問(wèn)題,比如在最后的一個(gè)紅包,就可能出現(xiàn)如表 1 所示的場(chǎng)景。
注意表 1 中加粗的文字,由此可見,在高并發(fā)的場(chǎng)景下可能出現(xiàn)錯(cuò)扣紅包的情況,這樣就會(huì)導(dǎo)致數(shù)據(jù)錯(cuò)誤。由于在一個(gè)瞬間產(chǎn)生很高的并發(fā),因此除了保證數(shù)據(jù)一致性,我們還要盡可能地保證系統(tǒng)的性能,加鎖會(huì)影響并發(fā),而不加鎖就難以保證數(shù)據(jù)的一致性,這就是高并發(fā)和鎖的矛盾。
| 時(shí)刻 | 線程一 | 線程二 | 備注 |
|---|---|---|---|
| T0 | —— | —— | 存在最后一個(gè)紅包可搶 |
| T1 | 讀取大紅包信息,存在最后一個(gè)紅包,可搶 | —— | —— |
| T2 | —— | 讀取大紅包信息,存在最后一個(gè)紅包,可搶 | —— |
| T3 | 扣減最后一個(gè)紅包 | —— | 此時(shí)已經(jīng)不存在紅包可搶 |
| T4 | —— | 扣減紅包 | 錯(cuò)誤發(fā)生了,超扣了 |
| T5 | 記錄用戶獲取紅包信息 | —— | —— |
| T6 | —— | 記錄用戶獲取紅包信息 | 因?yàn)殄e(cuò)誤扣減紅包而引發(fā)的錯(cuò)誤 |
為了解決這對(duì)矛盾,在當(dāng)前互聯(lián)網(wǎng)系統(tǒng)中,大部分企業(yè)提出了悲觀鎖和樂(lè)觀鎖的概念,而對(duì)于數(shù)據(jù)庫(kù)而言,如果在那么短的時(shí)間內(nèi)需要執(zhí)行大量 SQL,對(duì)于服務(wù)器的壓力可想而知,需要優(yōu)化數(shù)據(jù)庫(kù)的表設(shè)計(jì)、索引、SQL 語(yǔ)句等。
有些企業(yè)提出了使用 Redis 事務(wù)和 Lua 語(yǔ)言所提供的原子性來(lái)取代現(xiàn)有的數(shù)據(jù)庫(kù)的技術(shù),從而提高數(shù)據(jù)的存儲(chǔ)響應(yīng),以應(yīng)對(duì)高并發(fā)場(chǎng)景,嚴(yán)格來(lái)說(shuō)它也屬于樂(lè)觀鎖的概念。教程后面會(huì)討論關(guān)于數(shù)據(jù)不一致的方案,悲觀鎖、樂(lè)觀鎖和 Redis 實(shí)現(xiàn)的場(chǎng)景。
Redis悲觀鎖、樂(lè)觀鎖和調(diào)用Lua腳本三種方式的優(yōu)缺點(diǎn)
教程前面主要討論了 Java 互聯(lián)網(wǎng)的高并發(fā)應(yīng)用,先談及了一些常用的系統(tǒng)設(shè)計(jì)理念,用以搭建高可用的互聯(lián)網(wǎng)應(yīng)用系統(tǒng),還討論了數(shù)據(jù)不一致的超發(fā)問(wèn)題。
并且還論述了樂(lè)觀鎖、悲觀鎖和 Redis 如何消除數(shù)據(jù)不一致性的問(wèn)題,也對(duì)它們的性能進(jìn)行了探討。
悲觀鎖使用了數(shù)據(jù)庫(kù)的鎖機(jī)制,可以消除數(shù)據(jù)不一致性,對(duì)于開發(fā)者而言會(huì)十分簡(jiǎn)單,但是,使用悲觀鎖后,數(shù)據(jù)庫(kù)的性能有所下降,因?yàn)榇罅康木€程都會(huì)被阻塞,而且需要有大量的恢復(fù)過(guò)程,需要進(jìn)一步改變算法以提高系統(tǒng)的并發(fā)能力。
通過(guò) CAS 原理和 ABA 問(wèn)題的討論,我們更加明確了樂(lè)觀鎖的原理,使用樂(lè)觀鎖有助于提高并發(fā)性能,但是由于版本號(hào)沖突,樂(lè)觀鎖導(dǎo)致多次請(qǐng)求服務(wù)失敗的概率大大提高,而我們通過(guò)重入(按時(shí)間戳或者按次數(shù)限定)來(lái)提高成功的概率,這樣對(duì)于樂(lè)觀鎖而言實(shí)現(xiàn)的方式就相對(duì)復(fù)雜了,其性能也會(huì)隨著版本號(hào)沖突的概率提升而提升,并不穩(wěn)定。
使用樂(lè)觀鎖的弊端在于,導(dǎo)致大量的 SQL 被執(zhí)行,對(duì)于數(shù)據(jù)庫(kù)的性能要求較高,容易引起數(shù)據(jù)庫(kù)性能的瓶頸,而且對(duì)于開發(fā)還要考慮重入機(jī)制,從而導(dǎo)致開發(fā)難度加大。
使用 Redis 去實(shí)現(xiàn)高并發(fā),通過(guò) Redis 提供的 Lua 腳本的原子性,消除了數(shù)據(jù)不一致性,并且在整個(gè)過(guò)程中只有最后一次涉及數(shù)據(jù)庫(kù),而且是使用了新的線程。
在實(shí)際的操作中筆者更加傾向于使用 JMS 啟動(dòng)另外的服務(wù)器進(jìn)行操作。但是這樣使用的風(fēng)險(xiǎn)在于 Redis 的不穩(wěn)定性,因?yàn)槠涫聞?wù)和存儲(chǔ)都存在不穩(wěn)定的因素,所以更多的時(shí)候,筆者都建議使用獨(dú)立 Redis 服務(wù)器做高并發(fā)業(yè)務(wù),一方面可以提高 Redis 的性能,另一方面即使在高并發(fā)的場(chǎng)合,Redis 服務(wù)器宕機(jī)也不會(huì)影響現(xiàn)有的其他業(yè)務(wù),同時(shí)也可以使用備機(jī)等設(shè)備提高系統(tǒng)的高可用,保證網(wǎng)站的安全穩(wěn)定。
以上討論了 3 種方式實(shí)現(xiàn)高并發(fā)業(yè)務(wù)技術(shù)的利弊,妥善規(guī)避風(fēng)險(xiǎn),同時(shí)保證系統(tǒng)的高可用和高效是值得每一位開發(fā)者思考的問(wèn)題。