雖然在開發(fā)過程中,有些功能可能不需要考慮高并發(fā)情況,但是時刻考慮高并發(fā)場景處理,是程序員開發(fā)過程的一個很好的編程習慣,這種好的習慣也讓開發(fā)出來的制品比較穩(wěn)定靠譜。(本文更多探討代碼層面相關的,比較粗淺,服務架構(gòu)方面的不涉及,>_<)
高并發(fā)相關常用的一些指標有響應時間(Response Time),吞吐量(Throughput),每秒查詢率QPS(Query Per Second),并發(fā)用戶數(shù)等。
- 響應時間:系統(tǒng)對請求做出響應的時間。例如系統(tǒng)處理一個HTTP請求需要200ms,這個200ms就是系統(tǒng)的響應時間。
- 吞吐量:單位時間內(nèi)處理的請求數(shù)量。
- QPS:每秒響應請求數(shù)。在互聯(lián)網(wǎng)領域,這個指標和吞吐量區(qū)分的沒有這么明顯。 并發(fā)用戶數(shù):同時承載正常使用系統(tǒng)功能的用戶數(shù)量。例如一個即時通訊系統(tǒng),同時在線量一定程度上代表了系統(tǒng)的并發(fā)用戶數(shù)。
以下討論基于的編程語言和使用框架分別為:
php:Laravel框架
JAVA:SpringBoot框架
中間件:redis、Kafka
數(shù)據(jù)庫:MySQL、MongoDB
服務器:linux
服務器集群:k8s
一般對于高并發(fā)的處理,按照優(yōu)先級先后順序為:
前端 --》 Nginx --》Web應用 --》 服務器 --》數(shù)據(jù)庫
我們就以一個搶購頁面為例,來說明一下高并發(fā)場景下的一些處理。
一、前端頁面
由于活動頁的大部分信息都是固定不變的,所以考慮到網(wǎng)絡數(shù)據(jù)加載優(yōu)化,一般活動頁會生成一個靜態(tài)頁面。而頁面中的搶購按鈕會根據(jù)活動時間和服務端返回的時間,顯示剩余多少搶購時間。(這里需要使用服務端返回的當前時間來計算剩余多少時間,因為客戶端的時間,第一不一定標準,第二有可能客戶會人為的調(diào)整)
為了防止用戶重復點擊按鈕,也需要控制一下,在用戶點擊提交按鈕之后,按鈕禁用。
二、Nginx
這里主要用到nginx的重寫規(guī)則,訪問某個活動頁,通過nginx重寫規(guī)則,查找是否存在緩存頁面,若存在則直接返回,若不存在則進入頁面程序。這個時候需要應用程序里面添加生成緩存頁面的機制,這樣下次再訪問頁面時就可以走緩存頁面而不用進入程序里面。(Nginx本身也可以配置限流,但是這里不作為主要考慮對象,它作為性能優(yōu)越的代理服務器,發(fā)揮自身的優(yōu)勢就行了)
三、Web應用
1、緩存
使用緩存,主要還是為了減輕數(shù)據(jù)庫方面的壓力,這里有兩個原則。
第一,緩存的信息基本上不會變動,不能將很容易變動的數(shù)據(jù)緩存起來,那么將失去緩存的意義
第二,緩存數(shù)據(jù)一致性,比如將活動信息緩存起來,后端將活動信息調(diào)整時,同樣的也需要修改緩存
第三,緩存的時效性,需要指定緩存的失效時間,比如設置活動結(jié)束之后失效
2、CDN加速
3、消息隊列(異步)
在搶購過程中,生成訂單可能是需要耗費一點時間的,如果讓用戶等待,可能造成很不好的下單體驗。所以此時會考慮將非必要的業(yè)務處理放在異步去處理,比如使用kafka,將處理業(yè)務推送一條消息,然后讓異步程序去執(zhí)行生成訂單,此時就可以返回一個友好提示給用戶,讓用戶在訂單中心查看生成訂單結(jié)果。
4、Redis
redis中有個原子性的方法也可以控制并發(fā)—setnx,對于業(yè)務不是非常復雜的數(shù)據(jù)請求來說,比如只需要防止客戶重復提交,就可以利用setnx來控制。
//標志是否可以開啟事務
boolean do_transaction = true;
//鎖標志,一般以數(shù)據(jù)ID或者用戶ID組合形成唯一標志
String redis_index = 'REDIS_'+data_id;
//設置鎖,后面的值用于后續(xù)判斷鎖是否過期,防止死鎖發(fā)生
Integer result = Redis.setnx(redis_index, time() + 50 );
//如果result為1表示設置redis的key成功,可以進行事務提交
if (result != 1) {
Long previous_transaction_timeout = Redis.get( redis_index );
//如果上次提交事務的超時時間大于當前時間,事務可能還在處理中;反之事務已經(jīng)超時,造成死鎖,需要重新提交事務
if ( previous_transaction_timeout >= time()) {
do_transaction = false;
} else {
Redis.delete( redis_index );
result = Redis.setnx( redis_index , time() + 50 );
if (result != 1) {
do_transaction = false;
}
}
}
if ( !$do_transaction ) {
return '您的請求太頻繁啦~';
}
//進行事務處理
設置成功,返回 1 。 設置失敗,返回 0 。即使多個并發(fā)同時執(zhí)行setnx,也只是存在一個能夠正確設置并返回1。為了防止死鎖發(fā)生,我們可以將redis_val設置成一個過期時間戳,若第一步sentnx沒有成功,那么判斷redis_val是否已經(jīng)過期,若過期,則刪除當前redis_key,重新調(diào)用sentnx。
四、服務器
這里主要用到負載均衡和限流,系統(tǒng)對于訪問的數(shù)據(jù)列承載和處理能力是有限的,所以需要通過限流和負載均衡,將請求分一分,達到最大的優(yōu)化程序。比如某個節(jié)點一次性只能處理500個并發(fā),但是實際場景里面可能會有上萬的并發(fā)請求,如果不進行處理,系統(tǒng)直接就崩潰了。
對于微服務架構(gòu)來說,會將服務轉(zhuǎn)移到不同的微服務上,比如訂單微服務、會員微服務、促銷微服務、商品庫存微服務等等。微服務架構(gòu)需要考慮微服務治理。
五、數(shù)據(jù)庫
請求已經(jīng)落到數(shù)據(jù)庫這里的時候,說明前面各個節(jié)點該優(yōu)化的都已經(jīng)優(yōu)化完了,只能用數(shù)據(jù)庫硬鋼了。數(shù)據(jù)庫的一般優(yōu)化和處理為:
1、水平分割,比如存儲日志表,可以按照日期后綴去區(qū)分保存,例如log_20200101
2、根據(jù)實際用到的查詢條件,查看數(shù)據(jù)庫索引是否建立,然后已經(jīng)建立的索引是否合理
3、數(shù)據(jù)冗余,有些時候必須得連表才能獲取到額外的信息,我們可以考慮適當?shù)脑黾右恍┤哂嘧侄?/p>
補充:數(shù)據(jù)庫數(shù)據(jù)方面,需要考慮是否有些數(shù)據(jù)可以進行清理或者轉(zhuǎn)移備份






