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