電商那些年,我摸爬打滾出的高併發架構實戰精髓
一、關於高併發
高併發是指在同一個時間點,有很多使用者同時訪問URL地址,比如:淘寶的雙11、雙12,就會產生高併發。又如貼吧的爆吧,就是惡意的高併發請求,也就是DDOS攻擊,再屌絲點的說法就像玩LOL被ADC暴擊了一樣,那傷害你懂的。
來源:SFLYQ的部落格
原文:http://blog.thankbabe.com/2016/09/14/high-concurrency-scheme/
1、高併發會來帶的後果
- 服務端:導致站點伺服器/DB伺服器資源被佔滿崩潰,資料的儲存和更新結果和理想的設計是不一樣的,比如:出現重複的資料記錄,多次添加了使用者積分等。
- 使用者角度:尼瑪,這麼卡,老子來參加活動的,重新整理了還是這樣,垃圾網站,再也不來了!
- 我的經歷:在做公司產品網站的過程中,經常會有這樣的需求,比如搞個活動專題、抽獎、簽到、積分競拍等等,如果沒有考慮到高併發下的資料處理,那就Game Over了,很容易導致抽獎被多抽走,簽到發現一個使用者有多條記錄等等,各種超出正常邏輯的現象,這就是做產品網站必須考慮的問題,因為這些都是面向大量使用者的,而不是像做ERP管理系統、OA系統那樣,只是面向員工。
下面我進行例項分析,簡單粗暴,動態分析,純屬本人經驗分享,如有說錯或者更好的建議,請留言,大家一起成長。
2、併發下的資料處理
通過表設計,如:記錄表新增唯一約束,資料處理邏輯使用事物防止併發下的資料錯亂問題。通過服務端鎖程序防止包併發下的資料錯亂問題。這裡主要講述的是在併發請求下的資料邏輯處理的介面,如何保證資料的一致性和完整性,這裡的併發可能是大量使用者發起的,也可能攻擊者通過併發工具發起的併發請求。
- 需求點:
【簽到功能】一天一個使用者只能簽到一次,簽到成功後用戶獲取到一個積分。
- 已知表:
1、使用者表,包含積分欄位;
2、高併發意淫分析(屬於開發前的猜測): 在高併發的情況下,會導致一個使用者簽到記錄會有多條,或者使用者簽到後不止加一積分。
- 我的設計:首先根據需求我會新增一張簽到記錄表,重點來了,這張表需要把使用者唯一標識欄位(ID,Token)和簽到日期欄位新增為唯一約束,或者唯一索引,這樣就可以防止併發的時候插入重複使用者的簽到記錄。然後再程式程式碼邏輯裡,先執行簽到資料的新增(這裡可以防止併發,新增成功後再進行積分的新增,這樣就可以防止重複地新增積分了。最後我還是建議所有的資料操作都寫在一個sql事務裡面, 這樣在新增失敗,或者編輯使用者積分失敗的時候可以回滾資料。
- 需求點:【抽獎功能】抽獎一次消耗一個積分,抽獎中獎後編輯剩餘獎品總數,剩餘獎品總數為0,或者使用者積分為0的時候無法進行抽獎。
- 已知表:使用者表,包含積分欄位 獎品表,包含獎品剩餘數量欄位。
- 高併發意淫分析(屬於開發前的猜測):在高併發的情況下,會導致使用者參與抽獎的時候積分被扣除,而獎品實際上已經被抽完了。
- 我的設計:在事物裡,通過WITH(UPDLOCK)鎖住商品表,或者Update 表的獎品剩餘數量和最後編輯時間欄位,來把資料行鎖住,然後進行使用者積分的消耗,都完成後提交事物,失敗就回滾。 這樣就可以保證,只有可能存在一個操作在操作這件商品的數量,只有等到這個操作事物提交後,其他的操作這個商品行的事物才會繼續執行。
- 需求點:【快取資料到cache裡】,當快取不存在的時候,從資料庫中獲取並儲存在cache裡,如果存在從cache裡獲取,每天10點必須更新一次,其他時間點快取兩個小時更新一次 到10點的時候,凡是開啟頁面的使用者會自動重新整理頁面。
- 問題點:這裡有個邏輯使用者觸發快取的更新,使用者重新整理頁面,當快取存在的時候,會取到最後一次快取更新時間,如果當前時間大於十點,並且最後快取時間是10點前,則會從資料庫中重新獲取資料儲存到cache中。 還有客戶端頁面會在10點時候用js發起頁面的重新整理,就是因為有這樣的邏輯,導致10點的時候有很多併發請求同時過來,然後就會導致很多的sql查詢操作,理想的邏輯是,只有一個請求會去資料庫獲取,其他都是從快取中獲取資料。(因為這個sql查詢很耗伺服器效能,所以導致在10點的時候,突然間資料庫伺服器壓力暴增)
- 解決問題:C#通過(鎖)lock,在從資料讀取到快取的那段程式碼前面加上鎖,這樣在併發的情況下只會有一個請求是從資料庫裡獲取資料,其他都是從快取中獲取。
3、訪問量大的資料統計介面
- 需求: 使用者行為資料統計介面,用來記錄商品展示次數,使用者通過點選圖片,或者連結,或者其他方式進入到商品詳情的行為次數。
- 問題點:這介面是給前端ajax使用,訪問量會很大,一頁面展示的時候就會有幾十件商品的展示,滾動條滾到到頁面顯示商品的時候就會請求介面進行展示資料的統計,每次翻頁又會載入幾十件。
- 意淫分析:設想如果同時有1W個使用者同時線上訪問頁面,一個次拉動滾動條螢幕頁面展示10件商品,這樣就會有10W個請求過來,服務端需要把請求資料入庫。在實際線上環境可能還會超過這個請求量,如果不經過進行高併發設計處理,伺服器分分鐘給跪了。
- 解決問題:我們通過nodejs寫了一個數據處理介面,把統計資料先存到redis的list裡。(使用nodejs寫介面的好處是,nodejs使用單執行緒非同步事件機制,高併發處理能力強,不會因為資料邏輯處理問題導致伺服器資源被佔用而導致伺服器宕機) 然後再使用nodejs寫了一個指令碼,指令碼功能就是從redis裡出列資料儲存到mysql資料庫中。這個指令碼會一直執行,當redis沒有資料需要同步到資料庫中的時候,sleep,讓在進行資料同步操作。
4、高併發的下的伺服器壓力均衡,合理站點架設,DB部署
以下我所知道的:
- 伺服器代理nginx,做伺服器的均衡負載,把壓力均衡到多臺伺服器;
- 部署叢集MySQL資料庫, Redis伺服器,或者MongoDB伺服器,把一些常用的查詢資料,並且不會經常的變化的資料儲存到其他NoSQL DB伺服器中,來減少資料庫伺服器的壓力,加快資料的響應速度;
- 資料快取,Cache;
- 在高併發介面的設計中可以使用具有高併發能力的程式語言去開發,如:nodejs做web介面;
- 伺服器部署,圖片伺服器分離,靜態檔案走CDN;
- DBA資料庫的優化查詢條件,索引優化;
- 訊息儲存機制,將資料新增到資訊佇列中(redis list),然後再寫工具去入庫
- 指令碼合理控制請求,如,防止使用者重複點選導致的ajax多餘的請求,等等。
5、併發測試神器推薦
- Apache JMeter
- Microsoft Web Application Stress Tool
- Visual Studio 效能負載
二、關於高併發架構
為了讓業務可以流暢地執行並且給使用者一個好的互動體驗,我們需要根據業務場景預估達到的併發量等因素,來設計適合自己業務場景的高併發處理方案。
在電商相關產品開發的這些年,我有幸遇到了併發下的各種坑,這一路摸爬滾打過來有著不少的血淚史,這裡進行總結,作為自己的歸檔記錄,同時分享給大家。
1、伺服器架構
業務從發展的初期到逐漸成熟,伺服器架構也是從相對單一到叢集,再到分散式服務。
一個可以支援高併發的服務少不了好的伺服器架構,需要有均衡負載,資料庫需要主從叢集,NoSQL快取需要主從叢集,靜態檔案需要上傳CDN,這些都是能讓業務程式流暢執行的強大後盾。
伺服器這塊多是需要運維人員來配合搭建,具體我就不多說了,點到為止。
大致需要用到的伺服器架構如下:
伺服器:
- 均衡負載(如:nginx,阿里雲SLB)
- 資源監控
- 分散式
資料庫:
- 主從分離,叢集
- DBA 表優化,索引優化,等
- 分散式
NoSQL:
- Redis
主從分離,叢集
- MongoDB
主從分離,叢集
- memcache
主從分離,叢集
CDN:
- html
- css
- js
- image
2、併發測試
高併發相關的業務,需要進行併發的測試,通過大量的資料分析評估出整個架構可以支撐的併發量。
測試高併發可以使用第三方伺服器或者自己測試伺服器,利用測試工具進行併發請求測試,分析測試資料得到可以支撐併發數量的評估,這個可以作為一個預警參考,俗話說知己自彼百戰不殆。
第三方服務:
- 阿里雲效能測試
併發測試工具:
- Apache JMeter
- Visual Studio效能負載測試
- Microsoft Web Application Stress Tool
1)通用方案
場景: 使用者簽到,使用者中心,使用者訂單等。
伺服器架構圖:
說明:
場景中的這些業務基本是使用者進入APP後會操作到的,除了活動日(618、雙11等),這些業務的使用者量都不會高聚集,同時這些業務相關的表都是大資料表,業務多是查詢操作,所以我們需要減少使用者直接命中DB的查詢;優先查詢快取,如果快取不存在,再進行DB查詢,將查詢結果快取起來。
更新使用者相關快取需要分散式儲存,比如使用使用者ID進行hash分組,把使用者分佈到不同的快取中,這樣一個快取集合的總量不會很大,不會影響查詢效率。
方案如:
使用者簽到獲取積分:
- 計算出使用者分佈的key,Redis,hash中查詢使用者今日簽到資訊
- 如果查詢到簽到資訊,返回簽到資訊
- 如果沒有查詢到,DB查詢今日是否簽到過,如果有簽到過,就把簽到資訊同步Redis快取。
- 如果DB中也沒有查詢到今日的簽到記錄,就進行簽到邏輯,操作DB新增今日簽到記錄,添加簽到積分(這整個DB操作是一個事務)
- 快取簽到資訊到Redis,返回簽到資訊
- 注意這裡會有併發情況下的邏輯問題,如:一天簽到多次,發放多次積分給使用者。
使用者訂單:
- 這裡我們只快取使用者第一頁的訂單資訊,一頁40條資料,使用者一般也只會看第一頁的訂單資料
- 使用者訪問訂單列表,如果是第一頁讀快取,如果不是讀DB
- 計算出使用者分佈的key,Redis,hash中查詢使用者訂單資訊
- 如果查詢到使用者訂單資訊,返回訂單資訊
- 如果不存在就進行DB查詢第一頁的訂單資料,然後快取redis,返回訂單資訊
使用者中心:
- 計算出使用者分佈的key,Redis hash中查詢使用者訂單資訊
- 如果查詢到使用者資訊,返回使用者資訊
- 如果不存在進行使用者DB查詢,然後快取redis,返回使用者資訊
其他業務:
- 上面例子多是針對使用者儲存快取,如果是公用的快取資料需要注意一些問題,如:公用的快取資料需要考慮併發下的可能會導致大量命中DB查詢,可以使用管理後臺更新快取,或者DB查詢的鎖住操作。
以上例子是一個相對簡單的高併發架構,併發量不是很高的情況可以很好的支撐,但是隨著業務的壯大,使用者併發量增加,我們的架構也會進行不斷的優化和演變,比如對業務進行服務化,每個服務有自己的併發架構,自己的均衡伺服器,分散式資料庫,NoSQL主從叢集,如:使用者服務、訂單服務。
2)訊息佇列
場景:定時領取紅包等。
伺服器架構圖:
說明:
- 場景中的定時領取是一個高併發的業務,像秒殺活動使用者會在到點的時間湧入,DB瞬間就接受到一記暴擊,hold不住就會宕機,然後影響整個業務;
- 像這種不是隻有查詢的操作並且會有高併發的插入或者更新資料的業務,前面提到的通用方案就無法支撐,併發的時候都是直接命中DB;
- 設計這塊業務的時候就會使用訊息佇列的,可以將參與使用者的資訊新增到訊息佇列中,然後再寫個多執行緒程式去消耗佇列,給佇列中的使用者發放紅包;
方案如:
- 定時領取紅包;
- 一般習慣使用 redis的 list;
- 當用戶參與活動,將使用者參與資訊push到佇列中;
- 然後寫個多執行緒程式去pop資料,進行發放紅包的業務;
- 這樣可以支援高併發下的使用者可以正常的參與活動,並且避免資料庫伺服器宕機的危險。
附加: 通過訊息佇列可以做很多的服務。
如:定時簡訊傳送服務,使用sset(sorted set),傳送時間戳作為排序依據,簡訊資料佇列根據時間升序,然後寫個程式定時迴圈去讀取sset佇列中的第一條,當前時間是否超過傳送時間,如果超過就進行簡訊傳送。
3)一級快取
高併發請求連線快取伺服器超出伺服器能夠接收的請求連線量,部分使用者出現建立連線超時無法讀取到資料的問題;
因此需要有個方案當高併發時候時候可以減少命中快取伺服器;
這時候就出現了一級快取的方案,一級快取就是使用站點伺服器快取去儲存資料,注意只儲存部分請求量大的資料,並且快取的資料量要控制,不能過分的使用站點伺服器的記憶體而影響了站點應用程式的正常執行,一級快取需要設定秒單位的過期時間,具體時間根據業務場景設定,目的是當有高併發請求的時候可以讓資料的獲取命中到一級快取,而不用連線快取NoSQL資料伺服器,減少NoSQL資料伺服器的壓力。
比如APP首屏商品資料介面,這些資料是公共的不會針對使用者自定義,而且這些資料不會頻繁的更新,像這種介面的請求量比較大就可以加入一級快取;
伺服器架構圖:
合理地規範和使用NoSQL快取資料庫,根據業務拆分快取資料庫的叢集,這樣基本可以很好支援業務,一級快取畢竟是使用站點伺服器快取所以還是要善用。
4)靜態化資料
高併發請求資料不變化的情況下如果可以不請求自己的伺服器獲取資料那就可以減少伺服器的資源壓力。
對於更新頻繁度不高,並且資料允許短時間內的延遲,可以通過資料靜態化成JSON、XML、HTML等資料檔案上傳CDN,在拉取資料的時候優先到CDN拉取,如果沒有獲取到資料再從快取,資料庫中獲取,當管理人員操作後臺編輯資料再重新生成靜態檔案上傳同步到CDN,這樣在高併發的時候可以使資料的獲取命中在CDN伺服器上。
CDN節點同步有一定的延遲性,所以找一個靠譜的CDN伺服器商也很重要。
5)其他方案
對於更新頻繁度不高的資料,APP、PC瀏覽器,可以快取資料到本地,然後每次請求介面的時候上傳當前快取資料的版本號,服務端接收到版本號判斷版本號與最新資料版本號是否一致,如果不一樣就進行最新資料的查詢並返回最新資料和最新版本號,如果一樣就返回狀態碼告知資料已經是最新。減少伺服器壓力:資源、頻寬。